Key takeaways
- Collections questions cluster into four families: List implementations (ArrayList, LinkedList), Map implementations (HashMap, LinkedHashMap, TreeMap, and their thread-safe variants), Set implementations, and iteration/ordering contracts (fail-fast behaviour,
ComparablevsComparator, Queue semantics). - Knowing an implementation's name isn't the bar. Interviewers want to know why you'd pick one over another for a given access pattern. For example random access vs frequent insertion, ordering guarantees, thread-safety needs.
- Several of Java's most cited "gotchas" live in this list:
ConcurrentModificationException, HashMap keys that get mutated after insertion, and assumingHashMappreserves insertion order. All three compile fine and fail at runtime or silently, which is exactly why they're asked.
Intro
The Collections Framework is where a lot of real Java bugs live, which is exactly why it's interview territory. This guide collects the fifteen questions that come up most, covering List, Set, Map, and Queue implementations, plus the ordering and thread-safety contracts that catch experienced developers as often as beginners. Every question ships with working code and its printed output.
How interviewers read a Java Collections answer
| Layer | What a weaker answer does | What a stronger answer does |
| Conceptual clarity | Names an implementation, can't say what's different underneath | Names the underlying structure (array, linked nodes, hash table, red-black tree) and what that implies |
| Design judgment | Defaults to ArrayList/HashMap for everything | Picks the implementation based on the actual access pattern, reads vs writes, ordering and thread-safety |
| Correctness | Code compiles but violates an implicit contract (mutating a key, modifying a list mid-iteration) | Anticipates the contract and codes around it before being asked |
| Communication | States the definition and stops | Explains what breaks in production when the wrong choice is made |
1What is the difference between ArrayList and LinkedList, and when should you use each?
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ArrayListVsLinkedListDemo {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(0, 0); // insert at the front - shifts every element right, O(n)
System.out.println(arrayList); // [0, 1, 2]
System.out.println(arrayList.get(1)); // 1 - O(1), backed by a plain array
List<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(0, 0); // insert at the front - O(1), just relinks pointers
System.out.println(linkedList); // [0, 1, 2]
}
}ArrayList is backed by a resizable array. O(1) random access via get(index), but inserting anywhere except the end means shifting every subsequent element. LinkedList is backed by doubly-linked nodes, inserting at either end is O(1), but it has no real random access. The mistake is forgetting that second half:
import java.util.LinkedList;
import java.util.List;
public class LinkedListRandomAccessMistake {
public static void main(String[] args) {
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 5; i++) linkedList.add(i);
// Mistake: index-based access on a LinkedList - each get() walks from the head, O(n) per call
for (int i = 0; i < linkedList.size(); i++) {
System.out.print(linkedList.get(i) + " ");
}
System.out.println();
// 0 1 2 3 4 -- correct output, but O(n^2) overall on a LinkedList
}
}What this evaluates: Design judgment. The output is correct either way. The signal is recognising that get(i) in a loop is fine on an ArrayList and quietly quadratic on a LinkedList, and reaching for a for-each loop or Iterator instead.
2What is the difference between HashMap, LinkedHashMap, and TreeMap?
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class MapImplementationsDemo {
public static void main(String[] args) {
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
Map<String, Integer> treeMap = new TreeMap<>();
String[] keys = {"banana", "apple", "cherry"};
for (String key : keys) {
linkedHashMap.put(key, key.length());
treeMap.put(key, key.length());
}
System.out.println(linkedHashMap.keySet()); // [banana, apple, cherry] - insertion order preserved
System.out.println(treeMap.keySet()); // [apple, banana, cherry] - sorted (natural) order
}
}All three implement Map, but their ordering guarantees come from different underlying structures: HashMap is a plain hash table with no ordering guarantee at all, LinkedHashMap adds a linked list threading through the entries to preserve insertion order, and TreeMap is a red-black tree that keeps keys sorted, at O(log n) instead of O(1) per operation. The mistake is assuming plain HashMap behaves like the other two:
import java.util.HashMap;
import java.util.Map;
public class HashMapOrderMistake {
public static void main(String[] args) {
Map<Integer, String> scores = new HashMap<>();
scores.put(3, "three");
scores.put(1, "one");
scores.put(2, "two");
// Mistake: assuming HashMap preserves insertion order
for (Map.Entry<Integer, String> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// Order is not guaranteed. For small Integer keys you'll often see them
// print in ascending order (1, 2, 3) - because Integer.hashCode() equals
// the value itself, which maps directly to bucket index. That is NOT the
// same thing as insertion order (3, 1, 2), and it is not a contract you
// can rely on - it changes with key types, hash collisions, and resizing.
}
}What this evaluates: Conceptual clarity. Small integer keys often print in a way that looks ordered by coincidence, which is precisely what makes this mistake so easy to carry into production undetected.
3What is a fail-fast iterator, and how do you avoid ConcurrentModificationException?
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class FailFastIteratorDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Ravi", "Asha", "Meera"));
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
if (name.equals("Ravi")) {
iterator.remove(); // safe - goes through the iterator, which resyncs its internal state
}
}
System.out.println(names); // [Asha, Meera]
}
}Iterator.remove() is the only safe way to remove elements while iterating — it updates the iterator's own bookkeeping so the next next() call doesn't notice anything changed. Removing directly from the list underneath an active iterator is the classic mistake:
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationMistake {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Ravi", "Asha", "Meera"));
for (String name : names) {
if (name.equals("Ravi")) {
names.remove(name); // structural change during for-each - fail-fast detects it
}
}
System.out.println(names);
// Throws java.util.ConcurrentModificationException on the next iteration -
// the enhanced for-loop uses an Iterator under the hood, and names.remove()
// desyncs its internal modCount without going through that iterator.
}
}What this evaluates: Correctness. This is a compiles-clean, fails-at-runtime bug. A strong candidate names the fix (iterator.remove(), or collecting matches into a separate list to remove after the loop) without needing to see the stack trace first.
4What is the difference between Comparable and Comparator?
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
class Employee implements Comparable<Employee> {
String name;
int age;
Employee(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Employee other) {
return Integer.compare(this.age, other.age); // natural ordering: by age
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}
public class ComparableVsComparatorDemo {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>(List.of(
new Employee("Ravi", 35),
new Employee("Asha", 28),
new Employee("Meera", 42)
));
Collections.sort(employees); // uses compareTo() - natural ordering, by age
System.out.println(employees); // [Asha(28), Ravi(35), Meera(42)]
employees.sort(Comparator.comparing(e -> e.name)); // Comparator - an alternate ordering, by name
System.out.println(employees); // [Asha(28), Meera(42), Ravi(35)]
}
}Comparable is implemented by the class itself and defines one natural ordering. Comparator is external and lets you define as many orderings as you need without touching the class. The mistake is not having either when you need one:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Product { // does not implement Comparable
String name;
double price;
Product(String name, double price) {
this.name = name;
this.price = price;
}
}
public class NotComparableMistake {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Pen", 20));
products.add(new Product("Notebook", 60));
Collections.sort(products);
// Does not compile: Product does not implement Comparable<Product>,
// so Collections.sort(List<T>) has no natural ordering to fall back on.
// Fix: implement Comparable on Product, or call
// products.sort(Comparator.comparingDouble(p -> p.price)) instead.
}
}What this evaluates: Design judgment. A strong answer doesn't just know the fix, it knows when to reach for Comparable (one obvious natural order, e.g. price) versus Comparator (multiple situational orders, or a class you can't modify).
5How does HashMap actually work internally?
import java.util.HashMap;
import java.util.Map;
public class HashMapInternalsDemo {
// Reproduces HashMap's internal bucket-index formula for a table of the given capacity
static int bucketIndex(int hash, int capacity) {
int spread = hash ^ (hash >>> 16); // HashMap "spreads" high bits into the low bits
return spread & (capacity - 1); // capacity is always a power of two, so this is a fast modulo
}
public static void main(String[] args) {
int capacity = 16; // HashMap's default initial capacity
System.out.println(bucketIndex(3, capacity)); // 3
System.out.println(bucketIndex(19, capacity)); // 3 - collides with 3 in a 16-bucket table
Map<Integer, String> table = new HashMap<>();
table.put(3, "three");
table.put(19, "nineteen"); // same bucket as 3 - stored alongside it in that bucket's list
System.out.println(table.get(19)); // nineteen - equals() disambiguates within the bucket
}
}A HashMap computes a key's hash, spreads its bits to reduce collisions, then masks it down to a bucket index within the current table size. Two different keys can land in the same bucket. HashMap resolves that by chaining entries in a list per bucket (or a tree, once a bucket gets crowded) and calling equals() to find the exact match. The mistake that breaks this whole mechanism is mutating a key after it's already been inserted:
import java.util.HashMap;
import java.util.Map;
class MutableKey {
int value;
MutableKey(int value) { this.value = value; }
@Override
public int hashCode() { return value; }
@Override
public boolean equals(Object o) {
return o instanceof MutableKey && ((MutableKey) o).value == this.value;
}
}
public class MutableKeyMistake {
public static void main(String[] args) {
Map<MutableKey, String> map = new HashMap<>();
MutableKey key = new MutableKey(3);
map.put(key, "original bucket");
key.value = 5; // mutating the key after insertion changes its hash code
System.out.println(map.get(key)); // null - looks in bucket 5; the entry is still in bucket 3
System.out.println(map.get(new MutableKey(3))); // null too - bucket 3 has an entry, but it no longer equals 3
System.out.println(map.size()); // 1 - the entry still exists internally, just unreachable
}
}What this evaluates: Conceptual depth. Most candidates can say "HashMap uses hashing", but fewer can explain the spread-and-mask step, or predict that mutating a key strands its entry in the wrong bucket, permanently unreachable, without throwing any error at all.
6What is the difference between HashSet, LinkedHashSet, and TreeSet?
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
public class SetImplementationsDemo {
public static void main(String[] args) {
Set<Integer> linkedHashSet = new LinkedHashSet<>();
Set<Integer> treeSet = new TreeSet<>();
int[] values = {30, 10, 20, 10}; // 10 is a duplicate
for (int v : values) {
linkedHashSet.add(v);
treeSet.add(v);
}
System.out.println(linkedHashSet); // [30, 10, 20] - insertion order, duplicate dropped
System.out.println(treeSet); // [10, 20, 30] - sorted order, duplicate dropped
}
}Evaluates: Conceptual clarity. All three enforce uniqueness, but only LinkedHashSet preserves insertion order and only TreeSet guarantees sorted order; plain HashSet guarantees neither.
7What is the difference between Iterator and ListIterator?
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class ListIteratorDemo {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>(List.of("apple", "banana", "cherry"));
ListIterator<String> it = fruits.listIterator();
while (it.hasNext()) {
String fruit = it.next();
if (fruit.equals("banana")) {
it.set("blueberry"); // Iterator can only remove; ListIterator can also replace and insert
}
}
while (it.hasPrevious()) { // ListIterator can walk backward too - plain Iterator cannot
System.out.print(it.previous() + " ");
}
// cherry blueberry apple
}
}Evaluates: Conceptual clarity. ListIterator extends Iterator with backward traversal, in-place replacement (set), and insertion (add), none of which plain Iterator supports.
8What is the difference between HashMap, Hashtable, and ConcurrentHashMap?
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class HashMapVsHashtableDemo {
public static void main(String[] args) {
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "allowed as a key"); // HashMap permits one null key, and null values
System.out.println(hashMap.get(null)); // allowed as a key
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
try {
concurrentMap.put(null, "not allowed");
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap rejects null keys/values");
}
Map<String, String> hashtable = new Hashtable<>();
try {
hashtable.put(null, "not allowed either");
} catch (NullPointerException e) {
System.out.println("Hashtable rejects null keys/values too");
}
}
}Evaluates: Conceptual clarity. HashMap is unsynchronized and null-permissive; Hashtable is legacy and fully synchronized (one lock for the whole map); ConcurrentHashMap is the modern thread-safe choice, using fine-grained locking and rejecting nulls outright, since a null can't disambiguate "absent" from "present with a null value" under concurrent access.
9What is a PriorityQueue, and how does it order elements?
import java.util.PriorityQueue;
import java.util.Queue;
public class PriorityQueueDemo {
public static void main(String[] args) {
Queue<Integer> minHeap = new PriorityQueue<>(); // natural ordering: smallest first
minHeap.add(30);
minHeap.add(10);
minHeap.add(20);
System.out.println(minHeap.poll()); // 10 - smallest, not first-in
System.out.println(minHeap.poll()); // 20
System.out.println(minHeap.poll()); // 30
}
}Evaluates: Conceptual clarity. PriorityQueue is not FIFO; it's a heap that always returns the smallest element first (or, with a custom Comparator, the "highest priority" one), regardless of insertion order.
10What is the difference between Collections.synchronizedList and CopyOnWriteArrayList?
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class ThreadSafeListDemo {
public static void main(String[] args) {
List<Integer> synced = Collections.synchronizedList(new ArrayList<>(List.of(1, 2, 3)));
List<Integer> cow = new CopyOnWriteArrayList<>(List.of(1, 2, 3));
// synchronizedList: iterating still requires manual synchronization to be safe
synchronized (synced) {
for (int v : synced) {
System.out.print(v + " ");
}
}
System.out.println();
// CopyOnWriteArrayList: safe to iterate without manual locking - it iterates
// over a snapshot array taken when the iterator was created
for (int v : cow) {
System.out.print(v + " ");
}
System.out.println();
// 1 2 3
// 1 2 3
}
}Evaluates: Design judgment. synchronizedList locks every method call but still needs manual synchronization during iteration to avoid ConcurrentModificationException; CopyOnWriteArrayList iterates safely without locking by copying its backing array on every write, which suits read-heavy, write-rare workloads and is a poor fit for write-heavy ones.
11What is the difference between poll(), peek(), and remove() on a Queue?
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Queue;
public class QueueMethodsDemo {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
System.out.println(queue.poll()); // null - empty queue, poll() fails safely
try {
queue.remove(); // remove() throws on an empty queue instead
} catch (NoSuchElementException e) {
System.out.println("remove() threw NoSuchElementException on an empty queue");
}
queue.add(1);
queue.add(2);
System.out.println(queue.peek()); // 1 - looks without removing
System.out.println(queue.size()); // 2 - confirms peek() did not remove
System.out.println(queue.poll()); // 1 - removes and returns the head
System.out.println(queue.size()); // 1 - only element 2 is left
}
}Evaluates: Correctness. Queue offers two method families for the same operations: poll()/peek() return a sentinel (null) on an empty queue, while remove()/element() throw. Reaching for the wrong family is a common source of unexpected NullPointerExceptions or NoSuchElementExceptions.
12How do you sort a list of custom objects in Java?
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class Student {
String name;
double gpa;
Student(String name, double gpa) {
this.name = name;
this.gpa = gpa;
}
@Override
public String toString() {
return name + "(" + gpa + ")";
}
}
public class SortCustomObjectsDemo {
public static void main(String[] args) {
List<Student> students = new ArrayList<>(List.of(
new Student("Ravi", 8.2),
new Student("Asha", 9.1),
new Student("Meera", 7.8)
));
students.sort(Comparator.comparingDouble((Student s) -> s.gpa).reversed());
System.out.println(students); // [Asha(9.1), Ravi(8.2), Meera(7.8)]
}
}Evaluates: Idiom. Building a Comparator with Comparator.comparingDouble(...).reversed() instead of hand-writing a compare method, without touching the Student class itself.
13What is the difference between Set and List?
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SetVsListDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(5);
list.add(5);
list.add(3);
System.out.println(list); // [5, 5, 3] - order preserved, duplicates allowed
Set<Integer> set = new HashSet<>();
set.add(5);
set.add(5);
set.add(3);
System.out.println(set.size()); // 2 - the duplicate is silently dropped
System.out.println(list.get(0)); // 5 - List supports index access
// set.get(0) would not even compile - Set has no index-based access at all
}
}Evaluates: Correctness.List is ordered, index-accessible, and permits duplicates; Set guarantees uniqueness and, depending on implementation, may guarantee no order and no index access whatsoever.
14How do you make a collection unmodifiable in Java?
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UnmodifiableCollectionDemo {
public static void main(String[] args) {
List<Integer> mutable = new ArrayList<>(List.of(1, 2, 3));
List<Integer> readOnly = Collections.unmodifiableList(mutable);
try {
readOnly.add(4);
} catch (UnsupportedOperationException e) {
System.out.println("readOnly rejected the write");
}
mutable.add(4); // the backing list can still be changed directly
System.out.println(readOnly); // [1, 2, 3, 4] - the "view" reflects the change
}
}Evaluates: Design judgment. Collections.unmodifiableList blocks writes through that specific reference, but it's a live view over the original list, not an immutable copy; the backing list can still be mutated directly. For a genuine immutable snapshot, List.copyOf(mutable) is the right call instead.
15What is the difference between an array and an ArrayList?
import java.util.ArrayList;
import java.util.List;
public class ArrayVsArrayListDemo {
public static void main(String[] args) {
int[] array = new int[3]; // fixed size, holds primitives directly, size fixed at creation
array[0] = 10;
array[1] = 20;
array[2] = 30;
System.out.println(array.length); // 3
List<Integer> list = new ArrayList<>(); // grows dynamically, only holds objects
list.add(10);
list.add(20);
list.add(30);
list.add(40); // arrays can't do this without manual resizing
System.out.println(list.size()); // 4
}
}Evaluates: Conceptual clarity. An array is a fixed-size, low-level construct that holds primitives directly; ArrayList is a resizable object built on top of an array internally, but it only holds reference types (autoboxing int to Integer as needed), which carries real memory and performance cost at scale.
How should you prepare for a Java Collections interview?
- Match the implementation to the access pattern, out loud. For every question here, be ready to say "I'd pick X because the workload is mostly Y" — that's the actual thing being tested, not the implementation's name.
- Know which contracts the compiler won't enforce for you. Fail-fast iteration, key mutability, null handling across
Maptypes — these all compile cleanly and fail (or silently corrupt) at runtime. - Run the mistakes, not just the fixes. Seeing
ConcurrentModificationExceptionor a strandedHashMapentry once in a sandbox saves you from debugging it cold in production.
FAQ
What are the most commonly asked Java Collections interview questions? ArrayList vs LinkedList, the Map implementations (HashMap, LinkedHashMap, TreeMap), fail-fast iteration and ConcurrentModificationException, and Comparable vs Comparator come up most consistently, followed closely by HashMap's internal hashing mechanism.
Why does ConcurrentModificationException happen, and how do I avoid it? It's thrown when a collection is structurally modified directly while an active iterator (including the one behind an enhanced for-loop) is still walking it. Use Iterator.remove() instead of the collection's own remove(), or collect the items to remove into a separate list and remove them after the loop.
Does HashMap guarantee any ordering? No. LinkedHashMap guarantees insertion order and TreeMap guarantees sorted order — plain HashMap guarantees neither, even if small integer keys often happen to print in what looks like sorted order.
When should I use TreeMap instead of HashMap? When you need keys in sorted order for iteration, range queries, or "closest key" lookups. The trade-off is O(log n) operations instead of HashMap's average O(1).
How long does it take to prepare for a Java Collections interview? Less about breadth and more about depth on a handful of implementations. Being able to explain why HashMap behaves the way it does, from hashing through collision resolution, covers a disproportionate share of what gets asked.