Java Collections Framework: Lists, Maps & Trees
The Java Collections Framework (JCF) is the core architecture for storing and manipulating groups of objects. Choosing the right data structure—such as an ArrayList, LinkedList, HashMap, or ConcurrentHashMap—directly impacts memory consumption, search time complexity, and thread safety in high-throughput applications.
In this guide, we dive into data structure internals, trace HashMap bucket indexing and red-black tree treeification, evaluate ConcurrentHashMap thread safety, and compare fail-fast vs. fail-safe iterators.
1. Collections Framework Architecture
The framework separates data structures into two main root interface hierarchies: java.util.Collection (for sequences of elements) and java.util.Map (for key-value associations).
flowchart TD
Collection["java.util.Collection"]
Collection --> List["List\n(Ordered, Indexed, Duplicates)"]
Collection --> Set["Set\n(Unique Elements)"]
Collection --> Queue["Queue / Deque\n(FIFO / LIFO Ordering)"]
List --> ArrayList["ArrayList"]
List --> LinkedList["LinkedList"]
Set --> HashSet["HashSet"]
Set --> TreeSet["TreeSet"]
Map["java.util.Map\n(Key-Value Pairs)"] --> HashMap["HashMap"]
Map --> TreeMap["TreeMap"]
Map --> ConcurrentHashMap["ConcurrentHashMap"]
2. List Implementations: ArrayList vs. LinkedList
ArrayList Internals
ArrayList is backed by a contiguous dynamic Object array (Object[] elementData). Accessing an element by index runs in O(1) time complexity.
- Capacity Growth Factor: When the array exceeds its capacity,
ArrayListgrows by 50% (new capacity =oldCapacity + (oldCapacity >> 1)). - Memory Copy Cost: Growing the array requires allocating a new, larger contiguous memory array and calling
System.arraycopy()to move elements, which takes O(N) time during resize operations.
LinkedList Internals
LinkedList is implemented as a doubly-linked list where each node contains pointers to prev and next nodes alongside the data element. Index access takes O(N) time because it must traverse pointers from the head or tail node.
Memory Footprint: Each LinkedList node incurs significant object header and pointer overhead (typically 24 bytes of overhead per element), making ArrayList far more cache-friendly and memory efficient for most workloads.
3. HashMap Deep Dive: Hashing & Treeification
HashMap stores key-value pairs in an array of bucket nodes (Node<K,V>[] table). The default initial capacity is 16, and the default load factor is 0.75.
flowchart LR
subgraph BucketTable["HashMap Bucket Array (Capacity = 16)"]
B0["Bucket 0: null"]
B1["Bucket 1: Node (Key1) -> Node (Key2) -> Node (Key3)"]
B2["Bucket 2: Red-Black Tree Root Node (Key8)"]
B3["Bucket 3: null"]
end
Bitwise Bucket Index Calculation
Rather than using expensive modulo operations (hash % capacity), HashMap enforces that array capacity is always a power of 2. It calculates the bucket index using bitwise AND:
int index = (n - 1) & hash; // Fast bitwise operation equivalent to hash % n
Collision Resolution & Red-Black Treeification
When multiple keys hash to the same bucket index (collision), HashMap chains them in a singly-linked list.
- Treeification Threshold: When a single bucket list length reaches 8 and the total table capacity is at least 64,
HashMapconverts the linked list into a self-balancing Red-Black Tree. - Performance Impact: Treeification reduces worst-case lookup time complexity from O(N) to O(log N), protecting applications against HashDoS (Hash Denial of Service) attacks.
- Untreeification: If deletions reduce tree nodes in a bucket to 6 or fewer, the bucket converts back into a singly-linked list.
4. ConcurrentHashMap Mechanics
Standard HashMap is not thread-safe. Concurrent modifications can cause corrupted entries or infinite loops during resizing. Hashtable and Collections.synchronizedMap() provide thread safety, but they lock the entire map on every operation, creating severe contention bottlenecks.
flowchart TD
subgraph CHM["ConcurrentHashMap Bucket Lock Strategy"]
Op["Insert / Update Operation"] --> CheckEmpty{"Is Bucket Empty?"}
CheckEmpty -- Yes --> CAS["Lock-Free CAS Operation (Unsafe.compareAndSet)"]
CheckEmpty -- No --> LockHead["Synchronize ONLY Bucket Head Node (synchronized(head))"]
end
ConcurrentHashMap in Java 8+ replaces segment locks with fine-grained bucket-level locking:
- CAS (Compare-And-Swap): Inserting into an empty bucket uses lock-free CAS operations (
Unsafe.compareAndSetObject). - Bucket Synchronized Lock: Inserting into an occupied bucket locks only the head node of that specific bucket using
synchronized(headNode), allowing concurrent writes across different buckets without contention.
5. Fail-Fast vs. Fail-Safe Iterators
| Feature | Fail-Fast Iterators | Fail-Safe Iterators |
|---|---|---|
| Examples | ArrayList, HashMap, HashSet |
CopyOnWriteArrayList, ConcurrentHashMap |
| Mechanism | Checks internal modCount field during iteration. Throws exception if modified concurrently. |
Iterates over a clone array or a weakly-consistent view of the underlying data structure. |
| Exception | Throws ConcurrentModificationException |
Never throws ConcurrentModificationException |
| Memory Overhead | Low (Direct traversal) | Higher (Cloning array or snapshot views) |
- To review multithreading, object monitors, and thread synchronization, read Java Multithreading & Concurrency Mechanics.
- To examine static blocks, wrapper caching, and type conversion, read Java Language Fundamentals.
- To explore off-heap memory, garbage collection, and object headers, read JVM Memory Management: Heap, Stack & Metaspace.