Java Collections Framework Cheatsheet
A pragmatic tour of Java's collection interfaces and implementations, with guidance on choosing between List, Set, Map, and Queue variants in real applications.
What you'll learn
- ✓The shape of the Collections Framework hierarchy
- ✓When to pick ArrayList vs LinkedList vs ArrayDeque
- ✓How HashMap, LinkedHashMap, and TreeMap differ
- ✓Concurrent collection options under contention
- ✓Common iteration and modification pitfalls
Prerequisites
- •Basic familiarity with Java syntax and generics
What and Why
The Java Collections Framework (JCF) is the set of interfaces and classes in java.util that model groups of objects. It is one of the most heavily used parts of the standard library, and choosing the right collection often has bigger performance impact than micro-optimizing your code.
The framework gives you a uniform API: every collection supports iteration, size queries, and bulk operations. That uniformity is the productivity win. The cost is that you have to know which implementation matches your access pattern.
Mental Model
Think of the framework as three trees rooted in Iterable. Collection branches into List, Set, and Queue. Map lives on its own because it stores key-value pairs rather than elements. Each interface has several concrete classes optimized for a specific tradeoff between memory layout, ordering, and concurrency.
Iterable
|
Collection Map
|-- List |-- HashMap
| |-- ArrayList |-- LinkedHashMap
| |-- LinkedList |-- TreeMap
|-- Set |-- ConcurrentHashMap
| |-- HashSet
| |-- LinkedHashSet
| |-- TreeSet
|-- Queue / Deque
|-- ArrayDeque
|-- PriorityQueue
|-- LinkedBlockingQueue Hands-on Example
Here is a small program that demonstrates the most useful collections.
import java.util.*;
import java.util.concurrent.*;
public class CollectionsDemo {
public static void main(String[] args) {
// Random access, dynamic size
List<String> names = new ArrayList<>(List.of("Ada", "Bob", "Cleo"));
names.add("Dan");
System.out.println(names.get(2)); // Cleo
// Unique elements, no order
Set<Integer> ids = new HashSet<>();
ids.add(42); ids.add(42); ids.add(7);
System.out.println(ids.size()); // 2
// Sorted by key
NavigableMap<String, Integer> scores = new TreeMap<>();
scores.put("Ada", 90);
scores.put("Bob", 75);
System.out.println(scores.firstEntry()); // Ada=90
// FIFO queue
Deque<String> q = new ArrayDeque<>();
q.offer("first"); q.offer("second");
System.out.println(q.poll()); // first
// Thread-safe map
ConcurrentMap<String, Integer> hits = new ConcurrentHashMap<>();
hits.merge("home", 1, Integer::sum);
}
}
A few practical heuristics:
ArrayListis the defaultList. UseLinkedListonly when you really need O(1) insertion at both ends, and preferArrayDequefor that.HashMapis the defaultMap. UseLinkedHashMapwhen you need insertion-order iteration (great for LRU caches viaremoveEldestEntry).TreeMapandTreeSetgive you sorted views and range queries viasubMap,headSet,tailMap.- For concurrency, prefer
ConcurrentHashMapandCopyOnWriteArrayListoverCollections.synchronizedXxxwrappers.
Common Pitfalls
- Mutating during iteration: modifying a non-concurrent collection while iterating with an enhanced for-loop throws
ConcurrentModificationException. UseIterator.remove()orremoveIfinstead. Arrays.asListis fixed size: it returns a list backed by the original array.addandremovethrowUnsupportedOperationException. Wrap it innew ArrayList<>(...)if you need to grow it.- Hash code contracts: putting mutable objects into a
HashSetand then mutating them breaks lookups silently. OverrideequalsandhashCodeconsistently, and keep keys effectively immutable. - Null keys and values:
HashMapallows them,TreeMapandConcurrentHashMapdo not.
Practical Tips
Size your ArrayList and HashMap up front when you know the cardinality. The default load factor is 0.75, so new HashMap<>(expectedSize * 4 / 3 + 1) avoids the first resize.
Prefer List.of, Set.of, and Map.of for immutable literals introduced in Java 9. They are compact and reject nulls early.
For grouping operations, lean on Stream.collect(Collectors.groupingBy(...)) instead of hand-rolled loops. It composes well with counting, summingInt, and mapping.
When profiling memory, remember that HashMap entries carry significant overhead per node (key, value, hash, next pointer). For small fixed maps, an array of records can be cheaper.
Wrap-up
The JCF rewards thinking about access patterns before you reach for a class name. Start with ArrayList, HashMap, and ArrayDeque. Reach for ordered, sorted, or concurrent variants only when the requirement is real. Once these defaults are muscle memory, you can read most Java code at a glance and predict its performance characteristics.
Related articles
- Java Java Streams API Deep Dive
A practical tour of the Java Streams API: how it works, when to use it, lazy evaluation, collectors, parallel streams, and the pitfalls that trip up newcomers.
- Java Java Virtual Threads Explained
Virtual threads make blocking I/O cheap again. Here is how they work under the hood, when to use them, and what changes in your code, from a practical perspective.
- Java Java Collections Framework: List, Set, Map
Pick the right Java collection by performance and semantics. ArrayList vs LinkedList, HashMap vs TreeMap, immutability, and iteration without surprises.
- Java Java Concurrency with CompletableFuture
How CompletableFuture composes asynchronous work in Java: chaining, combining, error handling, executors, and the patterns that keep concurrent code readable.