Java Collections Framework Interview Questions & Answers
These questions cover essential and tricky concepts of the Java Collections Framework with senior-level depth.
1. Explain the Collection Hierarchy
The Java Collections Framework is organized into a well-defined hierarchy of interfaces and implementations:
๐๏ธ Java Collections Framework โ Hierarchy Overview
๐ก Click any node to see its details, complexity, and notes.
Key interfaces:
- List: Ordered collection allowing duplicates and index-based access. Maintains insertion order.
- Set: No duplicates.
HashSetis unordered,LinkedHashSetpreserves insertion order,TreeSetsorts elements. - Queue/Deque: FIFO processing (
Queue) or double-ended (Deque). - Map: Key-value pairs. Not part of the
Collectioninterface hierarchy.
Why Map doesn't extend Collection
Collection.add(E e) accepts a single element. Map.put(K key, V value) requires two parameters (a pair). The fundamental data model is different โ a Map is a collection of entries (key-value pairs), not individual elements. However, you can get Collection views: map.keySet(), map.values(), and map.entrySet().
2. What is the difference between Fail-Fast and Fail-Safe Iterators?
| Feature | Fail-Fast | Fail-Safe |
|---|---|---|
| Behavior | Throws ConcurrentModificationException on structural modification | Never throws exception |
| Works on | Original collection | Clone/snapshot of the collection |
| Collections | ArrayList, HashMap, HashSet | ConcurrentHashMap, CopyOnWriteArrayList |
| Memory | No extra memory | Extra memory for the copy/snapshot |
| Reflects changes | N/A (throws exception) | May not reflect modifications made after iterator creation |
How Fail-Fast detection works internally
// Inside ArrayList โ the modCount mechanism
transient int modCount = 0; // Incremented on add(), remove(), clear()
// Inside ArrayList$Itr (the iterator)
int expectedModCount = modCount; // Captured at iterator creation
public E next() {
if (modCount != expectedModCount) // Check on every next() call
throw new ConcurrentModificationException();
// ... return element
}
Important: Fail-fast is a best-effort mechanism, not a guarantee. The JavaDoc explicitly states it should not be relied upon for correctness โ only for bug detection.
3. What is a BlockingQueue?
BlockingQueue (in java.util.concurrent) is a thread-safe queue that supports blocking operations โ the thread waits instead of failing when the operation cannot be completed immediately.
Blocking behavior
| Operation | If Queue is Empty | If Queue is Full |
|---|---|---|
put(e) | N/A | Blocks until space available |
take() | Blocks until element available | N/A |
offer(e, timeout) | N/A | Waits up to timeout, returns false |
poll(timeout) | Waits up to timeout, returns null | N/A |
add(e) | N/A | Throws IllegalStateException |
remove() | Throws NoSuchElementException | N/A |
Common Implementations
| Implementation | Capacity | Ordering | Use Case |
|---|---|---|---|
ArrayBlockingQueue | Bounded (fixed) | FIFO | Producer-consumer with backpressure |
LinkedBlockingQueue | Optionally bounded | FIFO | Thread pool work queues (Executors.newFixedThreadPool) |
PriorityBlockingQueue | Unbounded | Priority-based | Task scheduling by priority |
SynchronousQueue | Zero capacity | Direct handoff | Executors.newCachedThreadPool |
DelayQueue | Unbounded | By delay expiration | Scheduled task execution |
Producer-Consumer Pattern
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);
// Producer thread โ blocks if queue is full (backpressure!)
queue.put(new Task("process-order"));
// Consumer thread โ blocks if queue is empty (waits for work)
Task task = queue.take();
task.execute();
4. Synchronized vs. Concurrent Collections
| Feature | Synchronized | Concurrent |
|---|---|---|
| Examples | Hashtable, Vector, Collections.synchronizedMap() | ConcurrentHashMap, CopyOnWriteArrayList |
| Lock granularity | Entire collection (coarse-grained) | Per-bucket/segment (fine-grained) |
| Read blocking | Yes โ readers block other readers | No โ reads are lock-free |
| Iterator | Fail-fast | Weakly consistent |
| Compound operations | Not atomic (check-then-act is racy) | Atomic (computeIfAbsent, putIfAbsent) |
| Scalability | Poor (serializes all access) | Excellent (high concurrency) |
The compound operation problem
// BROKEN with synchronizedMap โ NOT atomic!
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
if (!syncMap.containsKey("counter")) { // Thread A: false
// Thread B: also sees false, also enters this block
syncMap.put("counter", 1); // Both threads put โ lost update!
}
// CORRECT with ConcurrentHashMap โ atomic compound operation
ConcurrentHashMap<String, Integer> concMap = new ConcurrentHashMap<>();
concMap.putIfAbsent("counter", 1); // Atomic โ no race condition
concMap.computeIfAbsent("counter", k -> 1); // Also atomic
5. How does HashMap work internally?
HashMap works on the principle of hashing with a hybrid data structure:
The put(K, V) operation step by step:
- Hash calculation:
hash = key.hashCode() ^ (key.hashCode() >>> 16)โ the high bits are mixed into the low bits to improve distribution for small tables. - Bucket index:
index = hash & (table.length - 1)โ bitwise AND (faster than modulo for power-of-2 sizes). - Empty bucket: Store a new
Node<K,V>directly. - Collision (same bucket): Walk the linked list/tree. If an existing node has the same key (
equals()returns true), replace the value. Otherwise, append a new node. - Treeification check: If the linked list at this bucket exceeds 8 nodes (TREEIFY_THRESHOLD), convert to a Red-Black Tree for O(log n) lookup.
- Resize check: If total size exceeds
capacity ร loadFactor, resize (double the table).
The get(K) operation:
- Calculate hash and bucket index (same as put).
- Check the first node in the bucket โ if it matches, return immediately (O(1)).
- If not, and the bucket contains a tree, do a tree lookup (O(log n)).
- If it's a linked list, traverse linearly (O(n) worst case for that bucket).
Memory Layout
HashMap
โโโ Node[] table (length = capacity, always power of 2)
โ โโโ [0] โ null
โ โโโ [1] โ Node(hash=1, "Alice"โ"Engineer") โ Node(hash=1, "Bob"โ"Manager")
โ โโโ [2] โ null
โ โโโ [3] โ TreeBin โ TreeNode... (if โฅ 8 collisions)
โ โโโ ...
โโโ size = 2 (actual entries)
โโโ threshold = 12 (capacity ร loadFactor = 16 ร 0.75)
โโโ loadFactor = 0.75
The equals() and hashCode() Contract
| Rule | Consequence if Violated |
|---|---|
| Equal objects must have equal hashcodes | get() searches wrong bucket โ entry "lost" |
| Unequal objects may have equal hashcodes | Allowed (collision) โ performance impact only |
hashCode() must be consistent across calls | Entry moves between buckets between calls |
If equals() uses a field, hashCode() must too | Inconsistent behavior |
// CRITICAL: Override BOTH or NEITHER
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee e)) return false;
return id == e.id && Objects.equals(name, e.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name); // Same fields as equals()
}
