Java Collections Framework Interview Preparation
A. Fundamentals & Hierarchy Java Collections Framework (JCF) is a set of interfaces, classes, and utility methods used to store and manipulate groups of objects. Compared to arrays,β¦
A. Fundamentals & Hierarchy
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
Java Collections Framework (JCF) is a set of interfaces, classes, and utility methods used to store and manipulate groups of objects.
Compared to arrays, collections provide:
- Dynamic resizing
- Ready-made data structures like
ArrayList,HashMap,HashSet - Built-in methods for searching, sorting, and iteration
- Flexibility to switch implementations by coding to interfaces
Interview Point: Arrays are fixed-size, whereas collections are dynamic and much easier to work with.
Iterable
β
Collection
ββββββββββΌβββββββββ
List Set Queue
β β
SortedSet Deque
β
NavigableSet
Map (Separate hierarchy)
β
SortedMap
β
NavigableMap
- List β Ordered, allows duplicates
- Set β No duplicates
- Queue β FIFO processing
- Deque β Insert/remove from both ends
- Map β Stores key-value pairs (separate hierarchy)
Interview Point: Remember that Map does not extend Collection.
Because Collection stores individual objects, whereas Map stores key-value pairs.
Collection provides methods like:
add()remove()iterator()
Map provides:
put()get()remove(key)
Since their purpose is different, Java designers kept Map as a separate hierarchy.
Interview Point: Map still provides collection views using:
keySet()values()entrySet()
| Collection | Collections |
|---|---|
| Interface | Utility class |
| Root interface of Collection Framework | Contains static utility methods |
| Implemented by List, Set, Queue | Provides methods like sort(), reverse(), max(), frequency(), shuffle() |
Interview Point:
Collection = Interface
Collections = Utility class
- List β Ordered collection, allows duplicates.
- Set β Stores unique elements only.
- Queue β Processes elements in FIFO order.
- Deque β Allows insertion/removal from both front and rear.
- Map β Stores unique keys and corresponding values.
- SortedSet / SortedMap β Elements remain sorted.
- NavigableSet / NavigableMap β Adds navigation methods like
floor(),ceiling(),higher(), andlower().
Interview Point: Know the purpose of each interface rather than memorizing all methods.
| Arrays | Collections |
|---|---|
| Fixed size | Dynamic size |
| Can store primitives and objects | Stores objects only |
| Less functionality | Rich API with many methods |
| Faster for primitive data | Easier to use for most applications |
Interview Point: Use arrays for fixed-size or performance-critical primitive data, and collections for almost all business applications.
Array to List
List<String> list = new ArrayList<>(Arrays.asList(arr));
List to Array
String[] arr = list.toArray(new String[0]);
Trap:
List<String> list = Arrays.asList(arr);
This creates a fixed-size list.
add()βremove()βset()β
Also, changes to the array are reflected in the list and vice versa because both share the same backing array.
Interview Point: Wrap it inside new ArrayList<>() if you need a fully mutable list.
| Arrays.asList() | List.of() |
|---|---|
| Fixed-size list | Completely immutable |
Allows null | Doesn’t allow null |
| Backed by original array | Independent immutable collection |
set() allowed | set() not allowed |
Interview Point:
Arrays.asList()β Fixed-sizeList.of()(Java 9+) β Immutable
Generics provide compile-time type safety.
Example:
List<String> list = new ArrayList<>();
Only String objects can be added, reducing runtime errors and eliminating type casting.
Type Erasure means generic type information is removed during compilation.
So at runtime:
List<String>
and
List<Integer>
are simply treated as List.
Because of type erasure:
- Cannot create
new T[] - Cannot use
instanceof List<String> - Cannot overload methods that differ only by generic type
Interview Point: Generics exist only at compile time; they are erased at runtime.
Remember PECS:
Producer Extends, Consumer Super
? extends
Used when you only read data.
List<? extends Number>
You can read Number values but cannot add new elements.
? super
Used when you only write data.
List<? super Integer>
You can safely add Integer objects.
Interview Point:
- extends β Read
- super β Write
No.
Collections can store only objects.
Primitive values are automatically converted into wrapper classes using Autoboxing.
Example:
List<Integer> list = new ArrayList<>();
list.add(10);
Here 10 becomes an Integer.
Cost:
- Extra memory usage
- Additional object creation
- More GC overhead
For heavy numerical computations, primitive arrays are usually more efficient.
Interview Point: Collections store wrapper objects, not primitives.
RandomAccess is a marker interface.
It indicates that a collection supports fast indexed access (O(1)).
Example:
ArrayList β
LinkedList β
Java utility methods like Collections.binarySearch() use this marker to choose the most efficient algorithm.
Interview Point: RandomAccess has no methodsβit simply tells Java that indexed access is efficient.
B. List Implementations
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
ArrayList is backed by a dynamic array (Object[]).
- Initially, it has a small/default capacity.
- When the array becomes full, it creates a larger array (about 1.5x bigger) and copies the existing elements into it.
get()andset()are O(1) because elements are accessed by index.- Inserting or removing elements in the middle is O(n) because remaining elements need to be shifted.
ArrayListis not thread-safe, and its iterator is fail-fast.
Interview Point: ArrayList is fast for reading but slower for middle insertions and deletions.
If you already know approximately how many elements you’ll store, initialize the list with a capacity.
List<String> list = new ArrayList<>(100000);
or
list.ensureCapacity(100000);
This avoids multiple resize and copy operations, improving performance during bulk inserts.
Interview Point: Pre-sizing is a simple optimization when the expected size is known.
| ArrayList | LinkedList |
|---|---|
| Uses dynamic array | Uses doubly linked list |
Fast random access (get()) | Slow random access |
| Middle insertion/removal is slow | Faster insertion/removal after reaching the node |
| Less memory | More memory (stores node references) |
| Better CPU cache performance | Poor cache performance |
Interview Point: In most applications, ArrayList is the preferred choice because of better overall performance.
In practice, rarely.
Although insertion and deletion are O(1), finding the position first takes O(n).
Use LinkedList mainly when:
- Frequent insertion/removal at the beginning
- Implementing a queue or deque (though
ArrayDequeis usually faster)
Interview Point: Default to ArrayList unless benchmarking shows LinkedList is a better fit.
| ArrayList | Vector |
|---|---|
| Not synchronized | Synchronized |
| Better performance | Slower because every method is synchronized |
| Introduced in Java Collections Framework | Legacy class |
Interview Point: Prefer ArrayList. Use modern concurrent collections like CopyOnWriteArrayList instead of Vector when thread safety is needed.
CopyOnWriteArrayList creates a new copy of the underlying array whenever data is modified.
- Reading is very fast.
- Iteration is thread-safe.
- Iterators never throw
ConcurrentModificationException. - Changes made after iterator creation are not visible to that iterator.
It is ideal for:
- Event listeners
- Configuration data
- Read-heavy applications
Not suitable for:
- Frequent insertions or updates, because every write copies the entire array.
Interview Point: Excellent for many reads and few writes.
There are four common ways:
1. Using removeIf() (Recommended)
list.removeIf(x -> x % 2 == 0);
2. Using Iterator.remove()
Iterator<Integer> itr = list.iterator();
while (itr.hasNext()) {
if (itr.next() % 2 == 0)
itr.remove();
}
3. Using Streams
List<Integer> newList =
list.stream()
.filter(x -> x % 2 != 0)
.toList();
4. Reverse Loop
for (int i = list.size()-1; i >= 0; i--) {
if (condition)
list.remove(i);
}
Interview Point: Never call list.remove() inside a for-each loop, otherwise you’ll get ConcurrentModificationException.
For a List<Integer>:
list.remove(2);
Removes the element at index 2.
list.remove(Integer.valueOf(2));
Removes the first occurrence of value 2.
Interview Point: This happens because Java selects remove(int index) over remove(Object).
Some commonly used methods are:
Collections.sort(list)β Sorts the listCollections.reverse(list)β Reverses the listCollections.shuffle(list)β Randomly shuffles elementsCollections.rotate(list, n)β Rotates elementsCollections.swap(list, i, j)β Swaps two elements
All these methods modify the original list.
Interview Point: These are utility methods provided by the Collections class.
Method 1: Using HashSet (Most Common)
Set<Integer> seen = new HashSet<>();
Set<Integer> duplicates = new HashSet<>();
for (Integer num : list) {
if (!seen.add(num)) {
duplicates.add(num);
}
}
Method 2: Using Java Streams
list.stream()
.collect(Collectors.groupingBy(x -> x, Collectors.counting()))
.entrySet()
.stream()
.filter(e -> e.getValue() > 1)
.map(Map.Entry::getKey)
.toList();
Interview Point: Both approaches have an average time complexity of O(n).
Set<Integer> set = new HashSet<>(list);
- Duplicate elements are removed.
- Order is not guaranteed.
If you want to preserve insertion order:
new LinkedHashSet<>(list);
If you want sorted elements:
new TreeSet<>(list);
Interview Point: LinkedHashSet is commonly used to remove duplicates while preserving the original order.
subList() returns a view of the original list, not a new list.
Example:
List<Integer> sub = list.subList(2, 5);
Changes made to sub are reflected in the original list, and vice versa.
If the original list is structurally modified after creating the sublist, using the sublist may throw ConcurrentModificationException.
A useful shortcut:
list.subList(0, 10).clear();
This removes the first 10 elements from the original list.
Interview Point: subList() returns a view, not an independent copy.
C. Set Implementations
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
HashSet is internally backed by a HashMap.
- Each element is stored as a key in the
HashMap. - A single dummy object is used as the value for all keys.
Because of this, HashSet inherits most of its behavior from HashMap:
- Fast O(1) add, remove, and contains (average case)
- No duplicate elements
- No insertion order
- Allows one
null - Not thread-safe
Interview Point: HashSet is basically a HashMap where only the keys matter.
When you call:
set.add(obj);
HashSet performs these steps:
- Calls
hashCode()to find the bucket. - Checks existing elements in that bucket using
equals(). - If an equal object already exists, it doesn’t add the new one and returns false.
- Otherwise, it inserts the object and returns true.
Interview Point: A HashSet identifies duplicates using both hashCode() and equals().
| HashSet | LinkedHashSet | TreeSet |
|---|---|---|
| No ordering | Maintains insertion order | Maintains sorted order |
| O(1) operations | O(1) operations | O(log n) operations |
Allows one null | Allows one null | Doesn’t allow null |
| Backed by HashMap | Backed by LinkedHashMap | Backed by TreeMap |
Interview Point:
- HashSet β Fastest
- LinkedHashSet β Preserve insertion order
- TreeSet β Keep elements sorted
TreeSet is internally backed by a TreeMap, which uses a Red-Black Tree.
Because of this:
add()β O(log n)remove()β O(log n)contains()β O(log n)
It also provides sorted iteration and navigation methods like:
first()last()floor()ceiling()higher()lower()
Interview Point: TreeSet maintains sorted order using a Red-Black Tree.
No.
- Adding
nullthrowsNullPointerException. - Adding objects that cannot be compared throws
ClassCastException.
Example:
TreeSet<Object> set = new TreeSet<>();
set.add("Java");
set.add(100); // ClassCastException
Interview Point: All elements in a TreeSet must be mutually comparable.
If you modify a field that is used in hashCode() or equals():
- The object remains inside the set.
- But
contains()andremove()may fail because the object now hashes to a different bucket.
Interview Point: Never modify fields used in equals() or hashCode() after inserting an object into a HashSet.
NavigableSet extends SortedSet and provides methods to find nearby elements.
Some important methods are:
floor()β Greatest element β€ given valueceiling()β Smallest element β₯ given valuelower()β Greatest element < given valuehigher()β Smallest element > given valuepollFirst()pollLast()descendingSet()
Interview Point: NavigableSet is useful for range-based searches.
Union
Set<Integer> union = new HashSet<>(set1);
union.addAll(set2);
Intersection
Set<Integer> intersection = new HashSet<>(set1);
intersection.retainAll(set2);
Difference
Set<Integer> difference = new HashSet<>(set1);
difference.removeAll(set2);
Interview Point: These methods modify the current set, so create a copy first if you want to keep the original unchanged.
EnumSet is a specialized Set implementation designed only for enum types.
Internally, it stores enum values as bits, making operations like add(), remove(), and contains() extremely fast and memory efficient.
Example:
EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
Interview Point: Always prefer EnumSet over HashSet when storing enum values.
ConcurrentSkipListSet is a thread-safe sorted set.
Features:
- Elements remain sorted.
- Supports concurrent access.
- Average O(log n) operations.
- Iterators are weakly consistent, so they don’t throw
ConcurrentModificationException.
Interview Point: Use ConcurrentSkipListSet when you need both sorting and thread safety.
Since HashSet doesn’t maintain any order, it cannot be sorted directly.
You can:
Convert to TreeSet
Set<Integer> sorted = new TreeSet<>(hashSet);
Or use Streams
List<Integer> sorted =
hashSet.stream()
.sorted()
.toList();
Interview Point: A HashSet itself can never become sorted; you must create a sorted collection.
Override both equals() and hashCode() using the unique field, such as employeeId.
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Employee e)) return false;
return this.employeeId == e.employeeId;
}
@Override
public int hashCode() {
return Integer.hashCode(employeeId);
}
Without these methods, HashSet treats every object as different, even if the employee IDs are the same.
Interview Point: Always override both equals() and hashCode() together. If you’re using a Java record, these methods are generated automatically.
D. Map & HashMap Deep Internals
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
put(key, value)
- Calculate the key’s
hashCode(). - Find the bucket index.
- If the bucket is empty, insert the entry.
- If the bucket already has entries, compare using
equals().- Same key β Update the value.
- Different key β Add the new entry (collision).
- If the number of entries exceeds the threshold, resize the table.
get(key)
- Calculate the key’s hash.
- Find the bucket.
- Compare using
equals(). - Return the matching value or
null.
Interview Point: HashMap uses both hashCode() and equals() to store and retrieve data.
Each entry stores:
- Hash value
- Key
- Value
- Reference to the next node (if collisions occur)
Internally, each entry is represented as a Node<K,V>.
Interview Point: A HashMap is essentially an array of linked nodes (or trees when many collisions occur).
Because the table size is always a power of 2.
Using:
(n - 1) & hash
is much faster than using the modulo (%) operator and produces the same bucket index.
Interview Point: Power-of-two table sizes allow HashMap to calculate bucket indexes efficiently.
Before calculating the bucket index, HashMap mixes the hash bits using:
h ^ (h >>> 16)
This spreads the higher bits into the lower bits, resulting in a better distribution of keys and fewer collisions.
Interview Point: Hash spreading improves bucket distribution and reduces collisions.
A collision happens when different keys map to the same bucket.
HashMap handles collisions by:
- Storing entries in a linked list.
- Searching the list using
equals(). - If the list becomes too large, Java 8 converts it into a Red-Black Tree.
Interview Point: Java uses separate chaining, not open addressing.
Treeification happens when:
- A bucket contains 8 or more entries, and
- The table size is at least 64.
If the number of nodes later becomes 6 or less, it converts back into a linked list.
Interview Point: Remember the numbers: 8 β Tree, 6 β List, 64 β Minimum table size.
Normally, searching a long linked list takes O(n).
After converting it into a Red-Black Tree, lookup becomes O(log n).
This improves performance when many collisions occur.
Interview Point: Treeification protects HashMap from poor performance caused by excessive collisions.
Default values:
- Initial Capacity = 16
- Load Factor = 0.75
The resize threshold is:
16 Γ 0.75 = 12
After inserting 12 elements, the table resizes.
A load factor of 0.75 provides a good balance between memory usage and performance.
Interview Point: Threshold = Capacity Γ Load Factor.
When the threshold is exceeded:
- A new table with double the capacity is created.
- Existing entries are redistributed into the new table.
- The resize operation takes O(n) time.
Interview Point: Frequent resizing affects performance, so pre-size the map if the expected size is known.
HashMap is not thread-safe.
If multiple threads modify it simultaneously:
- Updates can be lost.
- Data may become inconsistent.
- Race conditions can occur.
Use ConcurrentHashMap instead.
Interview Point: Never use a plain HashMap for concurrent writes.
Rules:
- Equal objects must have the same
hashCode(). - If you override
equals(), you should also overridehashCode().
Otherwise:
- Duplicate objects may be stored in a
HashSet. HashMapmay fail to find existing keys.
Interview Point: Always override both methods together.
Example:
public int hashCode() {
return 1;
}
All objects go into the same bucket.
- Correctness remains.
- Performance drops.
- Lookup becomes O(n) (or O(log n) after treeification).
Interview Point: A poor hashCode() hurts performance, even if the program still works correctly.
Yes.
- One null key
- Multiple null values
Comparison:
| Collection | Null Key | Null Value |
|---|---|---|
| HashMap | β One | β Yes |
| Hashtable | β | β |
| ConcurrentHashMap | β | β |
| TreeMap | β | β Yes |
Interview Point: Only one null key is allowed because keys must be unique.
A good key should be:
- Immutable
- Properly implement
equals()andhashCode() - Have a well-distributed hash value
Examples:
StringIntegerEnum- Java
record
Interview Point: Immutable objects make the best HashMap keys.
If the modified field affects hashCode() or equals():
- The entry remains in the map.
- But
get()andcontainsKey()may returnfalse.
This happens because the key now belongs to a different bucket.
Interview Point: Never modify a key after inserting it into a HashMap.
| HashMap | Hashtable |
|---|---|
| Not synchronized | Synchronized |
| Faster | Slower |
| Allows one null key and null values | Doesn’t allow null |
| Introduced in Java Collections Framework | Legacy class |
Interview Point: Prefer HashMap. Use ConcurrentHashMap for thread safety instead of Hashtable.
LinkedHashMap extends HashMap by maintaining a doubly linked list of entries.
This preserves:
- Insertion order (default)
- Access order (optional)
Interview Point: LinkedHashMap provides predictable iteration order with performance close to HashMap.
Create the map with accessOrder = true and override removeEldestEntry().
Map<Integer, String> cache =
new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
return size() > 100;
}
};
When the size exceeds the limit, the least recently used entry is automatically removed.
Interview Point: LinkedHashMap can implement a simple LRU Cache with minimal code.
| HashMap | TreeMap |
|---|---|
| Uses hashing | Uses Red-Black Tree |
| O(1) average | O(log n) |
| No ordering | Keys remain sorted |
| Allows one null key | Doesn’t allow null keys |
Interview Point: Use HashMap for fast lookup and TreeMap when sorted keys are required.
NavigableMap extends SortedMap and provides methods like:
floorKey()ceilingKey()lowerKey()higherKey()firstEntry()lastEntry()descendingMap()
These methods help find the nearest matching key.
Interview Point: NavigableMap is useful for range-based lookups.
Some important methods are:
getOrDefault()putIfAbsent()computeIfAbsent()computeIfPresent()merge()
Example (Word Count):
Map<String, Integer> count = new HashMap<>();
for (String word : words) {
count.merge(word, 1, Integer::sum);
}
Interview Point: merge() is one of the most common interview questions for Java 8 collections.
Using entrySet() (Recommended)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
Using keySet()
for (String key : map.keySet()) {
System.out.println(key);
}
Using values()
for (Integer value : map.values()) {
System.out.println(value);
}
Using Java 8
map.forEach((k, v) -> System.out.println(k + " " + v));
Interview Point: Prefer entrySet() when both key and value are needed because it avoids an extra lookup.
Sort by Key
Map<String, Integer> sorted = new TreeMap<>(map);
Sort by Value
Map<String, Integer> sorted =
map.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue())
.collect(
LinkedHashMap::new,
(m, e) -> m.put(e.getKey(), e.getValue()),
Map::putAll
);
Interview Point: Use LinkedHashMap to preserve the sorted order when sorting by value.
Map.Entry
Represents a single key-value pair in a map.
Common methods:
getKey()getValue()setValue()
Map.entry() (Java 9)
Creates an immutable key-value pair.
Example:
Map.entry("Java", 100);
It is commonly used with Map.ofEntries().
Interview Point: Map.Entry is used while iterating over a map, whereas Map.entry() is a convenient factory method introduced in Java 9.
E. Specialized Maps
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
WeakHashMap stores its keys as weak references.
If a key is no longer used anywhere else in the application, the Garbage Collector (GC) automatically removes that key and its entry from the map.
It is commonly used for:
- Caches
- Metadata storage
- Listener management
Interview Point: WeakHashMap entries disappear automatically when their keys are no longer strongly referenced.
Unlike HashMap, IdentityHashMap compares keys using:
==
instead of
equals()
So two objects with the same content are treated as different if they are different objects.
It is mainly used for:
- Object graph traversal
- Serialization
- Deep copy implementations
Interview Point: IdentityHashMap checks object identity (==), not object equality (equals()).
EnumMap is designed specifically for enum keys.
Internally, it stores values in an array, using the enum’s ordinal as the index.
Benefits:
- Faster lookups
- Less memory usage
- No hashing or collisions
- Maintains enum declaration order
Example:
Map<Status, String> map = new EnumMap<>(Status.class);
Interview Point: Whenever the keys are enums, prefer EnumMap over HashMap.
Yes.
Example:
Map<Object, String> map = new WeakHashMap<>();
Object key = new Object();
map.put(key, "Java");
key = null; // Remove the strong reference
System.gc();
After garbage collection, the entry may be removed automatically because the key is no longer strongly referenced.
Interview Point: This behavior happens only in WeakHashMap, not in HashMap.
| Reference | Description | Common Use |
|---|---|---|
| Strong | Normal object reference. Object is not garbage collected while referenced. | Regular objects |
| Soft | Collected only when memory is low. | Memory-sensitive caches |
| Weak | Collected during the next GC if no strong reference exists. | WeakHashMap |
| Phantom | Used after an object is ready for final cleanup. get() always returns null. | Resource cleanup |
Interview Point: Remember the collection order:
Strong β Soft β Weak β Phantom
Properties is a legacy class used to store configuration as key-value pairs.
Features:
- Stores keys and values as
String - Can read and write
.propertiesfiles - Commonly used for application configuration
Example:
Properties properties = new Properties();
properties.load(inputStream);
Interview Point: Properties extends Hashtable and is mainly used for configuration files.
No.
The JDK does not provide built-in implementations.
Common alternatives:
- MultiMap β
Map<K, List<V>> - BiMap β Maintain two maps internally, or use Google Guava’s
HashBiMap.
Interview Point: Java provides Map, but not BiMap or MultiMap.
The most common approach is to use:
Map<String, List<String>> multiMap = new HashMap<>();
multiMap.computeIfAbsent("fruits", k -> new ArrayList<>())
.add("Apple");
computeIfAbsent() creates the list only if it doesn’t already exist.
Without it, you’d need extra containsKey() checks.
Interview Point: computeIfAbsent() is the cleanest and most common way to implement a MultiMap in Java 8+.
F. Queue & Deque
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
Queue provides two versions of each operation:
| Operation | Throws Exception | Returns Special Value |
|---|---|---|
| Insert | add() | offer() |
| Remove | remove() | poll() |
| Read Head | element() | peek() |
Difference:
add()throws an exception if insertion fails.offer()returnsfalse.remove()throws an exception if the queue is empty.poll()returnsnull.element()throws an exception if empty.peek()returnsnull.
Interview Point: For bounded queues, prefer offer(), poll(), and peek() because they avoid exceptions.
PriorityQueue is internally implemented using a Binary Heap stored in an array.
offer()β O(log n)poll()β O(log n)peek()β O(1)
By default, it is a Min Heap, so the smallest element is always at the head.
Interview Point: PriorityQueue uses a Binary Min Heap, not a sorted array.
No.
Only the head element is guaranteed to be the highest-priority (smallest by default).
Printing or iterating over the queue does not show sorted order.
To retrieve elements in sorted order, repeatedly call:
while (!pq.isEmpty()) {
System.out.println(pq.poll());
}
Interview Point: Only peek() returns the smallest element. The queue itself is not sorted.
Use a reverse comparator.
PriorityQueue<Integer> maxHeap =
new PriorityQueue<>(Comparator.reverseOrder());
For custom objects:
PriorityQueue<Task> pq =
new PriorityQueue<>(
Comparator.comparingInt(Task::getPriority).reversed()
);
Interview Point: Default PriorityQueue is a Min Heap. Use Comparator.reverseOrder() to create a Max Heap.
- β Does not allow
nullelements. - β Is not thread-safe.
If thread safety is required, use:
PriorityBlockingQueue
Interview Point: PriorityQueue is neither thread-safe nor does it allow null.
| ArrayDeque | LinkedList |
|---|---|
| Uses resizable array | Uses doubly linked list |
| Faster | Slower |
| Less memory | More memory |
| Better cache performance | Poor cache performance |
Doesn’t allow null | Allows null |
Interview Point: Prefer ArrayDeque for implementing stacks and queues.
Stack is a legacy class that extends Vector.
Problems:
- Synchronized (slower)
- Inherits unnecessary
Vectormethods - Older API
Modern replacement:
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10);
stack.pop();
stack.peek();
Interview Point: Use ArrayDeque instead of Stack.
BlockingQueue is a thread-safe queue used in producer-consumer scenarios.
Important methods:
put()β Waits if the queue is full.take()β Waits if the queue is empty.offer(timeout)β Waits for a specified time.poll(timeout)β Waits for a specified time.
Interview Point: BlockingQueue handles synchronization internallyβno need for wait() or notify().
| Implementation | Use Case |
|---|---|
ArrayBlockingQueue | Fixed-size queue |
LinkedBlockingQueue | Dynamic or optionally bounded queue |
PriorityBlockingQueue | Priority-based processing |
SynchronousQueue | Direct handoff between threads |
DelayQueue | Delayed task execution |
Interview Point: Choose the implementation based on your application’s concurrency requirements.
BlockingQueue<Task> queue =
new ArrayBlockingQueue<>(100);
// Producer
queue.put(task);
// Consumer
Task task = queue.take();
put()blocks when the queue is full.take()blocks when the queue is empty.
No explicit synchronization is required.
Interview Point: BlockingQueue is the simplest and most common way to implement the Producer-Consumer pattern.
ConcurrentLinkedQueue is a thread-safe, non-blocking FIFO queue.
Features:
- Lock-free implementation
- Multiple threads can access it simultaneously
- Doesn’t block producers or consumers
Use it when:
- High concurrency is required
- Threads should never block
Interview Point: If blocking behavior is needed, use a BlockingQueue instead.
| Queue | Common Use |
|---|---|
LinkedBlockingQueue | Thread pool task queue |
PriorityBlockingQueue | Job scheduling based on priority |
DelayQueue | Retry mechanism, task scheduling, cache expiration |
SynchronousQueue | Thread handoff in cached thread pools |
ArrayDeque | Stack, Queue, DFS, Undo/Redo |
Interview Point: Be able to relate queue implementations to real-world scenarios. Interviewers often ask where you’ve used them in production.
G. Sorting: Comparable & Comparator
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
| Comparable | Comparator |
|---|---|
Present in java.lang package | Present in java.util package |
| Defines the object’s natural ordering | Defines a custom ordering |
| Implemented inside the class | Implemented in a separate class or lambda |
| Only one natural sorting | Multiple sorting logics are possible |
Example:
Collections.sort(list); // Uses Comparable
Collections.sort(list, comparator); // Uses Comparator
Interview Point:
- Comparable β Natural sorting
- Comparator β Custom sorting
Implement the Comparable interface and override the compareTo() method.
Example:
class Employee implements Comparable<Employee> {
private double salary;
@Override
public int compareTo(Employee other) {
return Double.compare(this.salary, other.salary);
}
}
Return Values:
- Negative β Current object comes first.
- Zero β Both objects are equal.
- Positive β Current object comes after.
Interview Point: Always use methods like Integer.compare() or Double.compare() instead of subtraction.
Use Comparator.thenComparing().
Example:
list.sort(
Comparator.comparing(Employee::getDepartment)
.thenComparing(Employee::getSalary)
.thenComparing(Employee::getName)
);
This sorts:
- By Department
- If departments are same β By Salary
- If salary is same β By Name
Interview Point: thenComparing() works like SQL’s ORDER BY.
There are multiple ways:
Natural Order (Reverse)
Collections.sort(list, Comparator.reverseOrder());
Reverse a Custom Comparator
list.sort(
Comparator.comparing(Employee::getSalary)
.reversed()
);
Interview Point:Collections.reverse() only reverses the current list order. It does not sort the list.
Java uses different algorithms for objects and primitive data types.
| Data Type | Algorithm |
|---|---|
| Objects | TimSort |
| Primitive Arrays | Dual-Pivot QuickSort |
- TimSort is stable and performs very well for partially sorted data.
- Dual-Pivot QuickSort is faster for primitive values.
Interview Point:
- Objects β TimSort
- Primitives β Dual-Pivot QuickSort
A stable sort preserves the relative order of elements having equal values.
Example:
Before sorting by Salary:
Rahul HR 50000
Amit HR 50000
After stable sorting:
Rahul HR 50000
Amit HR 50000
The original order remains the same.
Interview Point: TimSort is a stable sorting algorithm.
Sorted collections like TreeSet and TreeMap use compareTo() (or Comparator) to identify duplicates.
If compareTo() returns 0 but equals() returns false, one object may be ignored because the collection treats them as duplicates.
Interview Point: Keep compareTo() and equals() consistent to avoid unexpected behavior in TreeSet and TreeMap.
Yes.
You can use a Comparator.
Example:
list.sort(
Comparator.comparing(Employee::getSalary)
);
or
Collections.sort(
list,
Comparator.comparing(Employee::getSalary)
);
This is useful when:
- You don’t want to modify the class.
- You need different sorting criteria (salary, name, age, etc.).
Interview Point: Use Comparable for the default sorting order and Comparator for custom or multiple sorting orders.
H. Iteration & Fail-Fast Behavior
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
| Iterable | Iterator |
|---|---|
| Represents a collection that can be iterated | Used to iterate over the collection |
Has iterator() method | Has hasNext(), next(), and remove() methods |
| Enables the for-each loop | Moves through elements one by one |
Example:
List<String> list = List.of("Java", "Spring");
for (String s : list) {
System.out.println(s);
}
Internally, the for-each loop uses an Iterator.
Interview Point:
- Iterable provides an iterator.
- Iterator actually traverses the collection.
| Iterator | ListIterator |
|---|---|
| Works with all collections | Works only with List |
| Moves only forward | Moves both forward and backward |
| Can remove elements | Can add, remove, and update elements |
| Doesn’t provide index | Provides nextIndex() and previousIndex() |
Interview Point:
- Use Iterator for general collections.
- Use ListIterator when you need bidirectional traversal or modification of a list.
A Fail-Fast Iterator throws a ConcurrentModificationException if the collection is structurally modified while it is being iterated.
Example:
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
for (Integer num : list) {
list.add(4); // ConcurrentModificationException
}
Most collections like:
ArrayListHashMapHashSetTreeMap
use fail-fast iterators.
Interview Point: Fail-fast helps detect accidental modifications during iteration.
It occurs when a collection is structurally modified while it is being iterated.
Example:
for (String s : list) {
list.remove(s); // ConcurrentModificationException
}
This can happen even in a single-threaded program.
Correct Ways:
Iterator<String> itr = list.iterator();
while (itr.hasNext()) {
if (condition) {
itr.remove();
}
}
or
list.removeIf(x -> condition);
Interview Point: Most ConcurrentModificationExceptions are caused by incorrect iteration, not by multiple threads.
| Fail-Fast | Fail-Safe (Weakly Consistent/Snapshot) |
|---|---|
Throws ConcurrentModificationException | Doesn’t throw ConcurrentModificationException |
| Iterates over the original collection | Iterates over a snapshot or tolerates concurrent updates |
Used by ArrayList, HashMap, HashSet | Used by CopyOnWriteArrayList, ConcurrentHashMap |
Interview Point:
- Fail-Fast β Detects modifications and throws an exception.
- Fail-Safe β Allows concurrent modification without throwing an exception.
There are three common approaches:
1. Using removeIf() (Recommended)
list.removeIf(x -> x % 2 == 0);
2. Using Iterator.remove()
Iterator<Integer> itr = list.iterator();
while (itr.hasNext()) {
if (itr.next() % 2 == 0) {
itr.remove();
}
}
3. Using Streams
List<Integer> result =
list.stream()
.filter(x -> x % 2 != 0)
.toList();
Interview Point: Never call list.remove() inside a for-each loop. Use Iterator.remove() or removeIf() instead.
I. Thread-Safe & Concurrent Collections
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
Java provides both legacy and modern thread-safe collections.
Legacy (Avoid in new applications)
VectorHashtableStack
Modern (java.util.concurrent)
ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueueBlockingQueueConcurrentSkipListMapConcurrentSkipListSet
Interview Point: Prefer java.util.concurrent collections over legacy classes.
Use the synchronized wrapper methods provided by the Collections class.
Example:
List<String> list =
Collections.synchronizedList(new ArrayList<>());
Map<Integer, String> map =
Collections.synchronizedMap(new HashMap<>());
Every operation is synchronized using a single lock.
Interview Point: Collections.synchronizedXXX() is the easiest way to make an existing collection thread-safe.
| Synchronized Collection | Concurrent Collection |
|---|---|
| Single lock for the entire collection | Fine-grained locking or lock-free mechanisms |
| Lower performance under high concurrency | Better performance with multiple threads |
| Suitable for low concurrency | Best for high concurrency |
Examples:
Collections.synchronizedMap()ConcurrentHashMap
Interview Point: Use Concurrent Collections in multi-threaded applications because they scale much better.
ConcurrentHashMap is designed for high-performance concurrent access.
- Reads are mostly lock-free.
- Multiple threads can read and write simultaneously.
- Instead of locking the entire map, it locks only the bucket being modified.
- This greatly reduces contention compared to
HashtableorsynchronizedMap().
Interview Point: ConcurrentHashMap uses fine-grained locking, not one global lock.
Because null creates ambiguity.
Example:
map.get(key);
If null is returned, we cannot determine whether:
- The key doesn’t exist, or
- The key exists but its value is
null.
To avoid this confusion in concurrent environments, ConcurrentHashMap doesn’t allow null keys or values.
Interview Point: HashMap allows null; ConcurrentHashMap does not.
| Feature | ConcurrentHashMap | synchronizedMap() | Hashtable |
|---|---|---|---|
| Thread-safe | β | β | β |
| Locking | Fine-grained | Single lock | Single lock |
| Performance | Fast | Moderate | Slow |
| Allows null | β | Same as wrapped map | β |
| Iterator | Weakly consistent | Fail-fast | Fail-fast |
Interview Point: In modern Java, ConcurrentHashMap is the preferred choice.
| CopyOnWriteArrayList | synchronizedList |
|---|---|
| Read operations are lock-free | Every operation uses a lock |
| Every write creates a new copy | No copying |
| Excellent for read-heavy applications | Better for frequent writes |
Use CopyOnWriteArrayList for:
- Event listeners
- Configuration
- Read-mostly applications
Interview Point:
- Many Reads, Few Writes β CopyOnWriteArrayList
- Frequent Writes β synchronizedList (or another concurrent design)
A Set can be implemented using the keys of a Map.
Java provides:
Set<String> set = ConcurrentHashMap.newKeySet();
Internally, it uses a ConcurrentHashMap.
Interview Point: ConcurrentHashMap.newKeySet() is the standard thread-safe replacement for a concurrent HashSet.
Use ConcurrentHashMap with computeIfAbsent().
Example:
Map<String, Data> cache = new ConcurrentHashMap<>();
Data data = cache.computeIfAbsent(
key,
k -> loadData(k)
);
If multiple threads request the same key simultaneously:
- Only one thread loads the data.
- Other threads reuse the cached value.
Interview Point: computeIfAbsent() avoids race conditions and is commonly used to build thread-safe caches.
J. Immutability & Utility Methods
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
Java provides multiple ways to create immutable collections:
| Method | Java Version | Description |
|---|---|---|
List.of(), Set.of(), Map.of() | Java 9 | Creates immutable collections |
List.copyOf(), Set.copyOf(), Map.copyOf() | Java 10 | Creates an immutable copy of an existing collection |
Collections.unmodifiableList() | Java 1.2 | Creates a read-only view of an existing collection |
Example:
List<String> list = List.of("Java", "Spring");
Any modification like add() or remove() throws UnsupportedOperationException.
Interview Point:
- Java 9+ β
List.of() - Java 10+ β
List.copyOf() - Older Java β
Collections.unmodifiableList()
Collections.unmodifiableList() | List.of() |
|---|---|
| Read-only view | Truly immutable collection |
| Reflects changes made to the original list | Independent immutable collection |
| Original list can still be modified | Cannot be modified at all |
Example:
List<String> list = new ArrayList<>();
list.add("Java");
List<String> unmodifiable = Collections.unmodifiableList(list);
list.add("Spring");
System.out.println(unmodifiable);
Output:
[Java, Spring]
The unmodifiable list also reflects the change because it’s only a view.
Interview Point:
- unmodifiableList() β Read-only view
- List.of() β Truly immutable collection
These factory methods have some restrictions:
- β Cannot add or remove elements.
- β Do not allow
nullelements. - β
Set.of()doesn’t allow duplicate elements. - β
Map.of()doesn’t allow duplicate keys.
Example:
List.of("Java", null); // NullPointerException
Set.of("A", "A"); // IllegalArgumentException
Interview Point: List.of() creates a truly immutable collection and does not allow null values.
List.copyOf() | new ArrayList<>(list) |
|---|---|
| Immutable copy | Mutable copy |
| Cannot be modified | Can be modified |
Rejects null elements | Allows null elements |
Example:
List<String> immutable = List.copyOf(list);
List<String> mutable = new ArrayList<>(list);
Use:
List.copyOf()β Defensive copying.new ArrayList<>()β When further modifications are needed.
Interview Point:
copyOf()β Immutable copynew ArrayList<>()β Mutable copy
Immutable collections provide several advantages:
- β Thread-safe (no synchronization required)
- β Prevent accidental modification
- β Safer to share between methods or threads
- β Useful for defensive programming
- β Easier to maintain and debug
They are commonly used for:
- Configuration data
- Constants
- Read-only APIs
- DTOs and Records
Interview Point: Immutable collections improve thread safety, data integrity, and code reliability.
K. Java 8+ Streams with Collections
Expand a question to read the answer. Tick when youβre prepared (progress saved on this device).
When using Collectors.toMap(), if two elements produce the same key, Java throws an IllegalStateException.
Example:
Map<String, String> map = employees.stream()
.collect(Collectors.toMap(
Employee::getDepartment,
Employee::getName
));
If multiple employees belong to the same department, this throws an exception.
Solution: Provide a merge function.
Map<String, String> map = employees.stream()
.collect(Collectors.toMap(
Employee::getDepartment,
Employee::getName,
(oldValue, newValue) -> oldValue
));
Interview Point: Always provide a merge function when duplicate keys are possible.
groupingBy() | partitioningBy() |
|---|---|
| Groups data based on any key | Splits data into only two groups (true and false) |
Returns Map<K, List<V>> | Returns Map<Boolean, List<V>> |
Example using groupingBy()
Map<String, List<Employee>> map =
employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Example using partitioningBy()
Map<Boolean, List<Employee>> map =
employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 100000));
Interview Point:
- groupingBy() β Multiple groups.
- partitioningBy() β Only two groups (
true/false).
Find Duplicate Elements
Set<Integer> seen = new HashSet<>();
List<Integer> duplicates = numbers.stream()
.filter(n -> !seen.add(n))
.toList();
Find Second Highest Number
Integer secondHighest = numbers.stream()
.distinct()
.sorted(Comparator.reverseOrder())
.skip(1)
.findFirst()
.orElse(null);
Find Frequency of Elements
Map<Integer, Long> frequency =
numbers.stream()
.collect(Collectors.groupingBy(
n -> n,
Collectors.counting()
));
Interview Point: These three Stream problems are among the most frequently asked live coding questions.
Some of the most commonly used collectors are:
| Collector | Purpose |
|---|---|
toList() | Convert Stream to List |
toSet() | Convert Stream to Set |
toMap() | Convert Stream to Map |
groupingBy() | Group elements |
partitioningBy() | Split into true/false groups |
counting() | Count elements |
joining() | Join strings |
mapping() | Transform values before collecting |
summingInt() | Calculate sum |
averagingInt() | Calculate average |
Interview Point: groupingBy(), toMap(), joining(), and counting() are the most commonly asked collectors in interviews.
Use Parallel Streams when:
- Processing a large dataset.
- Operations are CPU-intensive.
- Tasks are independent.
- No shared mutable state.
Avoid Parallel Streams when:
- Dataset is small.
- Operations involve I/O (database, file, API calls).
- Order of processing is important.
Example:
numbers.parallelStream()
.map(n -> n * n)
.forEach(System.out::println);
Interview Point: Parallel Streams improve performance only for suitable workloads. Always benchmark before using them.