Collections Framework
Architecture Overview
Section titled “Architecture Overview”The Java Collections Framework (JCF) is a unified architecture for representing and manipulating Collections. It was introduced in JDK 1.2 (1998) and has been extended in every major release since. The framework is built around a hierarchy of interfaces that define contracts for different Collection types, with concrete implementations that provide specific performance characteristics And behavioral guarantees.
The core design goals were: (1) reduce programming effort by providing high-performance data Structures, (2) reduce effort required to learn and use new APIs by providing a consistent Architecture, and (3) foster software reuse by providing interoperable interfaces.
graph TD
Iterable["Iterable<E><br/>iterator(), forEach(), spliterator()"]
Collection["Collection<E><br/>size(), isEmpty(), add(), remove(),<br/>contains(), stream(), toArray()"]
List["List<E><br/>get(), set(), add(index),<br/>indexOf(), subList()"]
Set["Set<E><br/>no duplicates"]
Queue["Queue<E><br/>poll(), peek(), offer()"]
Deque["Deque<E><br/>addFirst(), addLast(),<br/>pollFirst(), pollLast()"]
Iterable --> Collection
Collection --> List
Collection --> Set
Collection --> Queue
Queue --> Deque
SortedSet["SortedSet<E><br/>headSet(), tailSet(), subSet()"]
NavigableSet["NavigableSet<E><br/>lower(), higher(), ceiling(), floor()"]
Set --> SortedSet
SortedSet --> NavigableSet
SortedMap["SortedMap<K,V><br/>headMap(), tailMap(), subMap()"]
NavigableMap["NavigableMap<K,V><br/>lowerKey(), higherKey(), firstEntry(), lastEntry()"]
Map["Map<K,V><br/>put(), get(), containsKey(),<br/>keySet(), values(), entrySet()"]
Map -.->|not in Collection hierarchy| Collection
Map --> SortedMap
SortedMap --> NavigableMap
style Iterable fill:#e8e8e8
style Collection fill:#d4e6f1
style Map fill:#d4e6f1
style List fill:#d5f5e3
style Set fill:#d5f5e3
style Queue fill:#d5f5e3
style Deque fill:#d5f5e3## The Iterable InterfaceIterable<T> is the root of the collection type hierarchy. Any object implementing Iterable<T> Can be used with the enhanced for-each loop.
public interface Iterable<T> { Iterator<T> iterator();
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }
default Spliterator<T> spliterator() { return Spliterators.spliteratorUnknownSize(iterator(), 0); }}The enhanced for-each loop is syntactic sugar that the compiler translates into an Iterator-based Loop:
// Source codefor (String s : list) { System.out.println(s);}
// What the compiler generatesfor (Iterator<String> it = list.iterator(); it.hasNext(); ) { String s = it.next(); System.out.println(s);}List Implementations
Section titled “List Implementations”ArrayList
Section titled “ArrayList”ArrayList<E> is a resizable array implementation of List. It is the default choice for List-based collections in most applications because it provides O(1) random access and has excellent Cache locality due to its contiguous memory layout.
Internal Structure and Growth
Section titled “Internal Structure and Growth”An ArrayList stores elements in a backing Object[] array. When the array is full and a new Element is added, the list allocates a new, larger array and copies all elements from the old array To the new one.
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify inner class access private int size;
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } }
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // lazy allocation }}The growth strategy in OpenJDK uses a formula of newCapacity = oldCapacity + (oldCapacity >> 1) (i.e., 1.5x the old capacity). This amortizes the cost of resizing across many insertions:
private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1); // grow by 50% of old capacity return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; }}The initial new ArrayList<>() does not allocate an array of size 10 immediately. It stores a Shared static empty array reference. The first call to add() triggers allocation of an array of Default capacity 10. This lazy allocation is a significant optimization for applications that create Many empty lists that are never populated.
public boolean add(E e) { modCount++; add(e, elementData, size); return true;}
private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); // resize triggered only when needed elementData[s] = e; size = s + 1;}Time Complexity
Section titled “Time Complexity”| Operation | Average | Worst Case | Notes |
|---|---|---|---|
get(index) | O(1) | O(1) | Direct array index |
set(index, element) | O(1) | O(1) | Direct array index |
add(element) | O(1) amortized | O(n) | O(n) when resize needed |
add(index, element) | O(n) | O(n) | Must shift elements right |
remove(index) | O(n) | O(n) | Must shift elements left |
remove(Object) | O(n) | O(n) | Linear scan + shift |
contains(Object) | O(n) | O(n) | Linear scan |
indexOf(Object) | O(n) | O(n) | Linear scan |
LinkedList
Section titled “LinkedList”LinkedList<E> is a doubly-linked list implementation of both List and Deque. Each element is Wrapped in a node that holds references to the previous and next nodes.
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { transient Node<E> first; transient Node<E> last; transient int size;
private static class Node<E> { E item; Node<E> next; Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }}Time Complexity
Section titled “Time Complexity”| Operation | Average | Worst Case | Notes |
|---|---|---|---|
get(index) | O(n) | O(n) | Must traverse from head or tail |
add(element) | O(1) | O(1) | Appends to tail |
add(index, element) | O(n) | O(n) | Must traverse to index |
remove(index) | O(n) | O(n) | Must traverse to index |
addFirst() / addLast() | O(1) | O(1) | Direct pointer manipulation |
removeFirst() / removeLast() | O(1) | O(1) | Direct pointer manipulation |
ArrayList vs LinkedList: When to Use Each
Section titled “ArrayList vs LinkedList: When to Use Each”// Use ArrayList when:// 1. You primarily access elements by index (random access dominates)// 2. You iterate much more than you insert/remove in the middle// 3. Memory locality matters (ArrayList elements are contiguous in memory,// giving far better CPU cache hit rates than linked list node pointers)List<String> names = new ArrayList<>(1000); // set initial capacity if knownnames.add("Alice");names.get(500); // O(1) -- direct array access
// Use LinkedList when:// 1. You frequently add/remove at the head or tail (Deque operations)// 2. You need Queue/Deque semantics (FIFO, LIFO)// 3. You frequently insert or remove in the middle AND you have an iterator// already positioned at the insertion pointDeque<String> queue = new LinkedList<>();queue.addFirst("head");queue.addLast("tail");String head = queue.removeFirst(); // O(1)// Collections.binarySearch uses this check internallyif (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD) return indexedBinarySearch(list, key); // O(log n) with direct index accesselse return iteratorBinarySearch(list, key); // O(log n) but with O(n) iterator traversalSet Implementations
Section titled “Set Implementations”HashSet
Section titled “HashSet”HashSet<E> is backed by a HashMap<E, Object> instance. Each element is stored as a key in the Map, with a shared static PRESENT object as the value. It provides O(1) average-time performance For add``remove``containsAnd size operations.
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { private transient HashMap<E, Object> map; private static final Object PRESENT = new Object();
public HashSet() { map = new HashMap<>(); }
public boolean add(E e) { return map.put(e, PRESENT) == null; }
public boolean contains(Object o) { return map.containsKey(o); }
public boolean remove(Object o) { return map.remove(o) == PRESENT; }
public int size() { return map.size(); }}The contains() call delegates to HashMap.containsKey()Which first computes the hash, finds the Bucket, and then traverses the chain comparing elements with equals(). This is O(1) on average but Degrades to O(n) in the worst case if all elements hash to the same bucket.
LinkedHashSet
Section titled “LinkedHashSet”LinkedHashSet<E> extends HashSet but overrides the constructor to create a LinkedHashMap Instead of a HashMap. It maintains a doubly-linked list running through all of its entries, which Defines the insertion order. Unlike LinkedHashMap``LinkedHashSet does not support Access-order mode — iteration order always matches insertion order. This is the primary reason to Choose LinkedHashSet over HashSet.
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, false); // insertion order (accessOrder = false) }}The insertion-order guarantee has a cost: each entry carries two additional pointer fields (before, After) for the linked list, increasing per-entry memory overhead by 16 bytes on a 64-bit JVM with Compressed oops.
TreeSet
Section titled “TreeSet”TreeSet<E> is a NavigableSet implementation backed by a TreeMap. Elements are ordered using Their natural ordering (if they implement Comparable) or a Comparator provided at construction Time. It provides guaranteed O(log n) time for add``removeAnd contains.
// Natural ordering (elements must implement Comparable)Set<String> words = new TreeSet<>();words.add("delta");words.add("alpha");words.add("charlie");System.out.println(words); // [alpha, charlie, delta]
// Custom ordering via ComparatorSet<String> reverseWords = new TreeSet<>(Comparator.reverseOrder());reverseWords.addAll(words);System.out.println(reverseWords); // [delta, charlie, alpha]
// NavigableSet operations -- only available on TreeSet (and ConcurrentSkipListSet)NavigableSet<Integer> numbers = new TreeSet<>(List.of(10, 20, 30, 40, 50));numbers.lower(30); // 20 -- greatest element strictly less than 30numbers.floor(30); // 30 -- greatest element less than or equal to 30numbers.higher(30); // 40 -- least element strictly greater than 30numbers.ceiling(30); // 30 -- least element greater than or equal to 30numbers.subSet(20, 40); // [20, 30] -- range view (half-open interval)numbers.headSet(30); // [10, 20] -- elements less than 30numbers.tailSet(30); // [30, 40, 50] -- elements greater than or equal to 30TreeSet does not allow null elements (it would require comparing null, which throws NullPointerException). HashSet and LinkedHashSet allow at most one null element.
EnumSet
Section titled “EnumSet”EnumSet<E extends Enum<E>> is a specialized Set implementation for use with enum types. Internally, it is represented as a bit vector, where each bit corresponds to an enum constant. This Makes all operations O(1) and extremely memory-efficient.
public enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
EnumSet<Day> weekdays = EnumSet.range(Day.MON, Day.FRI);EnumSet<Day> weekend = EnumSet.complementOf(weekdays); // SAT, SUNEnumSet<Day> none = EnumSet.noneOf(Day.class);EnumSet<Day> all = EnumSet.allOf(Day.class);
// Internally stored as a single long (or long[]) bitmask// MON=1, TUE=2, WED=4, THU=8, FRI=16, SAT=32, SUN=64// weekdays = 0b0011111 = 31| Feature | HashSet | LinkedHashSet | TreeSet | EnumSet |
|---|---|---|---|---|
| Backing structure | HashMap | LinkedHashMap | TreeMap (red-black tree) | Bit vector |
| Ordering | None | Insertion order | Sorted (natural or Comparator) | Enum declaration order |
| Null elements | One allowed | One allowed | Not allowed | Not allowed |
add / contains / remove | O(1) avg | O(1) avg | O(log n) | O(1) |
| Element type | Any | Any | Comparable or Comparator | Enum only |
Map Implementations
Section titled “Map Implementations”HashMap
Section titled “HashMap”HashMap<K,V> is the most widely used Map implementation. It provides O(1) average-time performance For get and putBut does not guarantee any ordering of its entries.
Internal Structure
Section titled “Internal Structure”A HashMap is built on an array of buckets (called table). Each bucket is the head of a linked List (or, since Java 8, a balanced tree when a bucket’s chain exceeds a threshold). The bucket index For a key is determined by hash(key) & (table.length - 1)Where table.length is always a power Of two.
graph TD
subgraph HashMap["HashMap table[] (power of 2)"]
B0["bucket[0]<br/>(null)"]
B1["bucket[1]<br/>Node(K,V) --> Node(K,V) --> null"]
B2["bucket[2]<br/>TreeNode (red-black tree)"]
B3["bucket[3]<br/>(null)"]
B4["bucket[4]<br/>Node(K,V) --> null"]
B5["bucket[5]"]
B6["bucket[6]"]
B7["bucket[7]"]
end
subgraph CollisionChain ["Bucket 1: Separate Chaining"]
N1["Node(key=A, val=1)"]
N2["Node(key=B, val=2)"]
N3["Node(key=C, val=3)"]
N1 -->|"next"| N2
N2 -->|"next"| N3
N3 -->|"next"| Null1["null"]
end
subgraph TreeBin ["Bucket 2: Treeified (8+ nodes)"]
T1["TreeNode<br/>Red-Black Tree"]
end
B1 --- N1
B2 --- T1
style HashMap fill:#f8f9fa
style CollisionChain fill:#fff3cd
style TreeBin fill:#d4eddaHash Function
Section titled “Hash Function”HashMap applies a secondary hash function (a “scrambling” function) to the object’s hashCode() To spread higher bits into lower bits. This is critical because the bucket index depends only on the Lower bits (hash & (n-1) where n is a power of two). Without this secondary hash, keys that differ Only in higher bits would all land in the same bucket.
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}The expression h ^ (h >>> 16) XORs the upper 16 bits into the lower 16 bits. This ensures that Both the upper and lower bits of the original hash code influence the bucket selection, reducing Collisions when keys share similar lower bits.
Collision Resolution: Separate Chaining with Treeification
Section titled “Collision Resolution: Separate Chaining with Treeification”When two or more keys hash to the same bucket, they are stored in a linked list at that bucket Index. Since Java 8, when the number of nodes in a single bucket reaches 8 (the TREEIFY_THRESHOLD), the linked list is converted into a red-black tree. This improves worst-case Performance from O(n) to O(log n) for lookup in degenerate cases where many keys hash to the same Bucket.
// Simplified put logicfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // empty bucket -- direct insert else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // key already exists -- replace else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // tree bucket else { for (int binCount = 0; ; ++binCount) { // linked list traversal if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 8 nodes triggers treeification treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // found existing key p = e; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; // replace existing value return oldValue; } } ++modCount; if (++size > threshold) resize(); // grow the table when size exceeds capacity * loadFactor return null;}The load factor (default 0.75) determines when the table is resized. When size > capacity * loadFactorThe table is doubled in size and all entries are rehashed into the New buckets. A load factor of 0.75 balances time and space costs — it provides a good tradeoff Between collision probability and memory usage according to the Poisson distribution analysis in the Original HashMap documentation.
// Setting initial capacity when the expected size is known avoids costly resizes// Formula: capacity = expected_size / load_factor + 1Map<String, Integer> map = new HashMap<>(1000 / 0.75f + 1); // avoids resizing for ~1000 entriesTreeMap
Section titled “TreeMap”TreeMap<K,V> is a NavigableMap implementation backed by a red-black tree. All entries are kept In sorted order according to the natural ordering of the keys or a Comparator provided at Construction. It provides guaranteed O(log n) time for containsKey``get``putAnd remove.
Red-Black Tree Internals
Section titled “Red-Black Tree Internals”A red-black tree is a self-balancing binary search tree with the following invariants:
- Every node is either red or black.
- The root is always black.
- Every leaf (null node) is black.
- If a node is red, both its children are black (no two consecutive red nodes).
- Every path from a node to its descendant null nodes contains the same number of black nodes (black-height is uniform).
These invariants guarantee that the longest path from root to any leaf is at most twice the length Of the shortest path, which ensures O(log n) height and therefore O(log n) operations.
// Simplified TreeMap.Entry structurestatic final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK;
Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; }}When a new entry is inserted, it is placed as a red leaf at the correct position determined by the Binary search tree property. Then, the tree is rebalanced to restore the red-black invariants. The Rebalancing involves at most two rotations and recoloring operations.
// After insertion, fixUp is called to restore red-black tree propertiesprivate void fixAfterInsertion(Entry<K,V> x) { x.color = RED; while (x != null && x != root && x.parent.color == RED) { if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { Entry<K,V> y = rightOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { // Case 1: uncle is red -- recolor setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == rightOf(parentOf(x))) { // Case 2: uncle is black, x is right child -- left rotate x = parentOf(x); rotateLeft(x); } // Case 3: uncle is black, x is left child -- recolor + right rotate setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } else { // Symmetric cases for right parent } } root.color = BLACK; // Invariant 2: root is always black}NavigableMap<Integer, String> range = scores.subMap(5, 15); // [5, 15) — half-open System.out.println(range); // {5=Bob, 10=Charlie}
// subMap returns a VIEW, not a copy — changes to the view are reflected in the backing map range.put(7, “Eve”); System.out.println(scores); // {1=Alice, 5=Bob, 7=Eve, 10=Charlie, 15=Diana}
### LinkedHashMap
`LinkedHashMap<K,V>` extends `HashMap` and maintains a doubly-linked list that runs through all itsEntries. This linked list defines the iteration order, which is **insertion order by default** or**access order** (least-recently-used to most-recently-used) if constructed with`accessOrder = true`.
```java// Insertion-ordered LinkedHashMapMap<String, Integer> insertionOrder = new LinkedHashMap<>();insertionOrder.put("C", 3);insertionOrder.put("A", 1);insertionOrder.put("B", 2);System.out.println(insertionOrder.keySet()); // [C, A, B] -- insertion order
// Access-ordered LinkedHashMap (LRU cache basis)Map<String, Integer> lruOrder = new LinkedHashMap<>(16, 0.75f, true);lruOrder.put("C", 3);lruOrder.put("A", 1);lruOrder.put("B", 2);lruOrder.get("C"); // accessing C moves it to the endSystem.out.println(lruOrder.keySet()); // [A, B, C] -- access order (C moved to end)The accessOrder = true variant is the foundation for building LRU caches. By overriding removeEldestEntry() to return true when the map exceeds a certain size, the oldest (least-recently-used) entry is automatically evicted on each put:
// Simple LRU cache built on LinkedHashMappublic class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int maxEntries;
public LRUCache(int maxEntries) { super(maxEntries, 0.75f, true); // accessOrder = true this.maxEntries = maxEntries; }
@Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > maxEntries; }}ConcurrentHashMap<K,V> is a thread-safe Map designed for high concurrency. Unlike Hashtable or Collections.synchronizedMap()Which use a single lock for the entire map, ConcurrentHashMap Uses fine-grained locking to allow concurrent reads and writes.
Evolution
Section titled “Evolution”- Java 7: Segment-based locking. The map is divided into segments (default 16), each with its own
ReentrantLock. Concurrent writes to different segments do not block each other. Reads are lock-free. - Java 8+: The segment-based design was replaced with a flat array of nodes. Locking is done at the individual bucket level using CAS (compare-and-swap) and
synchronizedon the first node of a bucket. This eliminates the fixed segment count limitation and provides better scalability.
// Basic usage -- thread-safe without external synchronizationConcurrentHashMap<String, AtomicInteger> counts = new ConcurrentHashMap<>();
// Atomic compound operations (not possible with synchronizedMap)counts.computeIfAbsent("key", k -> new AtomicInteger(0));counts.computeIfPresent("key", (k, v) -> { v.incrementAndGet(); return v; });
// putIfAbsent is atomic -- avoids the check-then-act race conditioncounts.putIfAbsent("key", new AtomicInteger(0)); // atomic
// The following is NOT atomic with synchronizedMap -- a race condition exists:// if (!map.containsKey("key")) { map.put("key", value); } // WRONG in concurrent code| Feature | HashMap | LinkedHashMap | TreeMap | ConcurrentHashMap |
|---|---|---|---|---|
| Backing structure | Array + linked list / tree | HashMap + linked list | Red-black tree | Array + linked list / tree + CAS |
| Ordering | None | Insertion or access order | Sorted by key | None |
| Null keys/values | One null key, null values | One null key, null values | Not allowed | Not allowed |
get / put | O(1) avg | O(1) avg | O(log n) | O(1) avg |
| Thread-safe | No | No | No | Yes |
| Fail-fast iterator | Yes | Yes | Yes | Weakly consistent |
Iterator and ListIterator
Section titled “Iterator and ListIterator”Iterator
Section titled “Iterator”Iterator<E> provides a uniform way to traverse and remove elements from any collection. It Replaces the older Enumeration interface with a simpler API and the ability to safely remove Elements during iteration.
public interface Iterator<E> { boolean hasNext(); E next(); default void remove() { throw new UnsupportedOperationException("remove"); } default void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); }}List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));Iterator<String> it = names.iterator();while (it.hasNext()) { String name = it.next(); if (name.startsWith("B")) { it.remove(); // safe -- uses Iterator.remove(), not List.remove() }}// names is now [Alice, Charlie]ListIterator<E> extends Iterator<E> with bidirectional traversal and the ability to modify the List during iteration.
public interface ListIterator<E> extends Iterator<E> { boolean hasPrevious(); E previous(); int nextIndex(); int previousIndex(); void set(E e); // replaces the last element returned by next() or previous() void add(E e); // inserts element before the implicit cursor}List<String> list = new ArrayList<>(List.of("A", "B", "C", "D"));ListIterator<String> it = list.listIterator(2); // cursor starts at index 2 (before "C")
it.next(); // "C" -- cursor now at 3it.set("X"); // replaces "C" with "X" -- list is now [A, B, X, D]it.previous(); // "X" -- cursor back at 2it.add("Y"); // inserts "Y" before cursor -- list is now [A, B, Y, X, D]Fail-Fast vs Fail-Safe
Section titled “Fail-Fast vs Fail-Safe”Fail-Fast Iterators
Section titled “Fail-Fast Iterators”Most collection iterators in java.util are fail-fast: they detect concurrent structural Modification (additions, removals, or resizes) and throw ConcurrentModificationException Immediately. This is achieved through a modCount field on the collection. The iterator captures The expected modCount on creation and checks it on every call to next().
// Simplified ArrayList.Itrprivate class Itr implements Iterator<E> { int expectedModCount = modCount; // captured at iterator creation
public E next() { checkForComodification(); // throws CME if modCount changed // ... }
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }}Iterators from java.util.concurrent collections (e.g., ConcurrentHashMap.KeySetView.iterator()) Are weakly consistent rather than fail-fast. They do not throw ConcurrentModificationException. They may or may not reflect modifications made during iteration, But they will never throw an exception.
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();map.put("A", 1);map.put("B", 2);
Iterator<String> it = map.keySet().iterator();while (it.hasNext()) { String key = it.next(); map.put("C", 3); // modifying the map during iteration -- NO exception}The Collections.synchronizedCollection() wrapper returns a fail-fast iterator. If you need to Iterate over a synchronized collection and modify it during iteration, you must manually synchronize On the collection:
Collection<String> sync = Collections.synchronizedCollection(list);synchronized (sync) { // must synchronize manually during iteration for (String s : sync) { if (s.startsWith("A")) { sync.remove(s); } }}Comparable vs Comparator
Section titled “Comparable vs Comparator”Comparable (Natural Ordering)
Section titled “Comparable (Natural Ordering)”Comparable<T> defines the natural ordering of a class. The comparison logic is embedded in the Object itself.
public interface Comparable<T> { int compareTo(T o); // Returns negative if this < o, zero if this == o, positive if this > o}public record Person(String name, int age) implements Comparable<Person> { @Override public int compareTo(Person other) { int cmp = this.name.compareTo(other.name); return cmp != 0 ? cmp : Integer.compare(this.age, other.age); }}
List<Person> people = List.of( new Person("Charlie", 30), new Person("Alice", 25), new Person("Bob", 30));people.stream().sorted().forEach(System.out::println);// Alice(25), Bob(30), Charlie(30) -- sorted by name, then ageComparator (External Ordering)
Section titled “Comparator (External Ordering)”Comparator<T> defines an external ordering strategy that can be passed to sorting methods and Collection constructors. It separates the comparison logic from the objects being compared, allowing Multiple sort orders for the same type.
public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj);
// Comparator has many useful default methods (Java 8+) default Comparator<T> reversed(); default Comparator<T> thenComparing(Comparator<? super T> other); default <U> Comparator<T> thenComparing(Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator); static <T extends Comparable<? super T>> Comparator<T> naturalOrder(); static <T> Comparator<T> reverseOrder(); static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator); static <T> Comparator<T> nullsLast(Comparator<? super T> comparator);}// Chained comparators using default methodsComparator<Person> byName = Comparator.comparing(Person::name);Comparator<Person> byAge = Comparator.comparing(Person::age);Comparator<Person> byNameThenAge = byName.thenComparing(byAge);
Comparator<Person> byAgeDesc = Comparator.comparing(Person::age).reversed();
// Null-safe comparatorsComparator<String> nullSafe = Comparator.nullsFirst(Comparator.naturalOrder());List<String> mixed = List.of("Charlie", null, "Alice");mixed.stream().sorted(nullSafe).forEach(System.out::println);// null, Alice, Charlie -- null comes first- sgn(compare(x, y)) == -sgn(compare(y, x)) (antisymmetry)
- Transitive: if compare(x, y) > 0 and compare(y, z) > 0, then compare(x, z) > 0
- Consistent: repeated calls with the same arguments return the same result
- compare(x, y) == 0 implies sgn(compare(x, z)) == sgn(compare(y, z)) (consistency with equals is recommended but not required)
java.util.Collections provides static methods that operate on or return collections. It is the Companion utility class to the collections interfaces.
Sorting and Searching
Section titled “Sorting and Searching”List<Integer> numbers = new ArrayList<>(List.of(5, 3, 1, 4, 2));
Collections.sort(numbers); // natural ordering: [1, 2, 3, 4, 5]Collections.sort(numbers, Comparator.reverseOrder()); // [5, 4, 3, 2, 1]
// binarySearch requires the list to be SORTED; returns the index if found,// or (-(insertion point) - 1) if not foundint index = Collections.binarySearch(numbers, 3); // 2int notFound = Collections.binarySearch(numbers, 6); // negative value
// binarySearch with a Comparator must use the SAME Comparator used for sortingCollections.sort(numbers, Comparator.naturalOrder());Collections.binarySearch(numbers, 3, Comparator.naturalOrder());List<String> mutable = new ArrayList<>(List.of("A", "B", "C"));List<String> unmodifiable = Collections.unmodifiableList(mutable);
unmodifiable.add("D"); // UnsupportedOperationException
// WARNING: the unmodifiable wrapper is a VIEW -- changes to the backing list are visiblemutable.add("D");System.out.println(unmodifiable); // [A, B, C, D] -- the wrapper reflects the changeSynchronized Wrappers
Section titled “Synchronized Wrappers”List<String> syncList = Collections.synchronizedList(new ArrayList<>());Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// Each individual operation is thread-safesyncList.add("hello"); // synchronized internally
// BUT iteration requires manual synchronizationsynchronized (syncList) { for (String s : syncList) { System.out.println(s); }}// Reversing and rotatingCollections.reverse(list);Collections.rotate(list, 2); // rotate right by 2 positions
// Shuffling (uses ThreadLocalRandom internally)Collections.shuffle(list);
// Frequency and disjointint count = Collections.frequency(list, "target");boolean noOverlap = Collections.disjoint(list1, list2);
// Singleton and empty collectionsSet<String> single = Collections.singleton("only");List<String> empty = Collections.emptyList();Map<String, Integer> emptyMap = Collections.emptyMap();
// min/maxint max = Collections.max(numbers);int min = Collections.min(numbers, Comparator.reverseOrder());
// addAllCollections.addAll(list, "D", "E", "F");Arrays Utility Class
Section titled “Arrays Utility Class”java.util.Arrays provides static methods for manipulating arrays, including sorting, searching, Filling, and converting to/from collections.
int[] numbers = {5, 3, 1, 4, 2};Arrays.sort(numbers); // [1, 2, 3, 4, 5]int index = Arrays.binarySearch(numbers, 3); // 2
// Parallel sort -- uses ForkJoinPool for large arraysint[] large = new int[1_000_000];Arrays.fill(large, 0);Arrays.parallelSort(large);
// Array to ListString[] words = {"hello", "world"};List<String> list = Arrays.asList(words);// WARNING: Arrays.asList returns a fixed-size LIST BACKED BY THE ARRAY// list.add("new") throws UnsupportedOperationException// But: list.set(0, "hi") modifies the original array
// Safe copyList<String> copy = new ArrayList<>(Arrays.asList(words));
// fill and equalsArrays.fill(numbers, 0);boolean same = Arrays.equals(arr1, arr2); // deep element-by-element comparisonboolean deepSame = Arrays.deepEquals(matrix1, matrix2); // for nested arrays
// toString for debuggingSystem.out.println(Arrays.toString(numbers)); // [1, 2, 3, 4, 5]System.out.println(Arrays.deepToString(matrix)); // [[1, 2], [3, 4]]Immutable Collections (Java 9+)
Section titled “Immutable Collections (Java 9+)”Java 9 introduced factory methods List.of()``Set.of()And Map.of() that create compact, Unmodifiable collections. These are preferred over Collections.unmodifiableList(new ArrayList<>(...)) for creating immutable collections from a known Set of elements.
// List.of -- up to 10 elements have dedicated overloads for performanceList<String> empty = List.of();List<String> one = List.of("A");List<String> three = List.of("A", "B", "C");List<String> fromArray = List.of("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K");
// Set.of -- duplicates throw IllegalArgumentExceptionSet<String> unique = Set.of("A", "B", "C");// Set.of("A", "A"); // IllegalArgumentException: duplicate element: A
// Map.of -- duplicate keys throw IllegalArgumentExceptionMap<String, Integer> map = Map.of("A", 1, "B", 2, "C", 3);Map<String, Integer> fromEntries = Map.ofEntries( Map.entry("A", 1), Map.entry("B", 2), Map.entry("C", 3));Null Handling in Collections
Section titled “Null Handling in Collections”Null handling varies across collection implementations. Understanding these differences is critical To avoiding NullPointerException at unexpected times.
Summary Table
Section titled “Summary Table”| Collection | Null keys | Null values | Notes |
|---|---|---|---|
ArrayList | N/A | Allowed | Allows multiple null elements |
LinkedList | N/A | Allowed | Allows multiple null elements |
HashSet | N/A | One null allowed | Uses null.hashCode() = 0 |
LinkedHashSet | N/A | One null allowed | Same as HashSet |
TreeSet | N/A | Not allowed | compareTo(null) throws NPE |
EnumSet | N/A | Not allowed | Enums cannot be null |
HashMap | One null key allowed | Multiple null values allowed | Null key stored at bucket 0 |
LinkedHashMap | One null key allowed | Multiple null values allowed | Same as HashMap |
TreeMap | Not allowed (natural order) | Not allowed | Comparator must handle null; natural ordering throws NPE |
ConcurrentHashMap | Not allowed | Not allowed | Ambiguity between absent and null-mapped |
List.of() | N/A | Not allowed | Throws NPE |
Set.of() | N/A | Not allowed | Throws NPE |
Map.of() | Not allowed | Not allowed | Throws NPE |
How HashMap Handles the Null Key
Section titled “How HashMap Handles the Null Key”// HashMap places null keys at bucket index 0 (since hash(null) = 0)static final int hash(Object key) { return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
// The null key is stored in bucket[0] and compared with ==// (not .equals()) since null.equals() would throw NPEif (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))Sorted Collections and Null
Section titled “Sorted Collections and Null”// TreeSet with natural ordering -- null is not allowedSet<String> treeSet = new TreeSet<>();// treeSet.add(null); // NullPointerException: cannot compare null
// TreeSet with a null-safe Comparator -- null IS allowedSet<String> nullSafeTree = new TreeSet<>(Comparator.nullsFirst(Comparator.naturalOrder()));nullSafeTree.add(null);nullSafeTree.add("Alice");System.out.println(nullSafeTree); // [null, Alice]Intuition
Section titled “Intuition”Data structures toolkit: Collections are like a toolbox — Lists, Sets, Maps, and Queues each solve different problems. Choosing the right one affects performance and correctness.
Why it matters: Efficient data storage and retrieval is fundamental to programming. Understanding collections helps you design efficient algorithms.
The key insight: Choose your collection based on what you need — fast lookup (Map), ordered sequence (List), or unique elements (Set).
Summary of Design Principles
Section titled “Summary of Design Principles”The interface-implementation separation enables polymorphism. Code written against
List<E>works withArrayList``LinkedList``CopyOnWriteArrayListOr any future implementation without modification. Always declare variables with the interface type, not the implementation type.ArrayList is almost always the right default choice. Its O(1) random access, cache-friendly contiguous memory, and highly optimized
System.arraycopy()for shifts make it faster thanLinkedListfor virtually all workloads. UseLinkedListonly when you genuinely need frequent Deque operations or iterator-positioned insertions.HashMap’s secondary hash function is critical for performance. Without
h ^ (h >>> 16)Keys with similar lower bits (common forInteger``LongAnd sequential IDs) would cluster in the same bucket. The treeification threshold (8) ensures that even pathological collision scenarios degrade only to O(log n) rather than O(n).Fail-fast iterators are a debugging aid, not a concurrency mechanism. They detect bugs (modifying a collection during iteration) in single-threaded code. For actual concurrent access, use
java.util.concurrentcollections, which provide weakly consistent iterators and thread-safe operations.Prefer immutable collections when possible.
List.of()``Set.of()AndMap.of()create compact, unmodifiable collections with lower memory overhead than their mutable counterparts. Immutability eliminates entire classes of bugs (shared mutable state, concurrent modification) and enables safe sharing across threads without synchronization.
Common Pitfalls
Section titled “Common Pitfalls”Mixing up Big O, Big , and Big notation. Big O is an upper bound, not necessarily tight.
Writing pseudocode that is too language-specific rather than using standard algorithmic constructs.
Neglecting to normalise database designs, leading to data redundancy and update anomalies.
Forgetting edge cases in algorithm design (e.g., empty input, single element, already sorted data).
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Types and Variables — Generics in collections use type parameters to ensure type safety.
- Generics — Advanced generic concepts including bounded types and wildcards are used extensively in the collections API.
- Concurrency — Concurrent collections provide thread-safe operations for multi-threaded access.