Method Signature

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
  • key — the key to look up
  • mappingFunction — receives the key; its return value is inserted as the new value
  • Returns — the existing value if the key was already present, or the newly computed value
  • Side effect — if the function returns null, no entry is inserted

Basic Example

import java.util.HashMap;
import java.util.Map;

public class ComputeIfAbsentExample {

    public static void main(String[] args) {
        Map<Integer, String> numbers = new HashMap<>();
        numbers.put(1, "one");
        numbers.put(2, "two");
        numbers.put(3, "three");

        System.out.println("Before: " + numbers);

        // Key 4 is absent — value will be computed and inserted
        numbers.computeIfAbsent(4, k -> "four");

        // Key 5 is absent — lambda shorthand
        numbers.computeIfAbsent(5, k -> "five");

        // Key 1 already exists — no change
        numbers.computeIfAbsent(1, k -> "ONE");

        System.out.println("After:  " + numbers);
    }
}

Output:

Before: {1=one, 2=two, 3=three}
After:  {1=one, 2=two, 3=three, 4=four, 5=five}

Key 1 remains "one"computeIfAbsent() never overwrites existing values.


Common Pattern: Map of Lists

computeIfAbsent() is especially useful for building a Map<K, List<V>> — the classic "group by" pattern:

import java.util.*;

public class GroupByExample {

    public static void main(String[] args) {
        List<String> words = List.of("apple", "ant", "banana", "avocado", "blueberry");

        Map<Character, List<String>> grouped = new HashMap<>();

        for (String word : words) {
            char firstLetter = word.charAt(0);
            // Create the list if this letter hasn't been seen yet
            grouped.computeIfAbsent(firstLetter, k -> new ArrayList<>()).add(word);
        }

        grouped.forEach((letter, list) ->
                System.out.println(letter + ": " + list));
    }
}

Output:

a: [apple, ant, avocado]
b: [banana, blueberry]

Without computeIfAbsent(), you'd need to check whether the list exists, create it if not, then add — three lines instead of one.


Before vs. After Java 8

Old approach:

if (!grouped.containsKey(firstLetter)) {
    grouped.put(firstLetter, new ArrayList<>());
}
grouped.get(firstLetter).add(word);

With computeIfAbsent():

grouped.computeIfAbsent(firstLetter, k -> new ArrayList<>()).add(word);

Related Map Compute Methods

MethodBehavior
computeIfAbsent(k, fn)Insert only if key is absent
computeIfPresent(k, fn)Update only if key exists
compute(k, fn)Always compute; handles both cases
getOrDefault(k, default)Read-only; returns default without inserting
putIfAbsent(k, v)Insert a fixed value if absent (no function)

Memoization with computeIfAbsent()

computeIfAbsent() is perfect for caching computed results — only run the expensive computation once per key:

Map<Integer, Long> fibCache = new HashMap<>();

long fibonacci(int n) {
    if (n <= 1) return n;
    return fibCache.computeIfAbsent(n, k -> fibonacci(k - 1) + fibonacci(k - 2));
}

System.out.println(fibonacci(40)); // 102334155

The function is called at most once per key. On subsequent calls with the same key, the cached value is returned immediately. This turns an exponential recursion into a linear one.


Thread-Safe Use with ConcurrentHashMap

computeIfAbsent() is atomic on ConcurrentHashMap — the check and insertion happen as a single operation. This is important in concurrent environments where two threads might try to initialize the same key simultaneously:

import java.util.concurrent.ConcurrentHashMap;

ConcurrentHashMap<String, List<String>> groupedResults = new ConcurrentHashMap<>();

// Safe to call from multiple threads simultaneously
groupedResults.computeIfAbsent("category-A", k -> new ArrayList<>()).add("item1");
groupedResults.computeIfAbsent("category-A", k -> new ArrayList<>()).add("item2");

With a regular HashMap, this pattern is not thread-safe — two threads could both call the mapping function and the second would overwrite the first's result. Always use ConcurrentHashMap for shared maps accessed from multiple threads.


computeIfAbsent() vs putIfAbsent()

Both insert a value only if the key is absent, but there's an important difference:

// putIfAbsent — the new ArrayList is ALWAYS created, even if the key exists
map.putIfAbsent(key, new ArrayList<>());

// computeIfAbsent — the lambda is ONLY called if the key is absent
map.computeIfAbsent(key, k -> new ArrayList<>());
MethodWhen is value computed?Takes a function?
computeIfAbsent(k, fn)Only if key absentYes — receives the key
putIfAbsent(k, v)Always (value evaluated before call)No — fixed value

For values that are cheap to create (like a constant string), putIfAbsent() is fine. For values that are expensive to create (like a new database connection or a populated list), always use computeIfAbsent() to avoid unnecessary work.


Summary

computeIfAbsent() shines when you need lazy initialization of map values — especially for Map<K, List<V>> or Map<K, Set<V>> grouping patterns and for memoization caches. Prefer it over putIfAbsent() when the value is expensive to create. Use it with ConcurrentHashMap for thread-safe lazy initialization.

Removing map entries by value (rather than by key) requires a slightly different approach than map.remove(key). Java provides clean, expressive ways to do this using the map's values collection.


The Setup

All examples below use this map:

import java.util.*;

Map<Integer, String> inventory = new HashMap<>();
inventory.put(1, "apple");
inventory.put(2, "banana");
inventory.put(3, "cherry");
inventory.put(4, "banana");
inventory.put(5, "date");

Goal: remove all entries where the value is "banana" (keys 2 and 4).


Method 1: removeIf() on values()

The cleanest, most expressive approach:

inventory.values().removeIf("banana"::equals);

System.out.println(inventory);
// {1=apple, 3=cherry, 5=date}

map.values() returns a live view of the map's values. Modifying it (including removing) directly modifies the underlying map. removeIf() accepts a predicate and removes all matching entries.

With a lambda:

inventory.values().removeIf(value -> value.equals("banana"));

Method 2: removeAll() on values()

Removes all entries matching any value in a given collection:

inventory.values().removeAll(Collections.singleton("banana"));

Collections.singleton("banana") creates a single-element set containing "banana". This is equivalent to removeIf("banana"::equals) but useful when you want to remove multiple distinct values at once:

// Remove all entries with value "banana" OR "cherry"
inventory.values().removeAll(Set.of("banana", "cherry"));

Method 3: entrySet() with removeIf()

Use this when your removal condition depends on both the key and the value:

// Remove all entries where the key is even AND the value starts with "b"
inventory.entrySet().removeIf(entry ->
        entry.getKey() % 2 == 0 && entry.getValue().startsWith("b"));

This is the most flexible approach for complex conditions.


Method 4: Iterator (Pre-Java 8)

For environments that can't use lambdas:

Iterator<Map.Entry<Integer, String>> it = inventory.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<Integer, String> entry = it.next();
    if ("banana".equals(entry.getValue())) {
        it.remove(); // safe removal during iteration
    }
}
Do not use map.remove() inside a for-each loop — it will throw ConcurrentModificationException. The iterator's remove() method is the safe way to delete during iteration.

Comparison

MethodReadable?Complex conditions?Java version
values().removeIf()ExcellentValue onlyJava 8+
values().removeAll()GoodMultiple valuesJava 8+
entrySet().removeIf()GoodKey + valueJava 8+
IteratorVerboseKey + valueAll

Removing Null Values

To remove all entries where the value is null, the same removeIf() approach works — but use Objects.isNull() instead of .equals() to avoid a NullPointerException:

import java.util.Objects;

Map<String, String> config = new HashMap<>();
config.put("host", "localhost");
config.put("port", null);
config.put("user", "admin");
config.put("pass", null);

config.values().removeIf(Objects::isNull);

System.out.println(config); // {host=localhost, user=admin}

Calling "banana"::equals on a null value would throw a NullPointerException, so always use Objects::isNull or a null-safe predicate when nulls are possible.


Producing a New Filtered Map (Without Mutating the Original)

All the methods above modify the map in-place. If you want to keep the original and produce a new filtered map, use a stream:

Map<Integer, String> filtered = inventory.entrySet()
        .stream()
        .filter(entry -> !entry.getValue().equals("banana"))
        .collect(Collectors.toMap(
            Map.Entry::getKey,
            Map.Entry::getValue
        ));

System.out.println(inventory); // original unchanged
System.out.println(filtered);  // {1=apple, 3=cherry, 5=date}

This is the right approach when the original map must remain intact — for example, when it's a read-only view shared across multiple parts of your application.


Pitfall: Unmodifiable Maps

If your map was created with Map.of(), Map.copyOf(), or Collections.unmodifiableMap(), all the removal methods above will throw UnsupportedOperationException:

// This map is unmodifiable — removal will fail
Map<Integer, String> immutable = Map.of(1, "apple", 2, "banana");
immutable.values().removeIf("banana"::equals); // UnsupportedOperationException!

// Make a mutable copy first
Map<Integer, String> mutable = new HashMap<>(immutable);
mutable.values().removeIf("banana"::equals);

If you receive a map from an external source and don't know whether it's mutable, wrap it in new HashMap<>(map) before attempting any removal.


Summary

For most cases, values().removeIf(value -> ...) is the cleanest option. When you need to check both key and value, use entrySet().removeIf(entry -> ...). Use Objects::isNull when removing null values. If you need to preserve the original, produce a new filtered map with a stream instead of mutating. Avoid modifying the map inside a for-each loop — always use removeIf() or an explicit iterator.

There are a few ways to remove all entries from a Java Map. The right choice depends on whether other parts of your code hold a reference to the same map object, and whether thread safety matters.


Method 1: clear() — Empties the Existing Map Object

clear() removes all key-value pairs from the map in-place. The map object itself remains — it's the same reference, now empty. All implementations of the Map interface support it: HashMap, TreeMap, LinkedHashMap, ConcurrentHashMap, and others.

import java.util.HashMap;
import java.util.Map;

public class ClearMapExample {

    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "one");
        map.put(2, "two");
        map.put(3, "three");
        map.put(4, "four");
        map.put(5, "five");

        System.out.println("Before: " + map);
        System.out.println("Size:   " + map.size());

        map.clear();

        System.out.println("After:  " + map);
        System.out.println("Size:   " + map.size());
    }
}

Output:

Before: {1=one, 2=two, 3=three, 4=four, 5=five}
Size:   5
After:  {}
Size:   0

After clear(), isEmpty() returns true and size() returns 0. The map is ready to be repopulated.


Method 2: Reassignment — Replace With a New Map

If the variable is local or only you hold a reference to it, replacing it with a new map is equally valid:

Map<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");

// Replace with a fresh empty map
map = new HashMap<>();

System.out.println(map); // {}

Important caveat: If another variable also points to the original map, that variable still sees the old entries. The reassignment only changes this reference, not the underlying object.

Map<Integer, String> original = new HashMap<>();
original.put(1, "one");

Map<Integer, String> alias = original; // both point to same object

original = new HashMap<>(); // only original changes reference

System.out.println(alias);    // {1=one} — alias still sees old data
System.out.println(original); // {} — new empty map

When in doubt, use clear() — it affects the shared object rather than just the local pointer.


clear() Across Map Implementations

The behavior of clear() is consistent across all standard Map implementations:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

Map<String, Integer> hashMap = new HashMap<>();
Map<String, Integer> treeMap = new TreeMap<>();
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();

hashMap.put("a", 1);
treeMap.put("b", 2);
linkedHashMap.put("c", 3);
concurrentMap.put("d", 4);

hashMap.clear();
treeMap.clear();
linkedHashMap.clear();
concurrentMap.clear();

System.out.println(hashMap.isEmpty());       // true
System.out.println(treeMap.isEmpty());       // true
System.out.println(linkedHashMap.isEmpty()); // true
System.out.println(concurrentMap.isEmpty()); // true

Thread Safety

If your map is accessed from multiple threads, the approach matters:

Map Typeclear() Thread-Safe?Notes
HashMapNoRequires external synchronization
Collections.synchronizedMap()YesSynchronized on the map object
ConcurrentHashMapYesInternally handles concurrent access
TreeMapNoRequires external synchronization

For a ConcurrentHashMap, clear() is safe to call from multiple threads. For a regular HashMap, always synchronize externally if other threads might access the map during the clear:

synchronized (map) {
    map.clear();
}

Checking if a Map is Already Empty

Use isEmpty() to check before operating on a map you expect to be empty — or to guard against redundant work:

if (!map.isEmpty()) {
    map.clear();
}

// Or simply — clear() on an empty map is a no-op, safe to call unconditionally
map.clear();

Calling clear() on an already-empty map is safe and does nothing. There's no need to check first.


clear() vs. Iterating and Removing

Before Java had clear(), you might have removed entries one by one with an iterator. This is verbose and much slower:

// Old way — DO NOT do this
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
    it.next();
    it.remove();
}

// Modern way — one method call
map.clear();

clear() is O(n) just like iterating, but its internal implementation is optimized and avoids iterator overhead.


Summary

Use clear() when you want to empty the existing map object in place — especially when other code holds a reference to the same map. Use reassignment (map = new HashMap<>()) only when you're certain no other variable shares a reference to the old map. For concurrent access, ConcurrentHashMap.clear() is safe without additional synchronization.

BigInteger is Java's class for representing arbitrarily large integers — numbers with no practical upper or lower limit. It lives in java.math and is the right tool whenever your values can overflow int or long.


Why int and long Aren't Always Enough

Java's primitive integer types have hard limits:

TypeMin ValueMax Value
int-2,147,483,6482,147,483,647 (~2.1 billion)
long-9,223,372,036,854,775,8089,223,372,036,854,775,807 (~9.2 quintillion)

When a computation exceeds these bounds, the result silently wraps around — no exception, just a wrong answer:

int x = Integer.MAX_VALUE;
System.out.println(x + 1); // -2147483648 — silent overflow!

long y = Long.MAX_VALUE;
System.out.println(y + 1); // -9223372036854775808 — silent overflow!

BigInteger has no such limit. It allocates as much memory as needed to represent the value exactly.


Creating BigInteger Values

import java.math.BigInteger;

// From a long value
BigInteger a = BigInteger.valueOf(15);

// From a string — the only way for numbers larger than long
BigInteger huge = new BigInteger("123456789012345678901234567890");

// Built-in constants
BigInteger zero = BigInteger.ZERO;
BigInteger one  = BigInteger.ONE;
BigInteger two  = BigInteger.TWO;
BigInteger ten  = BigInteger.TEN;

Arithmetic Operations

BigInteger is immutable — every operation returns a new object. Use the result, don't just call the method:

BigInteger a = BigInteger.valueOf(15);
BigInteger b = BigInteger.valueOf(4);

System.out.println(a.add(b));      // 19
System.out.println(a.subtract(b)); // 11
System.out.println(a.multiply(b)); // 60
System.out.println(a.divide(b));   // 3 (integer division)
System.out.println(a.remainder(b));// 3 (a % b)
System.out.println(a.pow(3));      // 3375 (15^3)
System.out.println(a.abs());       // 15
System.out.println(a.negate());    // -15
System.out.println(a.gcd(b));      // 1 (greatest common divisor)

Real-World Example: Large Factorial

Computing 50! overflows long at around 20!. BigInteger handles it without issue:

public static BigInteger factorial(int n) {
    BigInteger result = BigInteger.ONE;
    for (int i = 2; i <= n; i++) {
        result = result.multiply(BigInteger.valueOf(i));
    }
    return result;
}

System.out.println(factorial(20));
// 2432902008176640000 — fits in long

System.out.println(factorial(50));
// 30414093201713378043612608166979581188299763898377856000000000000
// — impossible in long, exact in BigInteger

Comparison and Equality

Never use == to compare BigInteger values — it compares object references, not numeric values. Use compareTo() or equals():

BigInteger x = BigInteger.valueOf(100);
BigInteger y = BigInteger.valueOf(100);
BigInteger z = BigInteger.valueOf(200);

System.out.println(x == y);           // false (different objects)
System.out.println(x.equals(y));      // true
System.out.println(x.compareTo(y));   // 0 (equal)
System.out.println(x.compareTo(z));   // -1 (x < z)
System.out.println(z.compareTo(x));   // 1 (z > x)
System.out.println(x.max(z));         // 200
System.out.println(x.min(z));         // 100

Converting Between Types

BigInteger big = BigInteger.valueOf(42);

int    asInt    = big.intValue();     // 42 (throws if value doesn't fit)
long   asLong   = big.longValue();    // 42L
double asDouble = big.doubleValue();  // 42.0 (may lose precision)
String asString = big.toString();     // "42"
String asHex    = big.toString(16);   // "2a" (hexadecimal)
String asBinary = big.toString(2);    // "101010" (binary)

Use intValueExact() or longValueExact() (Java 8+) if you want an ArithmeticException instead of silent truncation when the value doesn't fit.


When to Use BigInteger

  • Factorial and combinatorics — values grow faster than long can hold
  • Cryptographic algorithms — RSA, DSA, and Diffie-Hellman operate on numbers hundreds of digits long
  • Financial calculations — when precision with very large sums is required (though BigDecimal is better for fractions)
  • Competitive programming — problems that explicitly work with arbitrarily large integers
  • Hash and ID generation — when encoding large identifiers that span more than 64 bits

Performance Consideration

BigInteger operations are significantly slower than primitive arithmetic — each operation involves heap allocation and multi-word arithmetic. For numbers that fit in long, always prefer long. Use BigInteger only when values genuinely exceed the long range or when exact arbitrarily-large arithmetic is required.


Summary

Use BigInteger when values can overflow long — factorials, cryptographic keys, and combinatorial counts are classic examples. Create values with BigInteger.valueOf() for numbers within long range, or the String constructor for larger values. Remember that BigInteger is immutable: every arithmetic method returns a new object. Compare with equals() or compareTo(), never with ==.

macOS automatically creates .DS_Store files in every folder you open in Finder. They're harmless on your own machine but are notorious for cluttering Git repositories. This guide covers how to delete them, keep them out of Git, and prevent future pollution.


What Is a .DS_Store File?

DS_Store stands for Desktop Services Store. macOS creates this hidden file in each directory to store metadata about the folder's Finder view: icon positions, window size, background image, sort order, and other display preferences.

The file is invisible in Finder by default (it starts with a dot), but it shows up in Git diffs and git status output, where it creates noise and accidental commits.


Delete All .DS_Store Files in the Current Folder

Run this command from your project's root directory to recursively find and delete every .DS_Store file:

find . -name ".DS_Store" -delete

The find command searches from the current directory (.) through all subdirectories. -name ".DS_Store" matches the filename exactly, and -delete removes each match.

To see what would be deleted before actually deleting (a dry run):

find . -name ".DS_Store"

This lists all matching files without removing them. Once you're confident, add -delete to do the cleanup.


Delete .DS_Store Files from a Specific Directory

# Delete from a specific path
find /Users/yourname/projects/myapp -name ".DS_Store" -delete

# Delete from your entire home directory
find ~ -name ".DS_Store" -delete

Remove .DS_Store Files Already Committed to Git

If .DS_Store files were committed before you added them to .gitignore, deleting them from disk isn't enough — they remain in Git's history and will keep reappearing. You need to untrack them:

# Remove from Git's index (stops tracking), but keep the file on disk
git rm --cached .DS_Store

# Remove all .DS_Store files from tracking recursively
git rm --cached -r --ignore-unmatch "**/.DS_Store"

# Then commit the removal
git commit -m "Remove .DS_Store files from tracking"
Note: --cached removes the file from Git's index without deleting it from your filesystem. Without --cached, Git would delete the file from disk too.

Prevent .DS_Store from Being Committed — .gitignore

Add .DS_Store to your project's .gitignore to prevent it from ever being committed:

# .gitignore
.DS_Store
**/.DS_Store

The **/.DS_Store pattern matches .DS_Store in any subdirectory, not just the root. Commit the .gitignore change so your whole team benefits.


Global .gitignore — Apply to All Your Repositories

If you work on many projects, set a global .gitignore so you never have to think about this again:

# Create (or edit) the global gitignore file
echo ".DS_Store" >> ~/.gitignore_global

# Tell Git to use it for all repositories
git config --global core.excludesfile ~/.gitignore_global

This applies to every repository on your machine, without touching each project's own .gitignore.


Other macOS Clutter Files Worth Ignoring

While you're at it, these other macOS artifacts are also worth adding to your .gitignore:

# macOS metadata and system files
.DS_Store
**/.DS_Store
.AppleDouble
.LSOverride
._*

# macOS Spotlight index files
.Spotlight-V100
.Trashes

# macOS icon files
Icon?

Why .DS_Store Files Matter in Git

Leaving .DS_Store files unignored causes several practical problems:

  • Polluted git status — they appear as untracked files constantly
  • Accidental commits — developers commit them without realizing
  • Merge conflicts — two developers editing Finder view preferences in the same folder creates a merge conflict on a file with no meaningful content
  • Repository bloat — they accumulate over time if not cleaned up
  • Security consideration.DS_Store files can leak folder structure information about your machine

Summary

Run find . -name ".DS_Store" -delete to clean up existing files. Add .DS_Store to .gitignore (or a global ~/.gitignore_global) to prevent future commits. If the files were already committed, use git rm --cached to untrack them without deleting them from disk.

Java provides several ways to generate a random integer within a specified range. The right choice depends on your Java version, whether you're in a multithreaded environment, and whether cryptographic quality is required.


Method 1: Math.random()

Math.random() returns a double in the range [0.0, 1.0). To convert it to an integer in a custom range:

public static int randomWithMath(int min, int max) {
    return (int) (min + Math.random() * (max - min + 1));
}

System.out.println(randomWithMath(35, 40)); // e.g. 37
System.out.println(randomWithMath(1, 100)); // e.g. 63

The formula min + random * (max - min + 1) scales the [0.0, 1.0) range to [min, max] inclusive. The + 1 ensures the maximum value is reachable (without it, the result is [min, max-1]).


Method 2: java.util.Random

The Random class provides more flexibility and reusability than Math.random():

import java.util.Random;

Random random = new Random();

// nextInt(bound) returns [0, bound) — add min to shift the range
int result = random.nextInt(max - min + 1) + min;
System.out.println(result); // e.g. 38

Using nextInt(bound) is cleaner than the nextDouble() approach because it works directly with integers — no casting needed.

// Full utility method
public static int randomWithRandom(int min, int max) {
    Random random = new Random();
    return random.nextInt(max - min + 1) + min;
}

System.out.println(randomWithRandom(35, 40)); // e.g. 36
Avoid creating a new Random() inside a tight loop — the same instance is fine to reuse. Creating many instances quickly can produce correlated values on some JVMs.

Method 3: ThreadLocalRandom — Recommended for Most Cases

Java 7 introduced ThreadLocalRandom as the preferred approach for most applications. It's faster than Random in concurrent code because each thread maintains its own generator — no contention:

import java.util.concurrent.ThreadLocalRandom;

// nextInt(min, maxExclusive) — note: upper bound is exclusive
int result = ThreadLocalRandom.current().nextInt(35, 41);
System.out.println(result); // e.g. 39

// General range method
public static int randomThreadLocal(int min, int max) {
    return ThreadLocalRandom.current().nextInt(min, max + 1);
}

Note that ThreadLocalRandom.nextInt(min, max) has an exclusive upper bound — unlike Random.nextInt(bound) + min. Pass max + 1 to include the maximum value.


Method 4: SecureRandom — For Security-Sensitive Code

When randomness is used for security purposes (tokens, session IDs, OTPs), use SecureRandom instead. It uses a cryptographically strong algorithm:

import java.security.SecureRandom;

SecureRandom secureRandom = new SecureRandom();
int result = secureRandom.nextInt(max - min + 1) + min;
System.out.println(result);

SecureRandom is slower than the other options — only use it when you actually need cryptographic-quality randomness. For simulations, games, or general use, ThreadLocalRandom is the better choice.


Java 8+: IntStream for Multiple Random Integers

To generate multiple random integers at once, IntStream is convenient:

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

// Generate 10 random integers between 1 and 50
List<Integer> randoms = ThreadLocalRandom.current()
        .ints(10, 1, 51)   // 10 values, range [1, 51)
        .boxed()
        .collect(Collectors.toList());

System.out.println(randoms);
// e.g. [23, 7, 45, 12, 33, 5, 49, 18, 31, 2]

Comparison

MethodThread-Safe?SpeedUse When
Math.random()Yes (synchronized)Slow under contentionSimple one-off use
java.util.RandomYes (synchronized)Slow under contentionSingle-threaded code
ThreadLocalRandomYes (no sharing)FastMost applications, multithreaded code
SecureRandomYesSlowSecurity: tokens, OTPs, session IDs

Summary

For general-purpose random integers in a range, use ThreadLocalRandom.current().nextInt(min, max + 1) — it's the fastest option and safe under concurrent access. Use java.util.Random.nextInt(bound) + min for simple single-threaded code, and SecureRandom only when the randomness is used in a security context such as token generation.

The Java Platform Module System (JPMS), introduced in Java 9, organizes the JDK itself into a set of named modules. You can list all available modules with a single command and filter them to find what you need.


The Command

java --list-modules

Run this in any terminal. It lists every module bundled in your JDK installation, one per line, with its version number.

Sample output (JDK 21):

java.base@21
java.compiler@21
java.datatransfer@21
java.desktop@21
java.instrument@21
java.logging@21
java.management@21
java.net.http@21
java.prefs@21
java.rmi@21
java.scripting@21
java.se@21
java.security.jgss@21
java.security.sasl@21
java.sql@21
java.sql.rowset@21
java.xml@21
java.xml.crypto@21
jdk.accessibility@21
jdk.attach@21
jdk.compiler@21
jdk.httpserver@21
jdk.jartool@21
jdk.javadoc@21
jdk.jconsole@21
jdk.jdeps@21
jdk.jdi@21
jdk.jfr@21
jdk.jlink@21
jdk.jpackage@21
jdk.jshell@21
jdk.management@21
jdk.net@21
jdk.sctp@21
jdk.security.auth@21
jdk.security.jgss@21
jdk.zipfs@21
...

Module Naming Conventions

PrefixMeaningExamples
java.*Java SE specification modules — defined by the Java standardjava.base, java.sql, java.net.http
jdk.*JDK-specific modules — tools and implementation details, not part of the Java SE specjdk.compiler, jdk.jshell, jdk.jfr

java.* modules are portable — they're available in any conforming Java SE implementation (OpenJDK, GraalVM, Amazon Corretto, etc.). jdk.* modules may differ between vendors.


Key Platform Modules to Know

ModuleWhat It Provides
java.baseCore classes: java.lang, java.util, java.io, java.nio — always available, never needs to be declared
java.sqlJDBC API: java.sql, javax.sql
java.net.httpModern HTTP client (Java 11+): java.net.http
java.loggingJUL: java.util.logging
java.desktopAWT, Swing: java.awt, javax.swing
jdk.compilerThe javac compiler API
jdk.jshellJShell REPL API
jdk.jfrJava Flight Recorder for profiling

Filtering the Module List

The output can be long. Pipe through grep to find specific modules:

# Find all SQL-related modules
java --list-modules | grep sql

# Find all security modules
java --list-modules | grep security

# Count total modules
java --list-modules | wc -l

Describe a Single Module

To see what packages a specific module exports and what it requires:

java --describe-module java.sql

Output:

java.sql@21
exports java.sql
exports javax.sql
requires java.logging transitive
requires java.transaction.xa transitive
requires java.xml transitive
uses java.sql.Driver

This tells you which packages are publicly accessible from the module, and which other modules it depends on.


module-info.java — Declaring Your Own Module

In a modular application, each module declares its dependencies and exports in a module-info.java file at the source root:

module com.myapp.core {
    requires java.sql;          // depends on java.sql module
    requires java.logging;      // depends on java.logging module
    exports com.myapp.api;      // makes this package visible to other modules
}

Modules that aren't declared in requires are not accessible at runtime, even if their JARs are on the classpath. This enforces explicit dependency declarations and prevents accidental use of internal APIs.


Classpath vs Module Path

Most applications still use the traditional classpath rather than the module system. You don't need to use module-info.java to run Java 9+ applications — all platform modules remain accessible via the unnamed module when you launch normally with java -cp. The module system becomes relevant when you're building a modular application or creating a custom runtime image with jlink.


Summary

Run java --list-modules to see all modules in your JDK. Modules prefixed java.* are part of the Java SE specification; jdk.* modules are JDK-specific tools. Use java --describe-module <name> to inspect a module's exports and requirements. For modular applications, declare your module's dependencies in module-info.java.

package-info.java is a special source file that lives in a Java package directory. It has two primary purposes: adding Javadoc documentation at the package level, and applying annotations to an entire package at once. It's optional, but it becomes increasingly valuable as codebases grow.


File Location and Structure

The file must be placed directly inside the package directory it describes — one file per package. It contains a package declaration, optionally preceded by a Javadoc comment and/or annotations:

/**
 * Provides utility classes for parsing and validating user input.
 *
 * <p>All classes in this package are thread-safe unless stated otherwise.
 * The primary entry point is {@link com.myapp.util.InputParser}.
 */
package com.myapp.util;

That's the entire file. No class declaration, no imports (unless needed for annotations). The package statement must be the only top-level declaration.


Use Case 1: Package-Level Javadoc

When you run javadoc, the comment in package-info.java becomes the description for that package in the generated HTML documentation. Without this file, the package page has no description.

A well-written package comment typically includes:

  • What the package is for (one sentence)
  • The main entry-point class(es) a new user should look at first
  • Any thread-safety guarantees or invariants that apply across the package
  • Links to related packages with {@link}
/**
 * Contains the HTTP client abstraction layer.
 *
 * <p>Use {@link com.myapp.http.HttpClient} as the primary entry point.
 * All implementations in this package are non-blocking by default.
 *
 * @see com.myapp.auth Authentication utilities used by this package
 */
package com.myapp.http;

Use Case 2: Package-Level Annotations

Annotations placed in package-info.java apply to the entire package. A common use is marking an entire package as deprecated when you're retiring a legacy API:

@Deprecated(since = "2.0", forRemoval = true)
package com.myapp.legacy;

This causes IDEs and javac to show a deprecation warning for any code that imports from com.myapp.legacy.

Another common use is suppressing warnings across an entire package:

@SuppressWarnings("deprecation")
package com.myapp.migration;

Null Safety Annotations (JetBrains, Checker Framework)

Many teams use package-info.java to set a default null-safety policy for all classes in the package. For example, with JetBrains annotations:

@org.jetbrains.annotations.NonNullApi
package com.myapp.service;

Or with the Checker Framework's @DefaultQualifier. This marks every method parameter and return type in the package as non-null by default, so you only need to annotate the exceptions with @Nullable. It's much less verbose than annotating every individual element.


Rules and Constraints

  • Exactly one package-info.java per package — you can't have two
  • The file must be in the same directory as the package's other .java files
  • No class, interface, or enum declarations allowed in this file
  • Import statements are allowed (required if annotations need them)
  • The file compiles to package-info.class — it's a real compilation artifact

package-info.java vs package.html

Before Java 5, the older way to document packages was a file called package.html — plain HTML that Javadoc would read. package-info.java replaced it:

package-info.javapackage.html (legacy)
IntroducedJava 5Java 1.1
Supports annotationsYesNo
Compiled by javacYesNo
Used todayPreferredDeprecated practice

If your codebase has package.html files, they still work but you should migrate them to package-info.java to gain annotation support.


Generating Javadoc

Run the standard javadoc command — package-info.java is automatically picked up:

javadoc -d docs/ -sourcepath src/ com.myapp.util

Or with Maven:

mvn javadoc:javadoc

The package description appears at the top of the package's summary page in the generated documentation.


Summary

package-info.java serves two purposes: providing Javadoc for an entire package, and applying annotations to all classes in the package at once. It's optional for small projects but becomes valuable in larger codebases — especially for marking deprecated packages, setting default null-safety policies, and giving new developers a clear orientation to each package's purpose. One file per package, placed in the package's source directory.

The @Deprecated annotation marks a method, class, constructor, field, or package as outdated — signaling to other developers that it should no longer be used and may be removed in a future version. It generates compiler warnings and IDE indicators whenever deprecated code is called.


Basic Usage

Apply @Deprecated directly above the element you want to mark:

public class Calculator {

    @Deprecated
    public int add(int a, int b) {
        return a + b;
    }

    public int addValues(int a, int b) {
        return a + b;
    }
}

When another class calls calc.add(1, 2), the compiler emits a warning and most IDEs render the method name with strikethrough styling.


The since and forRemoval Attributes (Java 9+)

Java 9 added two optional attributes to @Deprecated that make the annotation more informative:

public class DataProcessor {

    @Deprecated(since = "2.0", forRemoval = true)
    public void processLegacy(String data) {
        // old implementation
    }

    public void process(String data) {
        // new implementation
    }
}
AttributeTypeMeaning
sinceStringThe version in which the element was deprecated
forRemovalbooleantrue means the element will definitely be removed in a future release; false (default) means it's deprecated but not necessarily scheduled for removal

When forRemoval = true, IDEs and tools can distinguish between "use with caution" and "stop using this immediately." The compiler also emits a stronger warning variant (removal category vs. deprecation category).


The @deprecated Javadoc Tag

Alongside the annotation, always add a @deprecated Javadoc tag that explains why the element is deprecated and what to use instead:

/**
 * Computes the sum of two integers.
 *
 * @deprecated As of version 2.0, use {@link #addValues(int, int)} instead.
 *             This method does not handle integer overflow correctly.
 */
@Deprecated(since = "2.0", forRemoval = true)
public int add(int a, int b) {
    return a + b;
}

The @deprecated tag (lowercase) is what appears in Javadoc HTML output. The @Deprecated annotation (uppercase) is what the compiler and tools read. Both serve different audiences — use both together.


Deprecating a Class

/**
 * @deprecated Use {@link NewAuthService} instead. This class will be
 *             removed in version 4.0.
 */
@Deprecated(since = "3.0", forRemoval = true)
public class LegacyAuthService {
    // ...
}

Deprecating a class does not automatically deprecate its methods — each element is deprecated independently. However, marking the class gives a clear signal that the entire API is being retired.


Deprecating a Constructor

public class Connection {

    @Deprecated(since = "1.5")
    public Connection(String url, String user, String password) {
        // password passed as plain string — insecure
    }

    public Connection(String url, String user, char[] password) {
        // char[] is cleared after use — preferred
    }
}

When to Use @Deprecated

  • You've written a better version of a method and want callers to migrate to it
  • A class or API is being retired in an upcoming major version
  • A security or correctness issue was found in the current implementation
  • External library APIs that you're wrapping have been replaced

Don't use @Deprecated as a way to "soft delete" code you're too lazy to remove. If something should go, remove it. Reserve deprecation for situations where you need to maintain backward compatibility across a transition period.


Suppressing Deprecation Warnings

If you're intentionally calling deprecated code (e.g., during a migration), suppress the warning to keep build output clean:

@SuppressWarnings("deprecation")
public void migrationCode() {
    legacyService.processLegacy(data); // intentional — being migrated
}

Only suppress when you have a deliberate reason. Blanket suppression at the class level hides all deprecation warnings, including ones you genuinely need to fix.


Summary

Use @Deprecated when you have a better alternative and want to give callers time to migrate. Always add since and forRemoval attributes (Java 9+) so callers understand the urgency, and pair the annotation with a @deprecated Javadoc tag that points to the replacement. The annotation generates compiler warnings and IDE indicators — use @SuppressWarnings("deprecation") only when you're intentionally calling deprecated code during a controlled migration.

Maven is the most widely used build tool for Java projects. It handles dependency management, compilation, testing, and packaging through a standardized project structure and a single configuration file: pom.xml. This guide walks through creating a project from scratch using Maven's quickstart archetype.


Prerequisites

  • Java JDK installed (Java 8 or later)
  • Maven installed — verify with mvn --version
mvn --version
# Apache Maven 3.9.4
# Java version: 21.0.1, vendor: Eclipse Adoptium

If Maven isn't installed, download it from maven.apache.org or install it via brew install maven (macOS) or sudo apt install maven (Ubuntu).


Step 1: Generate the Project

Run the following command to generate a new project using the maven-archetype-quickstart template:

mvn archetype:generate \
  -DgroupId=com.mycompany.app \
  -DartifactId=my-java-app \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DarchetypeVersion=1.4 \
  -DinteractiveMode=false

The parameters:

ParameterDescription
groupIdReverse-domain identifier for your organization (com.mycompany.app)
artifactIdThe project/module name — becomes the folder name
archetypeArtifactIdThe template to use — quickstart is the standard Java starter
interactiveMode=falseSkips the interactive prompt; uses provided values directly

Maven downloads the archetype metadata and creates a folder named my-java-app in the current directory.


Step 2: Project Structure

The generated structure follows Maven's standard layout:

my-java-app/
├── pom.xml
└── src/
    ├── main/
    │   └── java/
    │       └── com/mycompany/app/
    │           └── App.java
    └── test/
        └── java/
            └── com/mycompany/app/
                └── AppTest.java

All production source code goes in src/main/java. All test code goes in src/test/java. This separation is enforced by Maven's build lifecycle.


Step 3: The Generated pom.xml

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-java-app</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Update maven.compiler.source and maven.compiler.target to match your Java version. To add dependencies, insert <dependency> blocks inside <dependencies>.


Step 4: Build the Project

cd my-java-app
mvn install

This runs the full build lifecycle: compile → test → package → install. The JAR is placed in target/my-java-app-1.0-SNAPSHOT.jar and also installed to your local Maven repository (~/.m2/repository).

Common lifecycle commands:

mvn compile        # compile source code only
mvn test           # compile and run tests
mvn package        # compile, test, and create JAR/WAR
mvn install        # package + install to local .m2 repo
mvn clean          # delete the target/ directory
mvn clean install  # clean then full build — most common

Importing Into an IDE

IntelliJ IDEA: File → Open → select the my-java-app folder. IntelliJ detects pom.xml and imports it as a Maven project automatically.

Eclipse: Generate Eclipse project files first, then import:

mvn eclipse:eclipse

In Eclipse: File → Import → Existing Projects into Workspace → navigate to the project folder.

VS Code: Open the folder. Install the "Extension Pack for Java" extension — it detects pom.xml and configures the project automatically.


Adding a Dependency

To add a library, add its <dependency> to pom.xml and run mvn install. Maven downloads it automatically from Maven Central:

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.10.1</version>
</dependency>

Find dependency coordinates at mvnrepository.com — search for the library and copy the <dependency> snippet.


Summary

Use mvn archetype:generate with the maven-archetype-quickstart template to scaffold a new Java project in seconds. The result is a standard Maven project structure with pom.xml, source and test directories, and a working build. Run mvn clean install to compile, test, and package your application. Open the pom.xml folder directly in IntelliJ or VS Code for automatic Maven integration.

Java has two sets of logical operators for boolean expressions: the short-circuit operators (&& and ||) and the non-short-circuit operators (& and |). They produce the same boolean result, but differ in when they evaluate the right operand. Understanding this difference matters for both correctness and performance.


How Short-Circuit Evaluation Works

OperatorNameShort-circuits when…
&&Conditional ANDLeft side is false — result is false regardless of right side
||Conditional ORLeft side is true — result is true regardless of right side
&Logical ANDNever — always evaluates both sides
|Logical ORNever — always evaluates both sides

Demonstrating Short-Circuit with &&

public class ShortCircuitDemo {

    static boolean isFalse() {
        System.out.println("isFalse() called");
        return false;
    }

    static boolean isTrue() {
        System.out.println("isTrue() called");
        return true;
    }

    public static void main(String[] args) {
        System.out.println("--- Using && ---");
        boolean result = isFalse() && isTrue();
        System.out.println("Result: " + result);

        System.out.println("--- Using & ---");
        result = isFalse() & isTrue();
        System.out.println("Result: " + result);
    }
}

Output:

--- Using && ---
isFalse() called
Result: false

--- Using & ---
isFalse() called
isTrue() called
Result: false

With &&, once isFalse() returns false, Java skips isTrue() entirely — the overall result is already determined. With &, both methods are always called.


Demonstrating Short-Circuit with ||

System.out.println("--- Using || ---");
boolean result = isTrue() || isFalse();
System.out.println("Result: " + result);

System.out.println("--- Using | ---");
result = isTrue() | isFalse();
System.out.println("Result: " + result);

Output:

--- Using || ---
isTrue() called
Result: true

--- Using | ---
isTrue() called
isFalse() called
Result: true

With ||, once isTrue() returns true, the right side is skipped. With |, both sides always run.


Practical Benefit: Null Safety

The most common real-world use of short-circuit evaluation is guarding against NullPointerException:

String text = null;

// Safe — if text is null, the second condition is never evaluated
if (text != null && text.length() > 5) {
    System.out.println("Long string: " + text);
}

// Unsafe with & — both sides always run; throws NullPointerException
if (text != null & text.length() > 5) { // NPE!
    System.out.println("Long string: " + text);
}

This pattern — checking for null before calling methods on an object — is ubiquitous in Java. It only works correctly because && short-circuits.


Practical Benefit: Avoiding Expensive Operations

Place the cheaper or most-likely-to-fail condition on the left side so the expensive one is skipped when possible:

// isEnabled() is a cheap boolean field check
// fetchFromDatabase() is an expensive I/O call
if (isEnabled() && fetchFromDatabase(id) != null) {
    process();
}

// If isEnabled() is false, fetchFromDatabase() is never called

When to Use Non-Short-Circuit Operators (& and |)

The non-short-circuit & and | are rarely used with booleans, but they have a valid use case: when you need both sides to execute for their side effects.

// Both increment operations must run — use & instead of &&
int x = 0, y = 0;
boolean result = (++x > 0) & (++y > 0);
System.out.println("x=" + x + " y=" + y); // x=1 y=1

// With &&, y might not be incremented:
x = 0; y = 0;
result = (++x > 5) && (++y > 0); // x=1 but y=0 — y not incremented

In practice, code with side effects inside conditions is hard to read. Prefer computing side effects before the condition rather than relying on &.


Summary

Use && and || (short-circuit) in almost all boolean conditions — they're safer (null guards work), more efficient (skip unnecessary work), and what Java developers expect. Use & and | only when you explicitly need both sides to always evaluate. Put cheaper or fail-fast conditions on the left side of && to maximize the performance benefit of skipping the right side.

Java's Map interface doesn't extend Iterable directly, so you can't use a for-each loop on the map itself. Instead, you iterate over one of its three collection views: entrySet(), keySet(), or values(). Here are five practical methods.


Setup — Sample Map

import java.util.*;

Map<Integer, String> countries = new HashMap<>();
countries.put(1, "USA");
countries.put(2, "Germany");
countries.put(3, "Japan");
countries.put(4, "Brazil");
countries.put(5, "India");

Method 1: entrySet() with For-Each Loop

The most common and readable approach. Each Map.Entry gives you direct access to both key and value without a second lookup:

for (Map.Entry<Integer, String> entry : countries.entrySet()) {
    System.out.println(entry.getKey() + " → " + entry.getValue());
}

Output:

1 → USA
2 → Germany
3 → Japan
4 → Brazil
5 → India

This is the preferred approach when you need both key and value. It avoids the extra map.get(key) call that keySet() iteration requires.


Method 2: entrySet() with Iterator

Use an explicit Iterator when you need to remove entries during iteration — the only safe way to modify a map while iterating:

Iterator<Map.Entry<Integer, String>> iterator = countries.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<Integer, String> entry = iterator.next();
    System.out.println(entry.getKey() + " → " + entry.getValue());

    // Safe to remove — iterator.remove() doesn't throw ConcurrentModificationException
    if (entry.getKey() == 3) {
        iterator.remove();
    }
}

Never call map.remove() inside a for-each loop — it throws ConcurrentModificationException. An explicit iterator's remove() is the safe alternative.


Method 3: keySet() with For-Each Loop

Iterate over keys, then look up each value separately:

for (Integer key : countries.keySet()) {
    System.out.println(key + " → " + countries.get(key));
}

This works but performs an extra get() per entry. Use this when you only need the keys, or when the value lookup is intentional. For both key and value, entrySet() is more efficient.


Method 4: keySet() with Iterator

Iterator<Integer> keyIterator = countries.keySet().iterator();
while (keyIterator.hasNext()) {
    Integer key = keyIterator.next();
    System.out.println(key + " → " + countries.get(key));
}

Method 5: Java 8 forEach() — Most Concise

Java 8 added a forEach() method directly on Map that accepts a BiConsumer:

countries.forEach((key, value) ->
    System.out.println(key + " → " + value));

This is the cleanest syntax for read-only iteration. It's internally backed by entrySet(), so performance is identical. You can't call iterator.remove() inside forEach() — use method 2 if you need to modify the map during iteration.


Iterating Values Only

When you only need the values (not the keys), iterate values():

for (String country : countries.values()) {
    System.out.println(country);
}

Comparison

MethodAccess Key?Access Value?Remove During?Java Version
entrySet() for-eachYesYes (direct)No5+
entrySet() iteratorYesYes (direct)Yes5+
keySet() for-eachYesVia get()No5+
keySet() iteratorYesVia get()Yes5+
forEach() lambdaYesYes (direct)No8+
values() for-eachNoYesNo5+

Which Method Should You Use?

  • Default choice: forEach((key, value) -> ...) — cleanest syntax, Java 8+
  • Need to remove during iteration: entrySet().iterator() with iterator.remove()
  • Keys only: for (K key : map.keySet())
  • Values only: for (V value : map.values())
  • Pre-Java 8: entrySet() for-each

Summary

Iterate over a Map using one of its three views: entrySet() for both keys and values, keySet() for keys alone, or values() for values alone. For most modern code, map.forEach((k, v) -> ...) is the cleanest option. When you need to remove entries during iteration, use an explicit iterator — never map.remove() inside a for-each loop.

LinkedHashMap maintains a doubly-linked list across its entries, preserving insertion order (or access order, depending on configuration). Its protected removeEldestEntry() method lets you control automatic eviction — making it the simplest way to build a bounded cache in Java without any third-party library.


What Is removeEldestEntry()?

removeEldestEntry(Map.Entry<K,V> eldest) is a protected method called automatically after every put() or putAll(). If it returns true, LinkedHashMap removes the eldest (first) entry from the map. If it returns false, nothing is removed.

The default implementation always returns false — no automatic removal. You override it to add eviction logic.


Basic Example: Size-Bounded Map

Keep at most 4 entries — automatically evict the oldest when a 5th is added:

import java.util.*;

LinkedHashMap<Integer, String> boundedMap = new LinkedHashMap<>() {
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
        return size() > 4;
    }
};

boundedMap.put(0, "zero");
boundedMap.put(1, "one");
boundedMap.put(2, "two");
boundedMap.put(3, "three");

System.out.println("After 4 puts: " + boundedMap);
// {0=zero, 1=one, 2=two, 3=three}

boundedMap.put(4, "four"); // triggers eviction of key=0

System.out.println("After 5th put: " + boundedMap);
// {1=one, 2=two, 3=three, 4=four}

When the 5th entry is added, size() becomes 5, removeEldestEntry() returns true, and the entry with key 0 (the eldest) is automatically removed.


LRU Cache Using Access Order

For a true Least Recently Used (LRU) cache, construct LinkedHashMap with accessOrder = true. This moves accessed entries to the tail of the list, so the head always holds the least-recently-used entry:

int capacity = 3;

LinkedHashMap<String, String> lruCache = new LinkedHashMap<>(capacity, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > capacity;
    }
};

lruCache.put("a", "Apple");
lruCache.put("b", "Banana");
lruCache.put("c", "Cherry");
System.out.println(lruCache); // {a=Apple, b=Banana, c=Cherry}

lruCache.get("a"); // "a" accessed — moves to tail (most recent)
System.out.println(lruCache); // {b=Banana, c=Cherry, a=Apple}

lruCache.put("d", "Date"); // evicts "b" — least recently used
System.out.println(lruCache); // {c=Cherry, a=Apple, d=Date}

The LinkedHashMap(initialCapacity, loadFactor, accessOrder) constructor's third argument enables access-order mode. With accessOrder = true, every get() reorders the entry to the tail, so the head is always the LRU candidate for eviction.


Insertion Order vs Access Order

ModeHow It WorksWhat Gets Evicted
Insertion order (default)Entries stay in the order they were put()The entry that was inserted first
Access order (true)Accessed entries move to the tailThe entry least recently accessed or inserted

Reusable LRU Cache Class

import java.util.LinkedHashMap;
import java.util.Map;

public class LruCache<K, V> extends LinkedHashMap<K, V> {

    private final int maxSize;

    public LruCache(int maxSize) {
        super(maxSize, 0.75f, true); // accessOrder = true
        this.maxSize = maxSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxSize;
    }
}

// Usage:
LruCache<Integer, String> cache = new LruCache<>(5);
cache.put(1, "Page 1");
cache.put(2, "Page 2");
cache.get(1); // "1" is now most recently used
System.out.println(cache.containsKey(1)); // true

Thread Safety

LinkedHashMap is not thread-safe. For a concurrent LRU cache, wrap it with Collections.synchronizedMap():

Map<String, String> syncCache = Collections.synchronizedMap(
    new LruCache<>(100)
);

For high-concurrency workloads, consider ConcurrentHashMap with a manual size-bounding strategy, or a library like Caffeine which provides a lock-free LRU cache.


Summary

Override removeEldestEntry() in a LinkedHashMap subclass to automatically evict entries when the map exceeds a size limit. For a true LRU cache, construct LinkedHashMap with accessOrder = true so accesses move entries to the tail and the head always holds the least-recently-used entry. This pattern is clean, built into the JDK, and requires no external dependencies.

Java 17 finalised sealed classes (JEP 409), a language feature that gives you fine‑grained control over inheritance. With sealed classes, you can declare a class or interface and explicitly list which types are allowed to extend or implement it. This fills the gap between final (no subclasses) and completely open inheritance (any subclass allowed).


The Inheritance Dilemma

Before sealed classes, Java developers faced a choice:

  • final classes – too restrictive; you cannot extend them at all.
  • Open classes – anyone can extend them, which makes it hard to reason about a fixed set of subtypes.

Sealed classes introduce a third option: “only these specific classes may extend or implement this type.” This is invaluable when modelling a closed domain, such as:

  • Shapes in a geometry library (Circle, Rectangle, Triangle)
  • Payment statuses (Success, Failure, Pending)
  • AST nodes in a compiler (IfNode, WhileNode, AssignmentNode)
  • JSON value types (JsonObject, JsonArray, JsonString, etc.)

Key Keywords and Their Roles

Understanding the interplay of sealed, permits, final, and non-sealed is essential:

KeywordUsed OnMeaning
sealedClass or interfaceRestricts which types may extend/implement it; must be followed by permits (unless subclasses are nested).
permitsAfter sealedLists the permitted direct subclasses or implementors.
finalSubclassCannot be extended further – the hierarchy ends here.
sealedSubclassIs itself sealed and can have its own permitted subclasses (nested sealed hierarchy).
non-sealedSubclassRe‑opens the subclass for arbitrary extension – the sealed constraint stops here.

Basic Example: Sealed Class with permits

Let's define a sealed Shape class that allows only three specific subclasses:

public abstract sealed class Shape permits Circle, Rectangle, Triangle {
    public abstract double area();
}

public final class Circle extends Shape {
    private final double radius;
    public Circle(double radius) { this.radius = radius; }

    @Override
    public double area() { return Math.PI * radius * radius; }
}

public final class Rectangle extends Shape {
    private final double width, height;
    public Rectangle(double width, double height) { this.width = width; this.height = height; }

    @Override
    public double area() { return width * height; }
}

public final class Triangle extends Shape {
    private final double base, height;
    public Triangle(double base, double height) { this.base = base; this.height = height; }

    @Override
    public double area() { return 0.5 * base * height; }
}

Any attempt to create a fourth subclass (e.g., Hexagon) will be rejected by the compiler – the hierarchy is closed.


Omitting permits with Nested Subclasses

If all permitted subclasses are defined as nested classes inside the sealed class, you can omit the permits clause:

public abstract sealed class Operation {
    public abstract int apply(int a, int b);

    public static final class Add extends Operation {
        @Override public int apply(int a, int b) { return a + b; }
    }

    public static final class Sub extends Operation {
        @Override public int apply(int a, int b) { return a - b; }
    }

    public static final class Mul extends Operation {
        @Override public int apply(int a, int b) { return a * b; }
    }
}

This keeps the code self‑contained and clearly communicates that these are the only operations.


Sealed Interfaces and Records

Sealed interfaces work equally well and pair beautifully with records (which are implicitly final):

public sealed interface PaymentResult permits Success, Failure, Pending { }

public record Success(String transactionId) implements PaymentResult { }
public record Failure(String reason)        implements PaymentResult { }
public record Pending(String reference)     implements PaymentResult { }

Records are a natural choice here because each variant holds different data, and they are inherently immutable.


Pattern Matching with switch (Java 21+)

The real power of sealed classes shines when combined with pattern matching in switch expressions. Because the compiler knows all permitted subtypes, it can verify exhaustiveness – no default case is needed:

double describeArea(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t  -> 0.5 * t.base() * t.height();
        // No default – compiler ensures all cases are covered
    };
}

If you later add a new permitted subclass (say Hexagon) to Shape, every switch over Shape that lacks a Hexagon case will become a compile‑time error – catching the gap immediately.


Reopening a Branch with non-sealed

Sometimes you want to allow arbitrary extension for some part of the hierarchy. Mark a permitted subclass as non-sealed to reopen it:

public abstract sealed class Vehicle permits Car, Truck, SpecialVehicle { }

public final class Car extends Vehicle { }
public final class Truck extends Vehicle { }
public non-sealed class SpecialVehicle extends Vehicle { }

// Now anyone can extend SpecialVehicle:
public class Ambulance extends SpecialVehicle { }
public class FireTruck  extends SpecialVehicle { }

This gives you a hybrid model: a closed top‑level family, with an open branch for extensibility where needed.


Important Rules and Constraints

  • The sealed class and all its permitted subclasses must be in the same package or in the same module.
  • Every permitted subclass must directly extend the sealed class (not a subclass of it).
  • Each permitted subclass must be marked final, sealed, or non-sealed.
  • If the sealed class is an interface, the implementors must be final, sealed, or non-sealed classes (records are implicitly final).
  • You cannot use a sealed class with enum – enums are already effectively a closed set of constants.

When to Use Sealed Classes vs Enums

FeatureSealed ClassesEnums
Number of instances Many (unlimited) – each subclass can have many objects Fixed set of singleton constants
State Each subclass can have its own fields and behaviour Enums can have fields, but they are effectively singletons
Pattern matching Excellent – can deconstruct and handle different shapes Limited to constant matching (switch on enum constants)
Use case Modelling complex, state‑ful variants (e.g., AST nodes, JSON types) Modelling fixed sets of named values (e.g., days of the week, status codes)

Real‑World Use Cases

1. State Machines

public sealed interface State permits Idle, Running, Paused, Stopped { }

public record Idle() implements State { }
public record Running(long startTime) implements State { }
public record Paused(long elapsed) implements State { }
public record Stopped() implements State { }

2. Abstract Syntax Tree Nodes

public sealed interface Expr permits Const, Add, Mul, Var { }

public record Const(int value) implements Expr { }
public record Add(Expr left, Expr right) implements Expr { }
public record Mul(Expr left, Expr right) implements Expr { }
public record Var(String name) implements Expr { }

3. JSON Data Types

public sealed interface JsonValue permits JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull { }

public record JsonObject(Map<String, JsonValue> members) implements JsonValue { }
public record JsonArray(List<JsonValue> elements) implements JsonValue { }
public record JsonString(String value) implements JsonValue { }
// ... and so on

4. Payment Processing Results

We already saw the PaymentResult example – it's a perfect fit for sealed interfaces with records.


Best Practices

  • Prefer sealed interfaces with records for data‑oriented hierarchies – they are concise and immutable.
  • Use sealed classes when you need shared state or behaviour that cannot be captured by records (e.g., mutable fields, helper methods).
  • Design for extensibility – if you anticipate that third‑party developers will need to extend a branch, mark that branch non-sealed.
  • Combine with pattern matching to write safe, readable, and maintainable code that the compiler checks for completeness.
  • Keep the permitted list small – a sealed hierarchy with more than 5–7 subtypes may indicate a design that could be simplified.

Summary

Sealed classes give you a powerful tool to declare closed type hierarchies while retaining the flexibility of object‑oriented design. They turn runtime errors into compile‑time checks, especially when used with pattern matching. Mark your root type with sealed, list your permitted subtypes, and choose final, sealed, or non-sealed for each one – depending on how open that branch should be.

Whether you are modelling a domain, building an interpreter, or handling responses from an external API, sealed classes help you express your intent clearly and let the compiler enforce your design decisions. Start using them today – they are available in Java 17 and later.


Happy coding!

Java records, introduced as a standard feature in Java 16 (previewed in Java 14), provide a concise way to declare immutable data‑carrying classes. A single‑line record definition replaces the constructor, accessors, equals(), hashCode(), and toString() that you would otherwise write by hand.


The Boilerplate Problem

Consider a traditional Java class for an Address – it requires a lot of repetitive code just to hold a few values:

// Traditional class – lots of boilerplate
public class Address {
    private final String street;
    private final String city;
    private final int zip;

    public Address(String street, String city, int zip) {
        this.street = street;
        this.city = city;
        this.zip = zip;
    }

    public String getStreet() { return street; }
    public String getCity()   { return city; }
    public int    getZip()    { return zip; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address address = (Address) o;
        return zip == address.zip &&
               Objects.equals(street, address.street) &&
               Objects.equals(city, address.city);
    }

    @Override
    public int hashCode() {
        return Objects.hash(street, city, zip);
    }

    @Override
    public String toString() {
        return "Address{" +
               "street='" + street + '\'' +
               ", city='" + city + '\'' +
               ", zip=" + zip +
               '}';
    }
}

The record equivalent is a single line – the compiler does all the work:

public record Address(String street, String city, int zip) {}

What Records Auto‑Generate

From this concise declaration, the compiler automatically generates:

Generated MemberDescription
Canonical constructor Address(String street, String city, int zip) – assigns all components.
Accessor methods street(), city(), zip() – note the no get prefix.
equals() Compares all components for structural equality.
hashCode() Based on all components, consistent with equals().
toString() Returns a string like Address[street=123 Main St, city=Springfield, zip=12345].

Basic Usage Example

public record Address(String street, String city, int zip) {}

Address addr = new Address("1044 Main Street", "Springfield", 12345);

System.out.println(addr.street());   // 1044 Main Street
System.out.println(addr.city());     // Springfield
System.out.println(addr.zip());      // 12345
System.out.println(addr);            // Address[street=1044 Main Street, city=Springfield, zip=12345]

Address addr2 = new Address("1044 Main Street", "Springfield", 12345);
System.out.println(addr.equals(addr2)); // true

Compact Constructor – Validation Made Easy

A compact constructor lets you add validation without repeating the parameter list. The compiler automatically inserts the field assignments after your validation code:

public record Address(String street, String city, int zip) {

    public Address {
        if (street == null || street.isBlank()) {
            throw new IllegalArgumentException("street cannot be blank");
        }
        if (city == null || city.isBlank()) {
            throw new IllegalArgumentException("city cannot be blank");
        }
        if (zip < 10000 || zip > 99999) {
            throw new IllegalArgumentException("invalid zip code: " + zip);
        }
        // No need to write this.street = street; etc.
        // The compiler adds those assignments after this block.
    }
}

This is much cleaner than writing a full canonical constructor – you only focus on invariants.


Custom Constructors and Overriding Generated Methods

You can also add additional constructors (which must delegate to the canonical constructor) and override generated methods:

public record Person(String name, int age) {

    // Additional constructor with default age
    public Person(String name) {
        this(name, 0); // delegates to canonical constructor
    }

    // Override toString for a custom format
    @Override
    public String toString() {
        return name + " (" + age + " years)";
    }
}

Records Can Have Instance Methods and Implement Interfaces

A record is still a class, so you can add custom instance methods and implement interfaces:

public interface Printable {
    void print();
}

public record Point(int x, int y) implements Printable {

    // Custom method
    public double distanceFromOrigin() {
        return Math.sqrt(x*x + y*y);
    }

    @Override
    public void print() {
        System.out.println("Point(" + x + ", " + y + ")");
    }
}

Point p = new Point(3, 4);
p.print();                     // Point(3, 4)
System.out.println(p.distanceFromOrigin()); // 5.0

Records – What You Can and Cannot Do

AllowedNot Allowed
Implement interfaces Extend a class (records implicitly extend java.lang.Record)
Define static fields and methods Define instance fields beyond the record components
Define instance methods Be extended by another class (records are implicitly final)
Override generated methods Use native methods
Add custom constructors Have mutable components (components are effectively final)

Records as DTOs and Value Objects

Records are ideal for Data Transfer Objects (DTOs) – objects whose only purpose is to carry data between layers. They work seamlessly with JSON libraries (Jackson, Gson, etc.) and are a perfect match for value objects:

// API response DTO
public record UserResponse(long id, String name, String email) {}

// Database query result
public record OrderSummary(long orderId, String status, double total) {}

// Event payload
public record UserCreatedEvent(long userId, String email, Instant createdAt) {}

// Value object in a domain model
public record Money(String currency, BigDecimal amount) {}

Most modern serialization frameworks support records natively (Jackson 2.12+, Gson 2.8.9+), making them a drop‑in replacement for traditional DTO classes.


Records vs Lombok @Data

Both reduce boilerplate, but they serve different purposes. Here’s a quick comparison:

Java RecordLombok @Data
Requires external dependency No – built into Java 16+ Yes – Lombok must be installed
Immutable by default Yes – all components are final No – generates setters (mutable)
Supports inheritance No (records are final) Yes – works with normal class hierarchies
Custom field logic Limited – only via compact constructor and methods Full control – you write the class body
Performance overhead Minimal – no reflection or annotation processing at runtime No runtime overhead – annotations are processed at compile time

Recommendation: Use records for simple, immutable data carriers on Java 16+. Use Lombok for mutable classes, inheritance, or when you need full control over the class structure.


Best Practices

  • Prefer records for DTOs, value objects, and events – they are concise and naturally encourage immutability.
  • Use compact constructors for validation – keep them short and focused on invariants.
  • Override toString() if the default format isn't suitable – but be careful to keep it informative.
  • Add static factory methods for common creation patterns (e.g., Person.of(name, age)).
  • Keep record components small – if a record has many fields (say >10), consider whether it’s a code smell and refactor.
  • Combine with pattern matching (Java 21+) – records work beautifully with deconstruction patterns.

Example: Record with Static Factory and Validation

public record Person(String firstName, String lastName, int age) {

    public Person {
        if (firstName == null || firstName.isBlank()) {
            throw new IllegalArgumentException("First name is required");
        }
        if (lastName == null || lastName.isBlank()) {
            throw new IllegalArgumentException("Last name is required");
        }
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age: " + age);
        }
    }

    // Static factory method
    public static Person of(String firstName, String lastName, int age) {
        return new Person(firstName, lastName, age);
    }

    // Custom method
    public String fullName() {
        return firstName + " " + lastName;
    }

    // Override toString to hide sensitive data if needed
    @Override
    public String toString() {
        return "Person[" + firstName + " " + lastName + ", age=" + age + "]";
    }
}

Summary

Java records revolutionise the way we write simple data classes. They eliminate the boilerplate of constructors, accessors, equals(), hashCode(), and toString() – all with a single line of code. Use records for DTOs, value objects, and event payloads where immutability is a feature. The compact constructor gives you a clean place to add validation, and you can still add custom methods and implement interfaces. Records are implicitly final – they cannot be extended, but that's intentional: they model data, not behaviour.

If you're on Java 16 or later, start using records today – they make your code clearer, safer, and more maintainable.


Happy coding!