In modern cryptography, secure key management is as important as the encryption algorithms themselves. A single master key often needs to generate multiple independent keys for different purposes—encryption, authentication, IV generation, etc. HKDF (HMAC-based Key Derivation Function) provides a standardized, cryptographically secure mechanism for deriving strong keys from a master secret. For Java applications, HKDF offers a robust foundation for key management in protocols, data encryption, and authentication systems.
What is HKDF?
HKDF is a key derivation function specified in RFC 5869 that uses HMAC as its pseudorandom function. It operates in two distinct phases:
- Extract: "Extracts" a fixed-length pseudorandom key (PRK) from the input key material (IKM)
- Expand: "Expands" the PRK into multiple cryptographically strong keys of desired length
Key features of HKDF:
- Salt support: Optional salt for additional security
- Context binding: Info parameter binds derived keys to specific contexts
- Variable output: Generates any amount of key material
- NIST approved: Based on HMAC, a widely standardized primitive
Why HKDF is Essential for Java Applications
- Key Separation: Derive different keys for different purposes from a single master key
- Forward Security: Regular key rotation without changing the master secret
- Protocol Implementation: Required for TLS 1.3, Signal Protocol, and many modern crypto systems
- Password-Based Encryption: Combined with PBKDF2 or Argon2 for strong key derivation
- Deterministic Key Generation: Reproducible keys when needed (with same inputs)
Implementing HKDF in Java
1. Basic HKDF Implementation
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* HKDF implementation based on RFC 5869
*/
public class HKDF {
private static final String HMAC_ALGORITHM = "HmacSHA256";
/**
* Extract phase: PRK = HMAC-Hash(salt, IKM)
*/
public byte[] extract(byte[] salt, byte[] inputKeyMaterial)
throws NoSuchAlgorithmException, InvalidKeyException {
// If salt is null or empty, use a zero-filled array
if (salt == null || salt.length == 0) {
salt = new byte[32]; // SHA-256 hash length
}
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(new SecretKeySpec(salt, HMAC_ALGORITHM));
return mac.doFinal(inputKeyMaterial);
}
/**
* Expand phase: OKM = HKDF-Expand(PRK, info, L)
*/
public byte[] expand(byte[] pseudorandomKey, byte[] info, int length)
throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(new SecretKeySpec(pseudorandomKey, HMAC_ALGORITHM));
byte[] result = new byte[length];
byte[] t = new byte[0];
int pos = 0;
for (byte counter = 1; pos < length; counter++) {
// T(i) = HMAC-Hash(PRK, T(i-1) | info | counter)
mac.update(t);
if (info != null) {
mac.update(info);
}
mac.update(counter);
t = mac.doFinal();
// Copy as much as needed
int toCopy = Math.min(t.length, length - pos);
System.arraycopy(t, 0, result, pos, toCopy);
pos += toCopy;
}
return result;
}
/**
* Combined extract-then-expand operation
*/
public byte[] deriveKey(byte[] salt, byte[] inputKeyMaterial,
byte[] info, int length)
throws NoSuchAlgorithmException, InvalidKeyException {
byte[] prk = extract(salt, inputKeyMaterial);
return expand(prk, info, length);
}
}
2. Enhanced HKDF with Multiple Algorithms
public class HKDFAdvanced {
public enum HashAlgorithm {
SHA256("HmacSHA256", 32),
SHA384("HmacSHA384", 48),
SHA512("HmacSHA512", 64);
public final String hmacAlgorithm;
public final int hashLength;
HashAlgorithm(String hmacAlgorithm, int hashLength) {
this.hmacAlgorithm = hmacAlgorithm;
this.hashLength = hashLength;
}
}
private final HashAlgorithm algorithm;
private final Mac hmac;
public HKDFAdvanced() {
this(HashAlgorithm.SHA256);
}
public HKDFAdvanced(HashAlgorithm algorithm) {
this.algorithm = algorithm;
try {
this.hmac = Mac.getInstance(algorithm.hmacAlgorithm);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Algorithm not available: " + algorithm, e);
}
}
/**
* Extract phase with validation
*/
public byte[] extract(byte[] salt, byte[] inputKeyMaterial) {
if (inputKeyMaterial == null || inputKeyMaterial.length == 0) {
throw new IllegalArgumentException("Input key material cannot be null or empty");
}
try {
// If salt is null or empty, use zeros of hash length
if (salt == null || salt.length == 0) {
salt = new byte[algorithm.hashLength];
}
Mac mac = (Mac) hmac.clone();
mac.init(new SecretKeySpec(salt, algorithm.hmacAlgorithm));
return mac.doFinal(inputKeyMaterial);
} catch (CloneNotSupportedException | InvalidKeyException e) {
throw new RuntimeException("HKDF extract failed", e);
}
}
/**
* Expand phase with validation
*/
public byte[] expand(byte[] pseudorandomKey, byte[] info, int length) {
if (pseudorandomKey == null || pseudorandomKey.length == 0) {
throw new IllegalArgumentException("PRK cannot be null or empty");
}
if (length <= 0) {
throw new IllegalArgumentException("Output length must be positive");
}
int maxLength = algorithm.hashLength * 255;
if (length > maxLength) {
throw new IllegalArgumentException(
String.format("Requested length %d exceeds maximum %d", length, maxLength));
}
try {
Mac mac = (Mac) hmac.clone();
mac.init(new SecretKeySpec(pseudorandomKey, algorithm.hmacAlgorithm));
byte[] result = new byte[length];
byte[] t = new byte[0];
int pos = 0;
for (byte counter = 1; pos < length; counter++) {
mac.update(t);
if (info != null) {
mac.update(info);
}
mac.update(counter);
t = mac.doFinal();
int toCopy = Math.min(t.length, length - pos);
System.arraycopy(t, 0, result, pos, toCopy);
pos += toCopy;
}
return result;
} catch (CloneNotSupportedException | InvalidKeyException e) {
throw new RuntimeException("HKDF expand failed", e);
}
}
/**
* One-shot derivation
*/
public byte[] deriveKey(byte[] salt, byte[] inputKeyMaterial,
byte[] info, int length) {
byte[] prk = extract(salt, inputKeyMaterial);
return expand(prk, info, length);
}
/**
* Derive multiple keys in one operation
*/
public DeriveResult deriveKeys(byte[] salt, byte[] inputKeyMaterial, KeyRequest... requests) {
byte[] prk = extract(salt, inputKeyMaterial);
DeriveResult result = new DeriveResult();
for (KeyRequest request : requests) {
byte[] key = expand(prk, request.info, request.length);
result.addKey(request.name, key);
}
return result;
}
public static class KeyRequest {
public final String name;
public final byte[] info;
public final int length;
public KeyRequest(String name, byte[] info, int length) {
this.name = name;
this.info = info;
this.length = length;
}
}
public static class DeriveResult {
private final Map<String, byte[]> keys = new LinkedHashMap<>();
private void addKey(String name, byte[] key) {
keys.put(name, key);
}
public byte[] getKey(String name) {
return keys.get(name);
}
public Map<String, byte[]> getAllKeys() {
return Collections.unmodifiableMap(keys);
}
}
}
3. HKDF Utility Class with Common Use Cases
public class HKDFUtil {
private static final HKDFAdvanced hkdf = new HKDFAdvanced();
// Common context strings for different key types
public static final byte[] CONTEXT_ENCRYPTION = "encryption".getBytes(StandardCharsets.UTF_8);
public static final byte[] CONTEXT_AUTHENTICATION = "authentication".getBytes(StandardCharsets.UTF_8);
public static final byte[] CONTEXT_IV = "initialization-vector".getBytes(StandardCharsets.UTF_8);
public static final byte[] CONTEXT_MAC = "message-authentication".getBytes(StandardCharsets.UTF_8);
public static final byte[] CONTEXT_SESSION = "session-key".getBytes(StandardCharsets.UTF_8);
public static final byte[] CONTEXT_NONCE = "nonce-generation".getBytes(StandardCharsets.UTF_8);
/**
* Derive an AES encryption key
*/
public static SecretKey deriveAESKey(byte[] masterSecret, byte[] salt, int keySizeBits)
throws Exception {
int keySizeBytes = keySizeBits / 8;
byte[] keyBytes = hkdf.deriveKey(salt, masterSecret, CONTEXT_ENCRYPTION, keySizeBytes);
return new SecretKeySpec(keyBytes, "AES");
}
/**
* Derive an Hmac key for authentication
*/
public static SecretKey deriveHmacKey(byte[] masterSecret, byte[] salt, String algorithm)
throws Exception {
int keySize = switch (algorithm) {
case "HmacSHA256" -> 32;
case "HmacSHA384" -> 48;
case "HmacSHA512" -> 64;
default -> 32;
};
byte[] keyBytes = hkdf.deriveKey(salt, masterSecret, CONTEXT_AUTHENTICATION, keySize);
return new SecretKeySpec(keyBytes, algorithm);
}
/**
* Derive an initialization vector (IV)
*/
public static byte[] deriveIV(byte[] masterSecret, byte[] salt, int ivLength)
throws Exception {
return hkdf.deriveKey(salt, masterSecret, CONTEXT_IV, ivLength);
}
/**
* Derive a complete set of keys for symmetric encryption
*/
public static EncryptionKeySet deriveEncryptionKeys(byte[] masterSecret, byte[] salt)
throws Exception {
byte[] prk = hkdf.extract(salt, masterSecret);
EncryptionKeySet keySet = new EncryptionKeySet();
keySet.encryptionKey = new SecretKeySpec(
hkdf.expand(prk, CONTEXT_ENCRYPTION, 32), "AES");
keySet.authenticationKey = new SecretKeySpec(
hkdf.expand(prk, CONTEXT_AUTHENTICATION, 32), "HmacSHA256");
keySet.iv = hkdf.expand(prk, CONTEXT_IV, 12);
return keySet;
}
public static class EncryptionKeySet {
public SecretKey encryptionKey;
public SecretKey authenticationKey;
public byte[] iv;
}
}
Practical Applications
1. Secure Session Key Derivation
@Service
public class SessionKeyService {
private final HKDFAdvanced hkdf = new HKDFAdvanced();
private final SecureRandom secureRandom = new SecureRandom();
public static class SessionKeys {
public final SecretKey clientToServerKey;
public final SecretKey serverToClientKey;
public final SecretKey clientMacKey;
public final SecretKey serverMacKey;
public final byte[] clientIV;
public final byte[] serverIV;
public SessionKeys(SecretKey c2s, SecretKey s2c, SecretKey cMac,
SecretKey sMac, byte[] cIV, byte[] sIV) {
this.clientToServerKey = c2s;
this.serverToClientKey = s2c;
this.clientMacKey = cMac;
this.serverMacKey = sMac;
this.clientIV = cIV;
this.serverIV = sIV;
}
}
public SessionKeys deriveSessionKeys(byte[] sharedSecret, byte[] sessionId) {
// Generate random salt
byte[] salt = new byte[32];
secureRandom.nextBytes(salt);
// Extract PRK
byte[] prk = hkdf.extract(salt, sharedSecret);
// Define context for different keys
byte[] c2sContext = concat(sessionId, "client-to-server".getBytes());
byte[] s2cContext = concat(sessionId, "server-to-client".getBytes());
byte[] cMacContext = concat(sessionId, "client-mac".getBytes());
byte[] sMacContext = concat(sessionId, "server-mac".getBytes());
byte[] cIVContext = concat(sessionId, "client-iv".getBytes());
byte[] sIVContext = concat(sessionId, "server-iv".getBytes());
// Derive all keys
SecretKey c2sKey = new SecretKeySpec(
hkdf.expand(prk, c2sContext, 32), "AES");
SecretKey s2cKey = new SecretKeySpec(
hkdf.expand(prk, s2cContext, 32), "AES");
SecretKey cMacKey = new SecretKeySpec(
hkdf.expand(prk, cMacContext, 32), "HmacSHA256");
SecretKey sMacKey = new SecretKeySpec(
hkdf.expand(prk, sMacContext, 32), "HmacSHA256");
byte[] cIV = hkdf.expand(prk, cIVContext, 12);
byte[] sIV = hkdf.expand(prk, sIVContext, 12);
return new SessionKeys(c2sKey, s2cKey, cMacKey, sMacKey, cIV, sIV);
}
private byte[] concat(byte[] a, byte[] b) {
byte[] result = new byte[a.length + b.length];
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}
}
2. Hierarchical Key Derivation
public class HierarchicalKeyDerivation {
private static final HKDFAdvanced hkdf = new HKDFAdvanced();
public static class DerivedKeys {
private final Map<String, byte[]> keys = new ConcurrentHashMap<>();
public void addKey(String path, byte[] key) {
keys.put(path, key);
}
public byte[] getKey(String path) {
return keys.get(path);
}
}
/**
* Derive keys in a hierarchical structure
* Root key -> user key -> purpose key
*/
public static byte[] deriveHierarchicalKey(byte[] masterKey, String... path)
throws Exception {
byte[] currentKey = masterKey;
byte[] salt = "hierarchical-salt".getBytes();
for (String component : path) {
byte[] info = component.getBytes(StandardCharsets.UTF_8);
currentKey = hkdf.deriveKey(salt, currentKey, info, 32);
salt = currentKey; // Use derived key as salt for next level
}
return currentKey;
}
/**
* Application: Multi-tenant key management
*/
public static class TenantKeyManager {
private final byte[] masterKey;
private final byte[] tenantSalt;
public TenantKeyManager(byte[] masterKey, byte[] tenantSalt) {
this.masterKey = masterKey;
this.tenantSalt = tenantSalt;
}
public TenantKeys getTenantKeys(String tenantId) throws Exception {
byte[] tenantRoot = deriveHierarchicalKey(masterKey, "tenants", tenantId);
return new TenantKeys(
deriveHierarchicalKey(tenantRoot, "encryption"),
deriveHierarchicalKey(tenantRoot, "signing"),
deriveHierarchicalKey(tenantRoot, "database")
);
}
}
public record TenantKeys(byte[] encryptionKey, byte[] signingKey, byte[] databaseKey) {}
}
3. Password-Based Key Derivation with HKDF
public class PasswordBasedHKDF {
private static final int PBKDF2_ITERATIONS = 100000;
private static final int SALT_LENGTH = 32;
/**
* Combine PBKDF2 (for password stretching) with HKDF (for key expansion)
*/
public static SecretKey deriveKeyFromPassword(
char[] password, byte[] salt, String purpose, int keyLength)
throws Exception {
// Step 1: Use PBKDF2 to stretch the password
PBEKeySpec spec = new PBEKeySpec(password, salt, PBKDF2_ITERATIONS, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] stretchedKey = factory.generateSecret(spec).getEncoded();
// Step 2: Use HKDF to derive purpose-specific key
HKDFAdvanced hkdf = new HKDFAdvanced();
byte[] purposeBytes = purpose.getBytes(StandardCharsets.UTF_8);
byte[] keyBytes = hkdf.deriveKey(null, stretchedKey, purposeBytes, keyLength);
return new SecretKeySpec(keyBytes, "AES");
}
/**
* Complete password-based encryption system
*/
public static class PasswordBasedEncryption {
public static EncryptedData encrypt(byte[] plaintext, char[] password)
throws Exception {
// Generate random salt
SecureRandom rng = new SecureRandom();
byte[] salt = new byte[SALT_LENGTH];
rng.nextBytes(salt);
// Generate random nonce
byte[] nonce = new byte[12];
rng.nextBytes(nonce);
// Derive encryption key
SecretKey key = deriveKeyFromPassword(password, salt, "encryption", 32);
// Encrypt
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext);
return new EncryptedData(salt, nonce, ciphertext);
}
public static byte[] decrypt(EncryptedData data, char[] password)
throws Exception {
SecretKey key = deriveKeyFromPassword(password, data.salt, "encryption", 32);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, data.nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(data.ciphertext);
}
public record EncryptedData(byte[] salt, byte[] nonce, byte[] ciphertext) {}
}
}
Testing and Validation
1. HKDF Test Cases
@Test
public class HKDFTest {
@Test
public void testRFC5869TestVectors() throws Exception {
// Test vectors from RFC 5869, Appendix A
HKDF hkdf = new HKDF();
// Test Case 1
byte[] ikm = hexToBytes("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
byte[] salt = hexToBytes("000102030405060708090a0b0c");
byte[] info = hexToBytes("f0f1f2f3f4f5f6f7f8f9");
byte[] okm = hkdf.deriveKey(salt, ikm, info, 42);
byte[] expected = hexToBytes(
"3cb25f25faacd57a90434f64d0362f2a" +
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf" +
"34007208d5b887185865");
assertArrayEquals(expected, okm);
}
@Test
public void testKeySeparation() throws Exception {
byte[] masterKey = "master-secret-12345".getBytes();
byte[] salt = "salt-value".getBytes();
HKDFAdvanced hkdf = new HKDFAdvanced();
// Derive keys for different purposes
byte[] encKey = hkdf.deriveKey(salt, masterKey, "encryption".getBytes(), 32);
byte[] authKey = hkdf.deriveKey(salt, masterKey, "auth".getBytes(), 32);
byte[] iv = hkdf.deriveKey(salt, masterKey, "iv".getBytes(), 12);
// Keys should be different
assertFalse(Arrays.equals(encKey, authKey));
assertFalse(Arrays.equals(encKey, iv));
// Deterministic: same inputs produce same outputs
byte[] encKey2 = hkdf.deriveKey(salt, masterKey, "encryption".getBytes(), 32);
assertArrayEquals(encKey, encKey2);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidLength() throws Exception {
HKDFAdvanced hkdf = new HKDFAdvanced();
byte[] prk = new byte[32];
hkdf.expand(prk, null, 10000); // Exceeds maximum
}
}
2. Integration Test with Real Crypto
@Test
public class HKDFIntegrationTest {
@Test
public void testEncryptionWithDerivedKeys() throws Exception {
// Setup
byte[] masterSecret = "top-secret-master-key".getBytes();
byte[] salt = "unique-salt-per-session".getBytes();
// Derive keys using HKDF
HKDFUtil.EncryptionKeySet keySet = HKDFUtil.deriveEncryptionKeys(masterSecret, salt);
// Test data
String plaintext = "Sensitive information that needs protection";
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
// Encrypt
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, keySet.iv);
cipher.init(Cipher.ENCRYPT_MODE, keySet.encryptionKey, spec);
byte[] ciphertext = cipher.doFinal(plaintextBytes);
byte[] authTag = Arrays.copyOfRange(ciphertext,
ciphertext.length - 16, ciphertext.length);
// Decrypt
cipher.init(Cipher.DECRYPT_MODE, keySet.encryptionKey, spec);
byte[] decrypted = cipher.doFinal(ciphertext);
// Verify
assertArrayEquals(plaintextBytes, decrypted);
}
}
Performance Benchmark
@Component
public class HKDFBenchmark {
private static final int ITERATIONS = 10000;
public void runBenchmark() throws Exception {
HKDFAdvanced hkdf = new HKDFAdvanced();
byte[] ikm = new byte[32];
byte[] salt = new byte[32];
new SecureRandom().nextBytes(ikm);
new SecureRandom().nextBytes(salt);
// Warmup
for (int i = 0; i < 1000; i++) {
hkdf.deriveKey(salt, ikm, "test".getBytes(), 32);
}
// Benchmark extract
long start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
hkdf.extract(salt, ikm);
}
long extractTime = System.nanoTime() - start;
// Benchmark expand
byte[] prk = hkdf.extract(salt, ikm);
start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
hkdf.expand(prk, "test".getBytes(), 32);
}
long expandTime = System.nanoTime() - start;
System.out.printf("HKDF Performance (%d iterations):%n", ITERATIONS);
System.out.printf(" Extract: %.2f µs/op%n", extractTime / ITERATIONS / 1000.0);
System.out.printf(" Expand: %.2f µs/op%n", expandTime / ITERATIONS / 1000.0);
System.out.printf(" Total: %.2f µs/op%n",
(extractTime + expandTime) / ITERATIONS / 1000.0);
}
}
Best Practices for HKDF in Java
- Always Use Salt: Even if null, HKDF handles it, but explicit salt adds security
- Unique Info per Purpose: Use distinct info strings for different key types
- Proper Key Lengths: Ensure derived keys match algorithm requirements
- Secure Random Salts: Generate salts using SecureRandom for each derivation
- Context Binding: Include protocol/version info to prevent cross-protocol attacks
- Key Destruction: Zero out derived keys when no longer needed
- Thread Safety: Use separate HKDF instances or synchronize properly
Configuration Example
# application.yml security: key-derivation: hkdf: algorithm: HmacSHA256 extract-salt-length: 32 default-output-length: 32 keys: encryption: algorithm: AES key-size: 256 context: "encryption-v1" mac: algorithm: HmacSHA256 key-size: 256 context: "mac-v1" session: client-encryption: length: 32 context: "session-client-enc" client-mac: length: 32 context: "session-client-mac"
Conclusion
HKDF provides Java applications with a standardized, secure, and flexible mechanism for key derivation that is essential for modern cryptographic systems. By separating key extraction from expansion and supporting context binding, HKDF enables robust key management practices that prevent key reuse and enable key separation.
Key advantages of HKDF in Java:
- Standardized approach based on RFC 5869
- Flexible key derivation for multiple purposes
- Secure when combined with strong HMAC algorithms
- Efficient with minimal performance overhead
- Interoperable with other cryptographic systems
For applications handling sensitive data, implementing HKDF correctly is not just a best practice—it's a fundamental requirement for cryptographic key hygiene. Whether building secure communication protocols, implementing file encryption, or managing session keys, HKDF provides the cryptographic foundation needed to ensure keys are derived securely and appropriately for each specific purpose.
Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)
Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/
Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/
Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/
Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/
Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/
Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/
Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/
Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/
Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/