What is the difference between List, Set and Queue
The List, Set, and Queue interfaces represent the three primary functional branches inheriting from java.util.Collection. Each interface fulfills a distinct architectural role i...
🟢 Junior Level
The List, Set, and Queue interfaces represent the three primary functional branches inheriting from java.util.Collection. Each interface fulfills a distinct architectural role in data organization and access.
Core Comparison Matrix
| Dimension | List (Ordered Sequence) |
Set (Unique Collection) |
Queue (Processing Buffer) |
|---|---|---|---|
| Duplicates | Allowed (any number of duplicates) | Forbidden (uniqueness strictly enforced) | Allowed (in most standard implementations) |
| Ordering | Strict index order (by insertion position) | Unordered (HashSet), insertion-ordered (LinkedHashSet), sorted (TreeSet) |
Processing sequence (FIFO, LIFO, or priority order) |
| Element Access | Positional index access: get(i), set(i, e) |
Membership query: contains(o) or Iterator |
Head/Tail operations: peek(), poll(), offer() |
| Real-World Analogy | Numbered grocery shopping list | Bag of unique lottery balls | Line of customers at a supermarket checkout |
| Core Implementations | ArrayList, LinkedList, CopyOnWriteArrayList |
HashSet, LinkedHashSet, TreeSet |
ArrayDeque, PriorityQueue, ConcurrentLinkedQueue |
// 1. List: Preserves insertion sequence and permits duplicate entries
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Kotlin");
list.add("Java"); // Result: [Java, Kotlin, Java]
// 2. Set: Rejects duplicate entries based on equals() and hashCode()
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Kotlin");
set.add("Java"); // Returns false; Result: [Java, Kotlin]
// 3. Queue: Organizes elements for FIFO extraction
Queue<String> queue = new ArrayDeque<>();
queue.offer("Task 1");
queue.offer("Task 2");
String next = queue.poll(); // Returns "Task 1"
🟡 Middle Level
1. Dual Method Contracts in Queue
The Queue interface is specifically engineered for bounded systems and dynamic pipelines. It defines two distinct groups of methods that behave differently when an operation fails (e.g., when the queue is full or empty):
| Operation | Throws Exception on Failure | Returns Special Value (false or null) |
|---|---|---|
| Insert | add(e) (throws IllegalStateException if full) |
offer(e) (returns false if full) |
| Remove Head | remove() (throws NoSuchElementException if empty) |
poll() (returns null if empty) |
| Examine Head | element() (throws NoSuchElementException if empty) |
peek() (returns null if empty) |
[!TIP] In production and high-throughput systems, always prefer
offer(),poll(), andpeek(). They avoid the heavy CPU overhead of generating, throwing, and unwinding exception stack traces during normal queue boundary conditions.
2. Algorithmic Time Complexity Comparison
| Operation | ArrayList (List) |
HashSet (Set) |
TreeSet (Set) |
ArrayDeque (Queue) |
PriorityQueue (Queue) |
|---|---|---|---|---|---|
Index Access (get(i)) |
$O(1)$ | Not Supported | Not Supported | Not Supported | Not Supported |
| Insertion at Tail | $O(1)$ amortized | $O(1)$ | $O(\log N)$ | $O(1)$ | $O(\log N)$ |
| Insertion at Head | $O(N)$ (array shift) | $O(1)$ | $O(\log N)$ | $O(1)$ | $O(\log N)$ |
Search (contains) |
$O(N)$ (linear scan) | $O(1)$ | $O(\log N)$ | $O(N)$ | $O(N)$ |
| Remove Arbitrary Value | $O(N)$ | $O(1)$ | $O(\log N)$ | $O(N)$ | $O(N)$ |
| Extract Head Element | $O(N)$ (remove(0)) |
Not Applicable | $O(\log N)$ (pollFirst) |
$O(1)$ | $O(\log N)$ |
3. Architectural Selection Flowchart
What is your primary engineering requirement?
│
├── Fast indexed retrieval by numeric position? ──► List (ArrayList)
│
├── Guarantee element uniqueness / rapid membership checks? ──► Set
│ ├── Maximum throughput, order irrelevant? ──────────► HashSet (O(1))
│ ├── Maintain exact insertion sequence? ─────────────► LinkedHashSet
│ └── Maintain continuous sorted natural order? ──────► TreeSet (O(log N))
│
└── Process tasks in arrival order or by priority? ──► Queue
├── High-speed FIFO queue or LIFO stack? ──────────► ArrayDeque (O(1))
└── Process elements ordered by priority/Comparator? ─► PriorityQueue (O(log N))
🔴 Senior Level
1. Memory Overhead: The Hidden Cost of HashSet
Engineers frequently underestimate the substantial memory discrepancy between ArrayList and HashSet:
ArrayList (1,000,000 elements):
[ Object[] backing array ] ──► 1,000,000 reference pointers (~4 MB with Compressed OOPs)
HashSet (1,000,000 elements):
Backed by HashMap, wrapping EVERY element inside an allocated HashMap$Node:
├── Mark Word + Klass Word (Object Header): 12 bytes
├── Cached hash code (int): 4 bytes
├── Key reference pointer: 4 bytes
├── Value reference pointer (dummy Object PRESENT): 4 bytes
├── Next collision node pointer: 4 bytes
└── JVM 8-byte alignment padding: 4 bytes
Total per Node: 32 bytes + (4 bytes per bucket table slot) = ~36–40 bytes per entry!
[!IMPORTANT] Storing 1,000,000 elements in an
ArrayListrequires ~4–8 MB of heap memory. Storing the exact same elements in aHashSetconsumes ~36–40 MB—a $5\times$ to $8\times$ memory amplification caused by node wrapper allocations, table load factors, and pointer metadata.
2. The Equality Paradox: TreeSet Bypasses equals()
The formal specification of Set states that uniqueness is determined by equals(). However, TreeSet violates this contract:
TreeSetis backed by a Red-Black tree (TreeMap). It evaluates element equality exclusively viaComparable.compareTo()orComparator.compare().- It never invokes
equals()orhashCode()during insertion or lookup!
class Account implements Comparable<Account> {
final int id;
final String holder;
Account(int id, String holder) { this.id = id; this.holder = holder; }
@Override
public boolean equals(Object o) {
// Strict equality: checks BOTH id AND holder
return o instanceof Account a && this.id == a.id && Objects.equals(this.holder, a.holder);
}
@Override
public int compareTo(Account o) {
// Compares ONLY id:
return Integer.compare(this.id, o.id);
}
}
Set<Account> treeSet = new TreeSet<>();
treeSet.add(new Account(101, "Alice"));
boolean added = treeSet.add(new Account(101, "Bob"));
// ⚠️ Returns FALSE! The second account is rejected as a duplicate because compareTo() == 0,
// even though equals() would return false!
3. Concurrent Alternatives for Multi-Threaded Architectures
Standard collection implementations (ArrayList, HashSet, ArrayDeque) are unsynchronized and fail-fast. For multi-threaded architectures, select specialized concurrent collections:
- For
List:CopyOnWriteArrayList: Ideal for read-heavy, write-rare scenarios (e.g., event listener registries). Mutators duplicate the entire underlying array, enabling lock-free iterators that never throwConcurrentModificationException.
- For
Set:ConcurrentHashMap.newKeySet(): The industry standard for concurrent sets. Backed byConcurrentHashMap, it uses CAS (Compare-And-Swap) on bucket heads and synchronized node locking for collisions, eliminating global collection locks.
- For
Queue:ConcurrentLinkedQueue: Unbounded, lock-free non-blocking queue based on the Michael & Scott queue algorithm.ArrayBlockingQueue/LinkedBlockingQueue: Thread-safe bounded queues that provide backpressure (blocking producers when full and blocking consumers when empty) for Producer-Consumer designs.
4. Extreme Scale: Bloom Filters vs. HashSet
When maintaining uniqueness checks across hundreds of millions of keys (e.g., URL crawlers, cache-aside deduplication):
- Storing 500 million strings in
HashSetrequires tens of gigabytes of JVM heap, inducing massive GC pause penalties. - A Bloom Filter (e.g., Guava
BloomFilteror Redis Bloom) uses bit arrays and multiple hash functions:- Consumes megabytes instead of gigabytes (up to a $95\%$ memory reduction).
- Provides $O(K)$ constant-time membership queries.
- Trade-off: Probabilistic semantics—guarantees zero false negatives (“definitely not in set”), with a small, mathematically tunable false positive rate (“probably in set”).
4 Tricky Questions
1. Why does HashSet consume 4 to 5 times more memory than ArrayList to store the exact same number of object references?
Answer:
ArrayList stores references in a single, contiguous array (Object[]). Each element slot consumes only 4 bytes (under 32-bit Compressed OOPs) or 8 bytes of memory.
HashSet is backed internally by a HashMap. Every element added to a HashSet is wrapped in an individual HashMap$Node heap object. Each node incurs:
- A 12-byte object header (Mark Word + Klass Word);
- A 4-byte cached hash integer;
- A 4-byte reference to the Key;
- A 4-byte reference to the static dummy value
PRESENT; - A 4-byte reference to the
nextcollision node; - 4 bytes of 8-byte boundary alignment padding.
This equals 32 bytes per node. Adding the 4-byte slot in the table array and the default load factor overhead (0.75, which leaves 25% of table buckets empty) brings total overhead to ~36–40 bytes per element, compared to just 4 bytes in ArrayList.
2. Why can TreeSet reject a new element as a duplicate even if itemA.equals(itemB) returns false?
Answer:
TreeSet is backed by a Red-Black Tree (TreeMap). Unlike HashSet, which strictly adheres to the contract of equals() and hashCode(), TreeSet locates nodes and determines equality exclusively through Comparable.compareTo() or the supplied Comparator.compare().
When inserting a new element, the tree traverses nodes by comparing values. If comparator.compare(existing, incoming) == 0, the tree concludes that the element already exists and overwrites or discards it. The equals() method is never invoked. If a class’s compareTo() implementation is inconsistent with equals() (e.g., comparing only on an id field while equals() compares both id and name), TreeSet will reject elements that equals() considers distinct.
3. What is the operational distinction between the two groups of insertion and removal methods in the Queue interface, and why is poll() preferred over remove() in high-throughput systems?
Answer:
The Queue interface splits operations into two design contracts:
- Exception-throwing methods (
add,remove,element): Designed for scenarios where failure constitutes an unexpected program error. They throwIllegalStateExceptionorNoSuchElementException. - Special-value methods (
offer,poll,peek): Designed for capacity-constrained and polling loops. They returnfalseornullupon failure.
In high-throughput systems, queues regularly fluctuate between empty and full states. If a consumer thread repeatedly calls remove() on an empty queue, the JVM must instantiate a NoSuchElementException and traverse the call stack to construct the execution trace. Generating thousands of exception stack traces per second causes severe CPU saturation and GC allocation pressure. In contrast, poll() returns a lightweight null reference, executing in a few nanoseconds without CPU or memory overhead.
4. Which data structure should you select to implement a high-throughput thread-safe LIFO stack in modern Java, and why is java.util.Stack considered an anti-pattern?
Answer:
You should use ConcurrentLinkedDeque (for non-blocking concurrent LIFO access) or an ArrayDeque wrapped in appropriate synchronization.
java.util.Stack is an obsolete legacy class dating back to Java 1.0 that extends Vector. Its flaws include:
- Liskov Substitution Principle Violation: Extending
Vectorallows callers to invoke index-based operations (stack.insertElementAt(item, 5)), completely violating the fundamental LIFO contract of a stack. - Synchronization Contention: Every method in
Vector/Stackis guarded by an intrinsic monitor lock (synchronized). In single-threaded execution, this incurs unnecessary lock overhead; in multi-threaded execution, it causes severe thread contention. - In contrast,
ArrayDequeuses an efficient circular array buffer without synchronization overhead, andConcurrentLinkedDequeprovides high-throughput lock-free CAS operations.
🎯 Interview Cheat Sheet
30-Second Summary
“The Collection Framework branches into three core interfaces:
Listpreserves insertion sequence, permits duplicate elements, and provides constant-time $O(1)$ random positional index access (ArrayList).Setguarantees element uniqueness, rejecting duplicates viaequals()/hashCode()(HashSet) or ordering comparisons (TreeSet), optimized for $O(1)$ membership checks.Queuestructures elements for consumption (FIFO or priority order), providing non-blocking operations (offer,poll,peek) that return special values instead of throwing exceptions. For concurrent systems, choose modern lock-free or CAS collections likeConcurrentHashMap.newKeySet()andConcurrentLinkedQueuerather than legacy synchronized wrappers.”
Common Follow-up Questions
- “Why is
ArrayDequepreferred overLinkedListfor both queues and stacks?” →ArrayDequeis backed by a circular array, avoidingNodeobject allocations, eliminating GC pressure, and maximizing CPU cache line utilization. - “What happens if you insert an element with a broken
hashCode()into aHashSet?” → The element can be inserted, but subsequent calls tocontains()orremove()will compute a different bucket index and fail to find it, causing silent logical corruption and memory leaks. - “When should you use
LinkedHashSet?” → When you require the $O(1)$ lookup performance of aHashSet, but also need predictable insertion-order iteration.
Red Flags (DO NOT Say)
- ❌ “Checking
list.contains(x)is just as fast asset.contains(x).” (Listsearch is $O(N)$ linear scanning;Setis $O(1)$ hash lookup). - ❌ “TreeSet uses equals() to check for duplicate elements.” (
TreeSetexclusively usescompareTo()/Comparator.compare() == 0). - ❌ “I use
java.util.Stackwhenever I need a LIFO stack in Java.” (Stackis legacy, extendsVector, and synchronizes every method;ArrayDequeis the modern standard).
Related Topics
- What are the main Collection Framework interfaces — Architectural overview of JCF
- What is the difference between ArrayList and LinkedList — Deep dive into List implementations
- How does HashSet work internally — Node layout and hashing mechanics
- What is TreeSet and how does it work — Red-Black tree internals
- What is Queue and what implementations exist — Detailed queue implementations
- What is Deque — Double-ended queue and stack contracts