Modern Java Collections: computeIfAbsent, Immutables, and Best Practices
Key Takeaways
- →computeIfAbsent on ConcurrentHashMap holds the bin lock for the whole mapping function, which must not modify the map — a detectably recursive update throws IllegalStateException, and other writers on that bin stall behind the held lock (a hard hang if the lambda waits on one of them)
- →List.of() is truly immutable with no backing mutable list; Collections.unmodifiableList() wraps the original, so mutations through the original reference still affect the unmodifiable view
- →Comparator.comparing() with thenComparingInt avoids boxing overhead and chains null-safe for multi-level sorts without anonymous class boilerplate
- →removeIf() on ArrayList is batch-optimized — marks removed elements in one pass, then compacts in a single operation (no per-removal O(n) shifts)
The classic computeIfAbsent-deadlock-on-ConcurrentHashMap incident. Production outage traces to a single line:
cache.computeIfAbsent(key, k -> loadFromDb(k)). TheloadFromDbmethod, under high concurrency, occasionally callscache.put(relatedKey, value)inside its body.ConcurrentHashMap[Java Collections Framework] runs the mapping function while holding that key's bin lock and forbids the function from modifying the map. The lucky outcome of the nestedputis anIllegalStateException: Recursive update; the unlucky one is a half-mutated bin plus every other writer on it stalled behind the held lock — a stall that hardens into a hang the moment the loader waits on a thread it just blocked. The fix is one line: compute the value outside the lambda, then useputIfAbsent. We've seen variants of this on multiple Java services.
Modern Java collection APIs — getOrDefault, computeIfAbsent, removeIf, List.of, Comparator.comparing[Java Collections Framework] — replace verbose null checks and mutable-list patterns with atomic, composable one-liners. Each has an edge case (deadlock, null rejection, mutation through original reference) that bites in production. Know the trap before you deploy.
- Replace null checks with
getOrDefault— eliminatesNullPointerExceptionat the call site - Use
computeIfAbsentfor cache init — atomic forConcurrentHashMap, but don't mutate inside the lambda - Swap
List.of()forCollections.unmodifiableList()— truly immutable, no backing mutable list
graph TD
T1[Thread A:<br/>computeIfAbsent K1] -->|holds bin lock| Seg[(ConcurrentHashMap<br/>bin)]
T1 -->|lambda runs:<br/>loadFromDb| Lambda[Mapping function]
Lambda -->|re-enters same map| Seg
Seg -.->|forbidden by the API| Block["🔥 recursive same-key update:<br/>IllegalStateException;<br/>cross-key: undefined behaviour"]
T2[Thread B:<br/>same-bin write] -.->|blocks on held lock| Stall["⏳ other writers stall until<br/>the lambda returns — a hang if<br/>the lambda waits on them"]
T3[Thread N:<br/>same-bin write] -.->|blocks on held lock| Stall
style Block fill:#fee
style Stall fill:#fee
style Lambda fill:#eef
The diagram shows the real shape: thread A holds the bin lock for the whole lambda, so the mapping function must not touch the map. A detectably recursive same-key update throws IllegalStateException: Recursive update; a cross-key write is undefined behaviour and can corrupt the structure the operation is mid-traversal on. Meanwhile every other writer on that bin (B, N) stalls until the lambda returns — and if the lambda itself waits on one of those blocked threads, the stall becomes a true deadlock. The fix is to keep the lambda pure — compute the value outside, then putIfAbsent.
The Quick Start: Collections Decision Table
Choose your pattern based on what you're doing:
| Use Case | API | Why | Example |
|---|---|---|---|
| Safe lookup with default | getOrDefault(key, default) | Eliminates null check boilerplate | map.getOrDefault("id", List.of()) |
| Lazy cache initialization | computeIfAbsent(key, fn) | Atomic for ConcurrentHashMap, no race | cache.computeIfAbsent(id, k -> loadProfile(k)) |
| Counting or accumulation | merge(key, value, fn) | Single call replaces getOrDefault+put | freq.merge(word, 1, Integer::sum) |
| Remove while iterating | removeIf(predicate) | No ConcurrentModificationException | sessions.removeIf(s -> s.isExpired()) |
| Immutable list constant | List.of(...) | No backing mutable list, rejects null | List.of("a", "b", "c") |
| Sort multiple fields | Comparator.comparing(...).thenComparing(...) | Composable, null-safe | list.sort(Comparator.comparing(User::dept).thenComparingInt(User::salary)) |
| Sequenced access (Java 21) | getFirst() / getLast() | Unified API across List, Deque, SortedSet | items.getFirst() |
computeIfAbsent: Atomic Cache Patterns
The multimap pattern — where each key holds a list — is a race condition waiting to happen:
// WRONG: check-then-put not atomic
Map<String, List<Order>> ordersByCustomer = new ConcurrentHashMap<>();
if (!ordersByCustomer.containsKey(key)) {
ordersByCustomer.put(key, new ArrayList<>()); // two threads both insert
}computeIfAbsent is atomic for ConcurrentHashMap:
// RIGHT: atomic initialization
Map<String, List<Order>> ordersByCustomer = new ConcurrentHashMap<>();
ordersByCustomer
.computeIfAbsent(customerId, k -> new ArrayList<>())
.add(order);The lambda only runs on cache miss, and for ConcurrentHashMap, the entire operation holds that key's bin lock. No race, no lost updates. This is the foundation of in-memory caches:
Map<String, UserProfile> profileCache = new ConcurrentHashMap<>();
public UserProfile getProfile(String userId) {
return profileCache.computeIfAbsent(userId, id ->
userRepository.findById(id).orElseThrow()
);
}The mapping function must not call put() or computeIfAbsent() on the same map. The bin lock is held for the whole lambda, so a detectably recursive update throws IllegalStateException: Recursive update and any other write to that bin blocks until the lambda returns (a hang if the lambda then waits on a blocked thread). Compute the value outside the lambda, then use putIfAbsent instead.
Comparator.comparing and removeIf: Composable Transformations
Replace verbose anonymous Comparator classes with chainable method references:
// OLD: error-prone, verbose
employees.sort(new Comparator<Employee>() {
public int compare(Employee a, Employee b) {
int d = a.department().compareTo(b.department());
return d != 0 ? d : Integer.compare(b.salary(), a.salary());
}
});
// NEW: declarative, null-safe
employees.sort(
Comparator.comparing(Employee::department)
.thenComparing(Comparator.comparingInt(Employee::salary).reversed())
);Use thenComparingInt / thenComparingLong for ascending primitive sorts (no boxing) — they take only a key extractor, so for descending order wrap the key with Comparator.comparingInt(...).reversed() as above. Use nullsLast() / nullsFirst() for nullable fields.
Similarly, removeIf replaces iterator-based removal without ConcurrentModificationException:
// OLD: Iterator boilerplate
Iterator<Session> it = sessions.iterator();
while (it.hasNext()) {
if (it.next().isExpired()) it.remove();
}
// NEW: one-liner
sessions.removeIf(Session::isExpired);For ArrayList, removeIf is batch-optimized[Java Collections Framework]: marks removed elements in a single pass, compacts survivors in one step. No O(n) shift cost per removal. The method returns true if anything was removed — useful for logging.
Factory Methods and Bulk Operations
Use List.of, Set.of, Map.of instead of wrapping mutable lists. Collections.unmodifiableList() wraps the original; the factory methods are truly independent:
// WRONG: wrapper — mutable original still affects the "unmodifiable" view
List<String> mutable = new ArrayList<>();
mutable.add("read");
List<String> perms = Collections.unmodifiableList(mutable);
mutable.add("admin"); // now perms has 3 elements!
// RIGHT: truly immutable
List<String> perms = List.of("read", "write");
Set<String> roles = Set.of("admin", "editor");
Map<String, Integer> limits = Map.of("free", 100, "pro", 10_000);Key behaviors[Java Collections Framework]: null elements throw NullPointerException (intentional — nulls hide bugs), duplicate keys in Map.of throw IllegalArgumentException, and Set.of iteration order is randomized. Use List.copyOf(mutableList) to freeze an existing collection — returns the same list if already immutable.
Map.merge is the most concise way to count and accumulate:
Map<String, Integer> freq = new HashMap<>();
for (String word : words) {
freq.merge(word, 1, Integer::sum); // insert 1 or add 1
}
// Also atomic for ConcurrentHashMap
Map<String, BigDecimal> totals = new ConcurrentHashMap<>();
for (Order order : orders) {
totals.merge(order.customerId(), order.amount(), BigDecimal::add);
}Use replaceAll for in-place bulk transformation:
Map<String, String> tags = new HashMap<>(loadTags());
tags.replaceAll((k, v) -> v.strip().toLowerCase());If a merge function returns null, the entry is removed — useful for expiration logic.
SequencedCollection and Choosing Map Implementations
Java 21's SequencedCollection unifies first/last access across List, Deque, and SortedSet:
// Java 21: uniform API
SequencedCollection<String> items = List.of("a", "b", "c");
String first = items.getFirst(); // "a"
String last = items.getLast(); // "c"
SequencedCollection<String> rev = items.reversed(); // O(1) view
for (String s : rev) { } // iterates c, b, aChoose your Map implementation strategically:
| Type | Use | Note |
|---|---|---|
HashMap | General-purpose lookups | Fastest, no ordering |
ConcurrentHashMap | Multi-threaded caches and counters | Fine-grained locking, atomic methods |
LinkedHashMap | LRU caches | removeEldestEntry override, O(1) access-order |
TreeMap | Sorted views, range queries | O(log n) per operation, use only if you need continuous sorted access |
EnumMap | Enum-keyed maps | Fastest option for enum keys |
Default to HashMap. Don't reach for TreeMap unless you need subMap() or continuous sorted iteration — a one-time stream().sorted() is faster for reads. The LinkedHashMap LRU pattern is the simplest bounded cache in the JDK:
Map<String, UserProfile> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 1000; // evict when > 1000 entries
}
};The third constructor arg (true) switches to access-order — every get moves the entry to the tail.
Concurrent Collections: When ConcurrentHashMap Beats synchronized
The classic Java production deadlock pattern: a hand-rolled synchronized (myMap) { myMap.computeIfAbsent(...) } inside a code path that also calls another synchronised method on the same monitor — a deadlock waiting for a traffic burst[Java Collections Framework]. We debugged this exact failure on multiple production teams. The fix is always to drop the explicit lock in favour of ConcurrentHashMap, which uses fine-grained striping under the hood.
The contention model in one picture — coarse-grained synchronized serialises every operation through a single monitor; ConcurrentHashMap stripes the locks across bins so readers and writers on different keys never collide:
graph TB
subgraph Sync["synchronized (cache) { ... }"]
T1["Thread A<br/>get('x')"] --> M1{"Single<br/>monitor"}
T2["Thread B<br/>put('y')"] --> M1
T3["Thread C<br/>get('z')"] --> M1
M1 -->|"serialised<br/>1 op at a time"| Cache1[(Map)]
end
subgraph CHM["ConcurrentHashMap (Java 8+)"]
T4["Thread A<br/>get('x')"] --> B1{"bin('x')<br/>lock-free read"}
T5["Thread B<br/>put('y')"] --> B2{"bin('y')<br/>fine-grained lock"}
T6["Thread C<br/>get('z')"] --> B3{"bin('z')<br/>lock-free read"}
B1 --> Cache2[(Map)]
B2 --> Cache2
B3 --> Cache2
end
Sync -->|"replace coarse lock<br/>with bin striping"| CHM
style M1 fill:#fdd
style B1 fill:#dfd
style B2 fill:#dfd
style B3 fill:#dfd
The red node is the contention point that disappears when you migrate. Reads on different keys are fully concurrent; writes only block other operations on the same bin (typically 1/16 to 1/64 of the keyspace).
The decision rule is simple: if writes are common, use ConcurrentHashMap; if writes are rare and reads dominate, CopyOnWriteArrayList / CopyOnWriteArraySet are cheaper because reads are lock-free[Java Collections Framework].
| Pattern | Use when | Avoid when |
|---|---|---|
ConcurrentHashMap | Frequent reads + writes; computeIfAbsent caching | Single-threaded code (HashMap is faster) |
CopyOnWriteArrayList | Read-heavy lists, infrequent writes | Frequent writes (each write copies the array) |
Collections.synchronizedMap | Wrapping legacy code that needs Map interface only | New code (use ConcurrentHashMap) |
Manual synchronized block | Fine when the critical section needs >1 collection op | Single op — let the concurrent collection's striping work |
// Anti-pattern: deadlock-prone, coarse-grained
synchronized (cache) {
if (!cache.containsKey(key)) {
cache.put(key, computeExpensive(key));
}
}
// Production pattern: lock-free read path, atomic compute
private final ConcurrentHashMap<String, Result> cache = new ConcurrentHashMap<>();
public Result get(String key) {
return cache.computeIfAbsent(key, this::computeExpensive);
}computeIfAbsent on ConcurrentHashMap holds the bin lock only for the duration of the lambda. Long-running computations should be moved out of the lambda — load asynchronously, then putIfAbsent once the value is ready, so other readers do not block on a slow loader.
Production Checklist
Before deploying collection-heavy code:
- Replace
containsKey+putwithcomputeIfAbsent— eliminates race conditions - Swap
Collections.unmodifiableList()forList.copyOf()orList.of()— no backing mutable list - Replace anonymous
ComparatorwithComparator.comparingchains — more readable - Use
removeIfinstead of iterator loops — noConcurrentModificationException - For caches and counters, verify
computeIfAbsentdoesn't callput()on the same map — deadlock risk - Choose
HashMapby default; reach forConcurrentHashMaponly for multi-threaded access - Validate
mergelogic doesn't return null unintentionally — null values delete entries
Java 21+ Sequenced Collections: A Unified First/Last API
Before Java 21, asking for the first or last element of an ordered collection meant remembering a different idiom for every type. A linked list used getFirst and getLast. An ArrayList used get(0) and get(size() - 1). A TreeSet used first and last. A LinkedHashSet had no direct API at all — you had to spin up an iterator or call stream().findFirst(). JEP 431 introduced the SequencedCollection, SequencedSet, and SequencedMap interfaces to unify this surface and added covariant reversed views that read in reverse without copying.
The new methods cost you nothing if you never use them, and they cost very little when you do — reversed returns a lightweight view that shares storage with the source. Here is the full surface in one place:
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.SequencedCollection;
import java.util.SequencedMap;
public class SequencedDemo {
public static void main(String[] args) {
// ArrayList now implements SequencedCollection
var orders = new ArrayList<String>();
orders.add("ord-100");
orders.add("ord-101");
orders.add("ord-102");
String head = orders.getFirst(); // "ord-100" — replaces orders.get(0)
String tail = orders.getLast(); // "ord-102" — replaces orders.get(orders.size() - 1)
orders.addFirst("ord-099"); // O(n) on ArrayList; O(1) on ArrayDeque
orders.removeLast(); // returns "ord-102"
// O(1) reversed VIEW — not a copy
SequencedCollection<String> rev = orders.reversed();
for (String id : rev) {
System.out.println(id);
}
// LinkedHashSet finally exposes first/last without an iterator dance
var seen = new LinkedHashSet<String>();
seen.add("alpha");
seen.add("beta");
seen.add("gamma");
System.out.println(seen.getFirst()); // "alpha"
// SequencedMap unlocks ordered map traversal both directions
SequencedMap<String, Integer> ranks = new LinkedHashMap<>();
ranks.put("gold", 1);
ranks.put("silver", 2);
ranks.put("bronze", 3);
System.out.println(ranks.firstEntry().getKey()); // "gold"
System.out.println(ranks.reversed().firstEntry()); // bronze=3
}
}The trap to watch for: addFirst and removeFirst on an ArrayList are still O(n) operations because every later element shifts. If you find yourself adding to the head frequently, switch the variable's static type to ArrayDeque, which provides amortised O(1) for both ends. Sequenced APIs make ergonomics uniform, but they do not change asymptotic costs.
reversed is the second hidden upgrade. Earlier code that wanted reverse iteration either reversed the source in place (mutating shared state) or created a defensive copy first and then called Collections.reverse on the copy (allocating an entirely new buffer). The new view-based reversal allocates nothing and stays consistent with the underlying collection if it changes. Bear in mind that mutations through the reversed view do propagate back — rev.removeFirst() removes the last element of the original.
Stream API Gotchas: Parallel Streams, toList, and Collector Choice
The Stream API rewards declarative thinking, but two specific corners produce subtle production bugs. The first is reaching for parallelStream because it sounds free. The second is treating Collectors.toList, Collectors.toUnmodifiableList, and Stream.toList as interchangeable.
Parallel streams use the common ForkJoinPool, which is shared across the entire JVM. If you call parallelStream inside a request handler and a large CPU-bound task is already running on the same pool, your handler's threads queue up behind it. Worse, parallel streams with a small per-element cost spend more time on splitting work than on the work itself. The break-even point lives somewhere around a few thousand elements doing meaningful arithmetic; below that, sequential streams win.
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class StreamGotchas {
// BAD: parallel stream over a tiny list shares the common pool
// and pays the splitter cost for nothing
long sumSmallBad(List<Integer> xs) {
return xs.parallelStream().mapToLong(Integer::longValue).sum();
}
// GOOD: stay sequential below the break-even point
long sumSmallGood(List<Integer> xs) {
return xs.stream().mapToLong(Integer::longValue).sum();
}
// GOOD: when you really need parallelism, isolate it on a private pool
// so request-handler threads do not contend with batch work
long sumLargeIsolated(int[] xs) throws Exception {
var pool = new ForkJoinPool(8);
try {
return pool.submit(() ->
IntStream.of(xs).parallel().mapToLong(i -> i).sum()
).get();
} finally {
pool.shutdown();
}
}
// toList() vs Collectors.toList() vs toUnmodifiableList() —
// they have different mutability and null contracts
void collectorContrast(java.util.stream.Stream<String> in) {
// Stream.toList() — Java 16+, returns an UNMODIFIABLE list, allows null
var a = in.toList();
// Collectors.toList() — returns an ArrayList; mutation allowed; allows null
var b = in.collect(Collectors.toList());
// Collectors.toUnmodifiableList() — unmodifiable, REJECTS null with NPE
var c = in.collect(Collectors.toUnmodifiableList());
}
}The contract differences bite during refactors. A method that used to return Collectors.toList() and was relied on for downstream add calls will silently break callers if you swap to Stream.toList. Conversely, returning Collectors.toList from a public API leaks a mutable list and invites callers to mutate your data. The conservative default is Stream.toList for read-only outputs and Collectors.toUnmodifiableList when you also want an explicit null-rejection check.
groupingBy is the most commonly misused collector. The default downstream collector materialises an ArrayList per group, which becomes a hotspot when groups are tiny. If you only need counts, use Collectors.counting. If you need uniques, use Collectors.toSet so duplicate values do not bloat the per-group list. And if the grouping key is enum-valued, pass a supplier that constructs an EnumMap — the speedup is meaningful at request scale.
import java.util.EnumMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
enum Region { US, EU, APAC }
record Order(Region region, long cents) {}
public class GroupingTuned {
Map<Region, Long> totalsByRegion(Stream<Order> orders) {
return orders.collect(Collectors.groupingBy(
Order::region,
() -> new EnumMap<>(Region.class),
Collectors.summingLong(Order::cents)
));
}
}That single supplier swap moves the result map from a 16-bucket HashMap to a flat array indexed by enum ordinal, eliminating hashing and giving you better cache behaviour on the read path.
A JMH Benchmark Sketch: HashMap vs ConcurrentHashMap vs Caffeine
Choosing a cache implementation by intuition is how teams end up with ConcurrentHashMap everywhere — including single-threaded code paths where it is slower than HashMap because of the bin-locking overhead on writes. A short JMH (Java Microbenchmark Harness) sketch keeps you honest. The harness amortises JIT warmup, runs each benchmark on dedicated threads, and reports throughput with confidence intervals so you can compare like for like.
The benchmark below covers three workloads — a single-threaded hot read, a multi-threaded mixed read/write, and a Caffeine cache with eviction — using the same 100k key range. Run it on your target hardware, not your laptop, before you trust the numbers.
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.openjdk.jmh.annotations.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 2)
@State(Scope.Benchmark)
@Fork(1)
public class CacheBench {
static final int KEYS = 100_000;
Map<Integer, String> hashMap;
Map<Integer, String> concurrentMap;
Cache<Integer, String> caffeine;
@Setup
public void setup() {
hashMap = new HashMap<>(KEYS);
concurrentMap = new ConcurrentHashMap<>(KEYS);
caffeine = Caffeine.newBuilder()
.maximumSize(KEYS)
.recordStats()
.build();
for (int i = 0; i < KEYS; i++) {
String v = "v-" + i;
hashMap.put(i, v);
concurrentMap.put(i, v);
caffeine.put(i, v);
}
}
@Benchmark
public String hashMapGet() {
return hashMap.get(ThreadLocalRandom.current().nextInt(KEYS));
}
@Benchmark
@Threads(8)
public String concurrentMapGet() {
return concurrentMap.get(ThreadLocalRandom.current().nextInt(KEYS));
}
@Benchmark
@Threads(8)
public String caffeineGet() {
return caffeine.getIfPresent(ThreadLocalRandom.current().nextInt(KEYS));
}
}On a typical x86 server-class box you will see HashMap win the single-threaded read benchmark by 20-30 percent over ConcurrentHashMap, because the lock-free read path on ConcurrentHashMap still pays for volatile reads on the bin head. Across eight threads, ConcurrentHashMap pulls ahead because HashMap is unsafe under concurrent writes (and indeed will livelock if you try). Caffeine's read path sits within five percent of ConcurrentHashMap and pulls ahead the moment you turn on TTL eviction, expire-after-access, or weight-based bounds — LinkedHashMap's removeEldestEntry cannot match Caffeine's W-TinyLFU eviction algorithm for hit ratio.
Three rules of thumb fall out of the numbers:
- If the cache is read by exactly one thread for its entire lifetime,
HashMapis the correct choice. Reach forConcurrentHashMaponly when concurrency is real. - For caches with eviction, retire
LinkedHashMap-based LRUs and adopt Caffeine. The hit-ratio gap is large enough to dominate any micro-optimisation on the lookup path. - Always benchmark with
@Threadsset to a realistic value. A single-threaded JMH run will mislead you into pickingHashMapfor a code path that actually has contention.
Frequently Asked Questions
What is the difference between List.of() and Collections.unmodifiableList()?
List.of() creates a truly immutable list that rejects null elements and throws UnsupportedOperationException on any mutation. Collections.unmodifiableList() wraps a mutable list — the wrapper is unmodifiable but the underlying list can still be mutated through the original reference.
Can computeIfAbsent cause a deadlock with ConcurrentHashMap?
The mapping function must not modify the same map. A detectably recursive update throws IllegalStateException ("Recursive update"); other threads writing to the same bin block until the lambda returns, which becomes a true deadlock only if the lambda waits on one of those blocked threads. computeIfAbsent holds the bin lock for the whole lambda — compute the value outside it and use putIfAbsent instead.
How do I avoid ConcurrentModificationException when removing from a collection?
Use collection.removeIf(predicate) instead of removing elements during iteration with a for-each loop. removeIf handles the internal iteration safely and reads as a single declarative statement.
What is SequencedCollection in Java 21?
SequencedCollection is a new interface that provides getFirst(), getLast(), addFirst(), addLast(), and reversed() methods for ordered collections. It unifies access patterns across List, Deque, SortedSet, and LinkedHashSet that previously required different method names for the same operation.
Keep Reading
- Java Streams: Pipeline Internals, Performance Traps, and Production Patterns — Stream collectors like
groupingByandtoMapbuild on these collection APIs - Modern Java Features: A Practical Guide from Java 8 to 21 — Records and sealed classes that pair with modern collection patterns
- Java Virtual Threads: The Concurrency Revolution — How
ConcurrentHashMapand structured concurrency interact under Project Loom - Java Date/Time API Guide —
NavigableMap<Instant, T>for time-bucketed collections - Java Testing with JUnit 5 and Mockito — Concurrency tests for ConcurrentHashMap / CopyOnWriteArrayList semantics
Engineering Team
A multidisciplinary team of backend engineers, architects, and DevOps practitioners shipping deep dives into distributed systems and production infrastructure.
Read Next
Java Streams: Pipeline Internals, Performance Traps, and Production Patterns
Java streams: pipeline internals, where streams beat loops, parallel stream traps, and when to use collectors vs teeing.
Java Date and Time API: The Definitive Guide to java.time
Stop fighting java.util.Date. Master LocalDateTime, ZonedDateTime, Instant, and Duration — predictable, thread-safe time handling.
Java Singleton Pattern: Thread-Safe, Reflection-Proof, Serialization-Safe
Java singletons: enum patterns, double-checked locking, and holder-classes — and when dependency injection is the better answer.