Back to News & Insights
JavaScript August 12, 2026 · 4 min read

Java Collections Framework: When to Use What – The Fellowship of the List

The Quest Begins (The "Why") I was knee‑deep in a legacy codebase the other day, trying to...

Java Collections Framework: When to Use What – The Fellowship of the List

I was knee‑deep in a legacy codebase the other day, trying to shove a list of user IDs into a method that expected an immutable collection. I reached for Arrays.asList(ids) because it felt familiar, tossed the list into the method, and… boom! UnsupportedOperationException when the code tried to add a default value. I stared at the stack trace, muttered “Not again,” and realized I’d fallen into the same trap that trips up so many Java devs: confusing a fixed‑size list with a truly immutable one.

That moment sparked a mini‑adventure. I wanted to uncover the hidden gems of the Java Collections Framework—those little‑known features that feel like discovering a secret passage in a dungeon. If you’ve ever felt like you’re wrestling with the API instead of letting it work for you, stick around. We’re about to turn frustration into finesse.

The Revelation (The Insight) Immutable Factory Methods – List.of(), Set.of(), Map.of()

Most of us grew up with Arrays.asList. It’s handy, but it returns a fixed‑size list backed by the original array. You can change the elements, but you can’t add or remove them—leading to those pesky UnsupportedOperationExceptions when a library assumes mutability.

Enter the static factory methods introduced in Java 9: List.of(), Set.of(), Map.of(). They give you genuinely immutable collections. No backing array, no surprise mutations, and they’re super concise.

Gotcha: If you accidentally pass null to any of these factories, you’ll get a NullPointerException right up front. That’s actually a good thing—it fails fast instead of letting a null lurk somewhere deep in your code. EnumSet and EnumMap – The Enum‑Only Power‑Ups

When your keys (or values) are enums, the generic HashSet or HashMap feels like using a sledgehammer to crack a nut. Java provides two specialized implementations: EnumSet and EnumMap. They store enum constants in a bit‑set or an array, making them blazingly fast and memory‑efficient.

Gotcha: You can’t store null in an EnumSet (it throws NullPointerException), and EnumMap requires that all keys come from the same enum type. Violate that and you’ll get a clear compile‑time error—again, a feature, not a bug. ConcurrentHashMap’s computeIfAbsent – Thread‑Safe Lazy Init

Before Java 8, lazily initializing a cache entry in a concurrent map meant writing clumsy double‑checked locking boilerplate or synchronizing the whole map—both error‑prone and slow.

ConcurrentHashMap.computeIfAbsent(key, k -> expensiveComputation(k)) does the check‑then‑compute atomically. If two threads race for the same key, only one executes the mapping function; the other gets the already‑computed value. No explicit locks, no race conditions.

Gotcha: The mapping function must be side‑effect‑free with respect to the map (i.e., don’t try to insert other keys inside it), or you could deadlock yourself. Keep it pure, and you’re golden.

Imagine you have an enum for game characters and you need to track which ones are currently active:

Trying to add a non‑enum value? The compiler stops you. Need a map from hero to mana cost?

Two threads calling getConfig("foo") at the same time? Only one runs parseConfig; the other gets the already‑cached result. No synchronized blocks, no volatile flags—just one tidy line.

Mastering these niche collections turns you from a “Java coder who fights the API” into a “Java wizard who makes the API work for you.” Immutable factories give you predictable, thread‑safe data structures with zero boilerplate—perfect for DTOs, function parameters, or any place where mutability is a liability. EnumSet/EnumMap let you harness the compile‑time safety of enums while enjoying performance that rivals primitive arrays. If your domain is defined by a fixed set of constants (think game states, HTTP status codes, or permission flags), these are the go‑to tools. ConcurrentHashMap.computeIfAbsent eliminates the classic double‑checked locking pattern, making concurrent caches both safer and easier to read. Less code means fewer bugs, and the atomic guarantee means you can scale confidently.

When you start reaching for these tools instinctively, your code becomes shorter, clearer, and far less prone to the subtle concurrency bugs that haunt many Java projects. You’ll spend less time debugging UnsupportedOperationExceptions and more time building the features that actually matter.

Here’s a challenge: take a piece of code where you currently use Arrays.asList or a plain HashMap for enum keys, and replace it with the appropriate immutable factory or EnumSet/EnumMap. Notice how the compiler helps you catch mistakes early, and how the runtime feels snappier.

If you’re feeling bold, try refactoring a lazy‑loaded cache to use ConcurrentHashMap.computeIfAbsent. Watch the synchronized blocks disappear and the performance stay steady under load.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation