The Java collections framework has 25+ years of history. The core types are stable; the modern additions (immutable factories, sequenced collections) round out long-standing gaps. This page is the practical guide — which collection to pick when, the immutable variants, and the patterns that have aged well.
Ordered, allows duplicates, indexed access.
| Implementation | When to use |
|---|---|
ArrayList | Default. Backed by array; fast random access; slow inserts in middle |
LinkedList | Almost never. Doubly-linked list; rarely the right answer |
CopyOnWriteArrayList | Read-heavy concurrent access; writes copy the entire array |
Default to ArrayList. The cases for the others are narrow.
Unordered (or ordered), no duplicates.
| Implementation | When to use |
|---|---|
HashSet | Default. Hash-based, no order |
LinkedHashSet | Insertion-order iteration matters |
TreeSet | Natural-order or custom-order iteration |
ConcurrentSkipListSet | Concurrent ordered set |
EnumSet | Set of enum values; bit-vector backed; very fast |
HashSet is the right default for most cases. LinkedHashSet if you need to preserve insertion order.
Key-value pairs, unique keys.
| Implementation | When to use |
|---|---|
HashMap | Default. Fast; no ordering |
LinkedHashMap | Insertion-order or access-order matters |
TreeMap | Sorted by key |
ConcurrentHashMap | Concurrent map (the right concurrent map) |
EnumMap | Map keyed by enum; very fast |
HashMap for general use. ConcurrentHashMap for shared mutable maps across threads. EnumMap when keyed by enum (faster, less memory).
| Implementation | When to use |
|---|---|
ArrayDeque | Default for stack/queue use; faster than LinkedList |
PriorityQueue | Heap-based priority queue |
LinkedBlockingQueue | Producer-consumer with blocking semantics |
ConcurrentLinkedQueue | Concurrent queue, non-blocking |
ArrayDeque for non-concurrent stack/queue. The Stack class is legacy; do not use it.
List<String> names = List.of("Alice", "Bob", "Carol");
Set<Integer> ports = Set.of(80, 443, 8080);
Map<String, String> headers = Map.of("Content-Type", "application/json");
The result is genuinely immutable — modification throws UnsupportedOperationException. These are the right default for "I have a small fixed collection."
For larger or computed immutable collections:
List<X> result = stream.collect(Collectors.toUnmodifiableList());
// or in Java 16+
List<X> result = stream.toList();
Long-standing API gaps filled. SequencedCollection, SequencedSet, SequencedMap interfaces add:
getFirst(), getLast()addFirst(e), addLast(e)reversed() — returns a reversed viewremoveFirst(), removeLast()Implemented by List, LinkedHashSet, LinkedHashMap, Deque, etc. Removes the need for awkward iterator().next() patterns to access the first element.
for (String name : names) {
process(name);
}
Almost always the right way to iterate when you don't need the index.
Iterator<String> it = names.iterator();
while (it.hasNext()) {
if (shouldRemove(it.next())) {
it.remove();
}
}
for (int i = 0; i < names.size(); i++) {
System.out.println(i + ": " + names.get(i));
}
names.stream()
.filter(n -> n.length() > 5)
.forEach(this::process);
See JavaStreamsAndFunctionalProgramming.
| Operation | ArrayList | LinkedList | HashMap | TreeMap |
|---|---|---|---|---|
| Get by index | O(1) | O(n) | n/a | n/a |
| Get by key | n/a | n/a | O(1) avg | O(log n) |
| Insert at end | O(1) amortized | O(1) | O(1) avg | O(log n) |
| Insert at front | O(n) | O(1) | n/a | n/a |
| Remove by value | O(n) | O(n) | O(1) avg | O(log n) |
| Iteration | O(n) | O(n) | O(n) | O(n) ordered |
The "amortized" on ArrayList insert: occasionally an internal array resize is O(n), averaged out across many inserts.
ArrayList(int initialCapacity) — provide expected size if known to avoid resizingHashMap(int initialCapacity) — initial bucket count; avoid rehashingHashMap(int capacity, float loadFactor) — load factor 0.75 is the defaultPre-sizing collections has a real performance impact when sizes are known and large.
Default collections are not thread-safe. Three options for concurrent access:
ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue) — designed for concurrent accessCollections.synchronizedXxx) — single-lock approach; simple but contention can be highFor maps shared across threads, ConcurrentHashMap is almost always the right choice.
LinkedList because "it's faster for inserts." It isn't, in practice. Use ArrayList.Iterator.remove() or copy.HashMap resize thrashing. Pre-size if you know the count.Vector or Hashtable. Legacy, synchronized for everything. Use modern equivalents.