Java Collections Framework Final
Java Collections Framework Final
Framework
The Java Collections Framework (JCF) is one of the most fundamental and widely used subsystems of
the Java Standard Library. Introduced in Java 1.2 (1998), it provides a unified architecture for storing,
retrieving, manipulating, and communicating aggregate data. Before its introduction, Java developers
relied on ad-hoc data structures — arrays, Vectors, and Hashtables — each with inconsistent APIs,
limited functionality, and no common abstraction.
• Lack of a common interface. You could not write a single method that accepted either a Vector
or an array.
• No standard algorithms. Every team wrote their own sort, search, and min-max utilities, leading
to code duplication.
• Poor performance. Vector's blanket synchronization was unnecessary overhead for
single-threaded use-cases.
• Fixed-size arrays. Primitive arrays cannot grow once allocated, forcing programmers to manage
resizing manually.
• Type safety. Without generics (pre-Java 5), collections stored Object, requiring casts and risking
ClassCastException at runtime.
■ Interview Insight: The JCF was designed by Joshua Bloch (author of Effective Java) and modelled on
the C++ Standard Template Library (STL), but with a cleaner, interface-driven design.
• Fixed size: Once created with new int[100], the array cannot shrink or grow. You must allocate a
new array and copy data.
• No built-in methods: Arrays have no add(), remove(), or contains() methods.
• Homogeneous type only: An array can hold one type. You cannot mix strategies without object
arrays and casting.
• No iterator protocol: You must manually manage index-based traversal.
• Primitive and object gap: You cannot store primitives in a generic way without boxing overhead.
if (size == [Link]) {
internalArray[size++] = element;
Iterable
■■■ Collection
■■■ List (ArrayList, LinkedList, Vector, Stack)
■■■ Set (HashSet, LinkedHashSet, TreeSet)
■■■ Queue (PriorityQueue, ArrayDeque, LinkedList)
■■■ Deque (ArrayDeque, LinkedList)
The power of the JCF comes from its carefully designed interface hierarchy. Interfaces define
contracts — what a collection promises to do — without specifying how. This separation enables
polymorphism, testability, and the ability to swap implementations.
2.1 Iterable<E>
[Link] is the root of all collections that can be traversed. It declares a single method:
Iterator<T> iterator(). Any class implementing Iterable can be used in a Java enhanced for-loop.
Internally, the compiler transforms:
Iterator it = [Link]();
while ([Link]()) {
String s = [Link]();
// body
This transformation means every Collection subtype benefits from the enhanced for-loop
automatically, since Collection extends Iterable.
2.2 Collection<E>
[Link] extends Iterable and is the foundational interface for all single-element containers. It
declares the core CRUD contract:
Design Principle: Collection intentionally does NOT specify ordering or uniqueness — those are
concerns of sub-interfaces List and Set respectively.
2.3 List<E>
[Link] extends Collection and adds the concept of positional access. Elements are stored in
insertion order and can be accessed by index. Key additions include:
E get(int index);
E remove(int index);
ListIterator listIterator();
Lists allow duplicate elements and maintain insertion order. Implementations differ in their
performance trade-offs: ArrayList for random access, LinkedList for frequent insertion/deletion.
2.4 Set<E>
[Link] extends Collection and adds the uniqueness constraint — no two elements may be equal
according to equals(). Set adds no new methods beyond Collection; its contract is enforced
behaviourally. Sub-interfaces include SortedSet and NavigableSet for ordered sets.
// Queue operations:
// Deque adds:
offerFirst(e) / offerLast(e)
pollFirst() / pollLast()
peekFirst() / peekLast()
V get(Object key);
V remove(Object key);
Set keySet();
Collection values();
Set> entrySet();
CHAPT
ER 3 List Implementations
3.1 ArrayList
ArrayList is the most commonly used List implementation. Internally, it maintains a plain Java Object
array (Object[] elementData). When you create new ArrayList() with no arguments, the internal array
starts empty and is lazily initialized to capacity 10 on first addition.
newCapacity = minCapacity;
The growth factor of 1.5× is a deliberate trade-off between memory waste (a factor of 2 wastes up to
50%) and reallocation frequency (a factor of 1.1 means frequent copies). At 1.5×, amortised insertion
is O(1).
Time Complexity
Operation Best Case Average Case Worst Case
if ([Link]()) [Link](s);
[Link](String::isEmpty);
Iterator it = [Link]();
while ([Link]()) {
if ([Link]().isEmpty()) [Link]();
3.2 LinkedList
LinkedList implements both List and Deque. Internally it uses a doubly-linked list of Node objects,
each holding a reference to the previous node, the next node, and the data element.
E item;
Node next;
Node prev;
[Link] = element;
[Link] = next;
[Link] = prev;
Because every node object costs ~48 bytes on a 64-bit JVM (object header + 3 references), LinkedList
carries significant memory overhead compared to ArrayList. Random access is O(n) because the list
must be traversed from head or tail to reach position i.
Stack extends Vector and adds push, pop, and peek semantics. It is considered legacy. The preferred
alternative is Deque with ArrayDeque as the implementation.
■ Best Practice: Never use Stack or Vector in new code. Use ArrayDeque for stack semantics and
ArrayList for resizable lists.
CHAPT
ER 4 Set Implementations
4.2 HashSet
HashSet is backed by a HashMap internally. When you add an element to a HashSet, it calls
[Link](element, PRESENT) where PRESENT is a static dummy Object. Uniqueness is enforced
because HashMap keys are unique.
return [Link](o);
Insertion order is not preserved. Elements are distributed across hash buckets, so iteration order
depends on hash values and appears random.
4.3 LinkedHashSet
LinkedHashSet extends HashSet and is backed by a LinkedHashMap. It adds a doubly-linked list
threading through all entries in insertion order. The result is a set that preserves insertion order while
still offering O(1) add/remove/contains. The memory overhead is approximately one extra pointer pair
per entry.
4.4 TreeSet
TreeSet is backed by a TreeMap, which is itself a Red-Black Tree. Elements are stored in sorted
order (natural ordering via Comparable, or a supplied Comparator). All basic operations are O(log n).
final K key;
V value;
Step 2: Compute bucket index: i = (n - 1) & hash where n = [Link]. Since n is always a power of
2, this bitwise AND is equivalent to modulo but faster.
Step 3: If table[i] is null, create a new Node and store it. Done.
Step 4: If table[i] is not null, traverse the chain at bucket i looking for a node with equal key (using ==
or equals()). If found, replace the value and return the old value.
Step 5: If not found, append a new Node at the end of the chain (or the Red-Black Tree if treeified).
Step 6: After insertion, check if ++size > threshold. If yes, call resize() to double the table and rehash
all entries.
int h;
// For [Link]=16: (15) & hash => uses only lowest 4 bits
During rehashing, the table doubles in size (always a power of 2). Each entry is re-distributed:
because the new index is (newCapacity - 1) & hash and newCapacity = 2 × oldCapacity, each entry
either stays at the same index or moves to oldIndex + oldCapacity. This elegant property means Java
8's rehashing only checks one bit to decide the new position.
• Lost updates: Both threads read size, both increment it, one write is lost.
• Broken chains: During resize, two threads can create circular linked lists (a famous Java bug
pre-8, causing infinite loops).
• Partial visibility: One thread may see partially constructed Node objects due to instruction
reordering without memory barriers.
• Resize corruption: If resize happens mid-iteration, the iterator's internal state becomes
inconsistent.
Java 5–7: Segment-based locking. The table was divided into 16 segments, each with its own
ReentrantLock. Only the segment being modified was locked, allowing up to 16 concurrent writers.
Java 8+: Lock-striping with CAS and synchronized blocks. Segments were eliminated. The table
array itself is the locking structure. Reads are lock-free (using volatile reads). Writes use
Compare-And-Swap (CAS) for inserting into empty buckets, and synchronized on the bucket head
node for non-empty buckets. This allows up to N concurrent writers where N = number of non-colliding
buckets.
if (f == null) {
} else {
synchronized (f) {
■ Note: ConcurrentHashMap does NOT allow null keys or null values (unlike HashMap). This is
intentional: in a concurrent context, a null return from get() would be ambiguous — does it mean key not
found, or value is null?
5.4 LinkedHashMap
LinkedHashMap extends HashMap and maintains a doubly-linked list through all entries, recording
insertion order. It overrides HashMap's hook methods (afterNodeInsertion, afterNodeAccess,
afterNodeRemoval) to update this linked list. The accessOrder constructor flag switches from
insertion-order to access-order, making LinkedHashMap the foundation for implementing an LRU
(Least Recently Used) cache.
[Link] = capacity;
@Override
5.5 TreeMap
TreeMap implements NavigableMap using a Red-Black Tree. All keys are maintained in sorted order.
The Red-Black Tree guarantees O(log n) for put, get, and remove. It provides the richest query API of
any Map: firstKey(), lastKey(), floorKey(), ceilingKey(), headMap(), tailMap(), and subMap().
These invariants guarantee that the longest path (alternating red-black) is at most twice the shortest
(all-black), keeping the tree height bounded at 2·log(n+1). Insertions and deletions maintain these
invariants through rotations (left-rotate and right-rotate) and recolouring.
K key;
V value;
Entry left;
Entry right;
Entry parent;
5.7 Hashtable
Hashtable is the legacy precursor to HashMap, introduced in Java 1.0. Like Vector, every method is
synchronized. It does not allow null keys or values. In modern Java, always prefer
Backing structure Hash Array Hash+LinkedList Red-Black Tree Hash Array Hash Array+CAS
public E next() {
if (modCount != expectedModCount)
6.3 ListIterator
ListIterator extends Iterator and is available only for List implementations. It adds bidirectional traversal
(hasPrevious(), previous()), the ability to add elements during iteration (add()), and index queries
(nextIndex(), previousIndex()).
6.4 Spliterator
Spliterator (Java 8+) is designed for parallel traversal. The name means 'splittable iterator'. It can
partition itself into two, allowing the two halves to be processed in parallel. It reports characteristics
about the collection (SIZED, ORDERED, DISTINCT, SORTED, IMMUTABLE, CONCURRENT,
NONNULL, SUBSIZED) enabling the Streams API to optimize operations.
Sorting — Comparable,
CHAPT
ER 7 Comparator, and
TimSort
@Override
return [Link]([Link]);
// Usage
[Link](comp);
// Or inline lambda
• Stable sort: Equal elements maintain their relative order — critical for multi-key sorting.
• Adaptive: Runs as fast as O(n) on already-sorted input, O(n log n) worst case.
• Efficient for small arrays: Uses binary insertion sort for runs smaller than ~64 elements.
• Memory: Uses O(n) auxiliary memory for the merge phase.
Note: [Link]() for primitive arrays uses Dual-Pivot Quicksort (not TimSort) since stability is irrelevant
for primitives and quicksort has better cache performance.
[Link]([Link](5, 2, 8, 1, 9));
while (![Link]()) {
[Link]([Link](5, 2, 8, 1, 9));
[Link]([Link]()); // 9
PriorityQueue> topK =
Heap operations: insert appends at the end and sifts up O(log n); poll removes the root, moves the
last element to root, and sifts down O(log n); peek returns root without removal O(1).
[Link]("a"); // addFirst
[Link]("b");
[Link]("a"); // addLast
[Link]("b");
9.1 [Link]
[Link] is a utility class containing exclusively static methods that operate on or return
collections. It is analogous to [Link] but for collection types.
Key Methods
List list = new ArrayList<>([Link](3, 1, 4, 1, 5, 9, 2, 6));
[Link](list, [Link]()); //
descending
[Link](list); // O(n)
[Link]([Link](list)); // O(n)
[Link]([Link](list)); // O(n)
■ Key difference: [Link]() returns a view — the underlying list can still be modified
through the original reference and the view will reflect changes. [Link]() returns a truly immutable list with
no backing mutable structure.
9.2 [Link]
int[] arr = {3, 1, 4, 1, 5, 9};
[Link]([Link](arr)); // [0, 0, 0, 0, 0, 0]
[Link]([Link](matrix)); // 2D array
Performance Analysis
CHAPT
ER 10 and Choosing the Right
Collection
* Amortised O(1) — occasional O(n) for resize. ** O(1) at head/tail; O(n) if traversal needed to find
element.
→ Use HashMap (no order needed), LinkedHashMap (insertion order), TreeMap (sorted keys).
// 4. Iterate entrySet() not keySet() when you need both key and
value
use([Link](), [Link]());
[Link](word, count);
Top 50 Interview
CHAPT
ER 11 Questions — Beginner
to Expert
— BEGINNER —
A: List allows duplicate elements and maintains insertion order. Set enforces uniqueness
(no duplicates). List provides positional access by index; Set does not. HashSet offers
O(1) lookup; [Link]() is O(n).
A: Comparable defines the natural ordering of a class via compareTo() — the class itself
knows how to compare. Comparator defines an external ordering via compare() —
useful for multiple sort orders or sorting third-party classes you cannot modify.
Comparator is a functional interface in Java 8+.
A: HashMap provides O(1) get/put with no ordering. TreeMap provides O(log n) get/put
and maintains keys in sorted order (Red-Black Tree). TreeMap also implements
NavigableMap with range-query methods. Use HashMap for performance; TreeMap
when you need sorted iteration or range queries.
— INTERMEDIATE —
A: HashMap uses a Node[] table (bucket array). put() computes hash = hashCode() ^
(hashCode() >>> 16), then index = (n-1) & hash. It chains collisions via linked lists
(pre-Java 8) or Red-Black Trees when a bucket exceeds 8 entries (Java 8+). Resizes
when size > capacity * loadFactor, doubling the table.
Q What happens when two keys have the same hashCode in a HashMap?
12
:
A: This is a collision. The new entry is appended to the linked list (or inserted into the
Red-Black Tree) at the bucket index. Lookup traverses the chain comparing keys with
equals(). If the chain length reaches 8 and the table size >= 64, the chain is converted
to a Red-Black Tree for O(log n) worst-case lookup.
A: The load factor (default 0.75) controls when resizing occurs. threshold = capacity *
loadFactor. When size exceeds threshold, the table doubles and all entries are
rehashed. A lower load factor reduces collisions but wastes memory. 0.75 is the
empirically optimal trade-off between time and space.
A: CME is thrown by fail-fast iterators when the collection's modCount changes during
iteration. Avoid by: (1) using [Link]() instead of [Link](), (2) using
removeIf(predicate) (Java 8+), (3) using a concurrent collection like
CopyOnWriteArrayList, (4) iterating over a copy.
A: LinkedHashMap maintains insertion order (or access order) via a doubly-linked list
through entries — O(1) operations. TreeMap maintains sorted key order via a
Red-Black Tree — O(log n) operations but with range-query support. Use
LinkedHashMap for LRU caches; TreeMap for sorted maps with range queries.
A: In Java 8+, CHM uses volatile reads for lock-free gets. Puts on empty buckets use
CAS (Compare-And-Swap) atomics. Puts on occupied buckets synchronize only on
the bucket's head node, allowing concurrent writes to different buckets. No lock is held
for reads, and size() uses LongAdder for accurate counting.
A: Both remove the head of the queue. remove() throws NoSuchElementException if the
queue is empty. poll() returns null if the queue is empty. Similarly, peek() returns null
on empty queue; element() throws an exception. Always prefer the null-returning
versions (poll, peek, offer) in production code.
A: Both are backed by dynamic arrays. Vector synchronizes every method, making it
thread-safe but ~3× slower than ArrayList in single-threaded code. ArrayList is not
synchronized. Both grow when full: Vector doubles capacity; ArrayList grows by 50%.
Vector is legacy — use ArrayList or CopyOnWriteArrayList instead.
— ADVANCED —
A: When a bucket's chain exceeds TREEIFY_THRESHOLD (8) entries AND the table
size >= MIN_TREEIFY_CAPACITY (64), the linked list is converted to a Red-Black
Tree. This improves worst-case lookup within a bucket from O(n) to O(log n). When
entries drop to UNTREEIFY_THRESHOLD (6), the tree converts back to a list.
A: RBT is a BST where: every node is red or black; root is black; null leaves are black;
red nodes have only black children; every root-to-leaf path has the same black-height.
These invariants ensure height ≤ 2·log(n+1), guaranteeing O(log n) for all operations.
Insertions/deletions maintain invariants via rotations and recolouring.
A: Spliterator (Java 8) is a parallel-capable iterator. trySplit() splits it into two halves for
parallel processing. It reports characteristics (SIZED, SORTED, ORDERED,
DISTINCT, etc.) that the Streams framework uses to optimize operations — e.g., a
SIZED+SUBSIZED spliterator enables precise work partitioning without needing to
count elements.
A: ArrayDeque maintains a fixed-size array with head and tail integer indices. addFirst
decrements head (wrapping around with modulo); addLast increments tail. When head
== tail, the array is full and is doubled. Because elements are never shifted, both-end
O(1) operations are true, not amortised due to shifting.
A: Though individual additions that trigger resize are O(n), the cost is spread across all
additions. With a growth factor of 1.5×, after n total insertions the total copy work is
n/1.5 + n/1.5^2 + ... ≈ 3n. So the total work for n insertions is O(n), making the
amortised cost per insertion O(1).
A: [Link]() (and [Link]()) uses TimSort — a stable, adaptive O(n log n) hybrid.
[Link](Object[]) also uses TimSort. [Link](primitive[]) uses Dual-Pivot
Quicksort — unstable but faster in practice for primitives due to better cache
performance and no need for stability.
A: EnumSet is a specialized Set for enum types backed by a bit vector (one long per 64
enum constants). All operations are O(1) with no hashing. It is the most
memory-efficient and fastest Set possible for enum types. HashSet uses a HashMap
internally with object overhead per entry. Always use EnumSet when the element type
is an enum.
— EXPERT —
Q Explain the HashMap infinite loop bug in Java pre-8 under concurrent
31 access.
:
A: In Java pre-8, during resize, two threads could each begin rehashing the same bucket.
The transfer() method reversed the linked list order. With two threads, a circular
reference could form (A->B->A), causing an infinite loop on subsequent get(). Java 8
fixed this by maintaining order during transfer and using Red-Black Trees.
Q How does HashMap handle hash collisions for keys with hashCode() always
32 returning the same value?
:
A: All keys hash to the same bucket. Pre-Java 8: O(n) linked list, making all operations
O(n). Java 8+: once the chain reaches 8 entries and table size >= 64, the bucket
converts to a Red-Black Tree, improving to O(log n). This is why HashMap
performance degrades gracefully even under pathological inputs.
A: The spreading function hash = h ^ (h >>> 16) XORs the upper 16 bits into the lower 16
bits. When the table is small (e.g., capacity 16), only the lowest 4 bits of the hash
determine the bucket. Without spreading, keys whose hashCode() differs only in high
bits would all land in the same bucket. Spreading distributes entropy from high bits into
the low bits used for indexing.
A: size() uses an internal LongAdder (a striped counter). Each update increments a cell in
the stripe array determined by thread affinity, reducing contention. size() sums all cells.
This is more scalable than an AtomicLong (single CAS point) under high concurrency.
The result is an estimate — exact count is only guaranteed at quiescence.
Q How would you detect and fix memory leaks in collection-heavy code?
35
:
A: Common causes: static Maps/Lists that grow unboundedly; removing objects from
collections but forgetting to remove them from related Maps; using
non-equals-hashCode-correct objects as Map keys. Fix with: bounded collections
(LinkedHashMap + removeEldestEntry), WeakHashMap for cache entries where GC
should reclaim them, heap profiling tools (VisualVM, Eclipse MAT) to identify retained
collections.
A: CopyOnWriteArrayList creates a new array copy on every write — reads are lock-free
and fast; writes are expensive. Best for read-heavy, write-rare scenarios.
synchronizedList wraps every method in synchronized — reads and writes are both
locked, creating a bottleneck. COWAL iterators never throw CME; synchronizedList
iterators require external synchronization.
A: Search, insert, delete: all O(log n) guaranteed due to the height bound of 2·log(n+1).
Rotation and recolouring during insert/delete: O(log n) in the worst case but O(1)
amortised rotations per operation. In-order traversal: O(n). Space: O(n) for n entries,
with constant overhead per node (key, value, color, parent, left, right pointers).
A: TimSort scans the array for natural 'runs' (ascending or descending sequences).
Descending runs are reversed in O(k). Short runs are extended to minRun length
(32-64 elements) using binary insertion sort. Runs are pushed onto a stack and
merged when they satisfy invariants (ensuring merge costs are O(n log n) total). This
makes TimSort near-linear for partially sorted data.
A: resize() creates a new array of double the capacity. Every entry in the old table is
re-distributed: for each entry, if (hash & oldCapacity) == 0, it stays at oldIndex;
otherwise it moves to oldIndex + oldCapacity. This works because newCapacity = 2 ×
oldCapacity means the new index bit is exactly the bit that was zero before. Java 8's
resize preserves list order (no reversal), fixing the pre-8 circular reference bug.
Q How would you design a collection that is both sorted and allows O(1)
42 lookup?
:
A: No standard data structure achieves both. Trade-offs: TreeMap is sorted with O(log n)
lookup. HashMap is O(1) lookup but unordered. A hybrid approach: maintain both a
HashMap and a TreeSet in sync. Writes update both structures O(log n); lookups use
HashMap O(1); sorted iteration uses TreeSet. Alternatively, use a skip list
(ConcurrentSkipListMap in Java) which provides O(log n) sorted operations with good
concurrent performance.
A: WeakHashMap holds weak references to its keys. When a key has no other strong
references, it becomes eligible for garbage collection. The GC nullifies the weak
reference and a ReferenceQueue is used to remove stale entries during subsequent
operations. This makes WeakHashMap ideal for caches where entries should be
automatically evicted when keys are no longer used elsewhere.
A: PriorityQueue allows duplicates. Elements with equal priority are stored according to
their heap position — there is no guaranteed ordering among equal-priority elements.
poll() returns one of the minimum-priority elements but which one among equals is
undefined. If strict FIFO among equals is required, use a Comparator that breaks ties
by insertion order (e.g., using an AtomicLong sequence number).
A: IdentityHashMap uses reference equality (==) instead of equals() for key comparison,
and [Link]() for hashing. Use cases: serialization frameworks
(tracking object identity to avoid infinite loops in circular graphs), proxy caches (where
different proxy instances of the same logical object should be separate keys), and
graph algorithms.
A: unmodifiableList() returns a view — the returned list reflects changes made to the
original mutable list. It throws UnsupportedOperationException on write attempts, but
reads show current state of the original. [Link]() creates a defensive copy — the
returned list is independent and truly immutable. Modifying the original after copyOf()
has no effect on the copy.
Q What is the contract of [Link] and how is it used for efficient iteration?
48
:
Best Practices
• Always program to the interface: List<String> list = new ArrayList<>() not ArrayList<String>.
• Pre-size collections: new ArrayList<>(expectedSize), new HashMap<>(size * 4/3 + 1).
• Use computeIfAbsent(), getOrDefault(), putIfAbsent() to simplify Map code.
• Iterate entrySet() when you need both key and value to avoid double lookups.
• Prefer ArrayDeque over Stack and LinkedList for stack/queue semantics.
• Use EnumSet and EnumMap for enum-keyed collections — they are the fastest and most
memory-efficient.
• Prefer [Link](), [Link](), [Link]() for immutable collections.
• Use ConcurrentHashMap, not Hashtable or synchronizedMap, for concurrent maps.
• Always override hashCode() when overriding equals().
• Never use mutable objects as HashMap/HashSet keys — mutating a key after insertion orphans
the entry.
Common Mistakes
• Modifying a collection inside a for-each loop — causes ConcurrentModificationException.
• Using == instead of equals() to compare collection elements.
• Forgetting that [Link]() returns a fixed-size list — add() throws
UnsupportedOperationException.
• Using int subtraction in compareTo() — integer overflow produces wrong ordering.
• Using HashMap in multithreaded code without synchronization.
• Treating ConcurrentHashMap's size() as exact under concurrent modification.
• Storing null in collections that do not support it (TreeSet, TreeMap, ConcurrentHashMap).
• Not initializing HashMap with expected capacity — causes unnecessary resizing and GC
pressure.
• Iterating keySet() and calling get() for each key — iterate entrySet() instead.
• Using Stack — it extends Vector and is synchronised. Use ArrayDeque.
HashMap internals Bucket array + chaining. Hash spreading. Load factor 0.75.
Java 8 treeification at 8 entries.
Choosing collections Access pattern, ordering, thread safety, null support, memory
budget.
■ Final Tip: The Java Collections Framework is not just an API — it is a masterclass in object-oriented
design. Every architectural decision (interfaces vs classes, fail-fast iterators, load factors, treeification)
has a deep reason behind it. Understanding the WHY makes you a better engineer and a stronger
interview candidate.