Deep Dive: Java Collections Framework

The Java Collections Framework is one of the most fundamental and powerful parts of the Java Standard Library. Understanding how to choose and use the right collection can dramatically impact your application's performance, memory usage, and code maintainability.
This guide provides a comprehensive deep-dive into the Collections Framework, with performance analysis, practical examples, and best practices for experienced developers.
Collections Framework Overview
The Collections Framework consists of interfaces, implementations, and algorithms that represent and manipulate collections of objects.
Core Hierarchy
Collection (interface)
├── List (interface)
│ ├── ArrayList
│ ├── LinkedList
│ └── Vector (legacy, synchronized)
│
├── Set (interface)
│ ├── HashSet
│ ├── LinkedHashSet
│ └── TreeSet (SortedSet)
│
└── Queue (interface)
├── ArrayDeque
├── PriorityQueue
└── LinkedList
Map (interface, separate from Collection)
├── HashMap
├── LinkedHashMap
├── TreeMap (SortedMap)
├── Hashtable (legacy, synchronized)
└── ConcurrentHashMap (thread-safe)Key Design Principles:
- Interface-based: Program to interfaces, not implementations
- Fail-fast iterators: Most collections throw
ConcurrentModificationExceptionwhen structurally modified during iteration - Generic types: Type-safe collections using Java Generics
- Consistent API: Common methods across all collection types
List Interface: Ordered Collections
Lists maintain insertion order and allow duplicate elements. Choose between ArrayList and LinkedList based on your access patterns.
ArrayList: Dynamic Array
Best for: Random access, iteration, and when you know approximate size.
List<String> names = new ArrayList<>(100); // Initial capacity
// O(1) - Add to end (amortized)
names.add("Alice");
names.add("Bob");
// O(1) - Random access by index
String first = names.get(0);
// O(n) - Insert in middle (requires shifting)
names.add(1, "Charlie");
// O(n) - Search by value
boolean hasAlice = names.contains("Alice");
// O(n) - Remove by index (requires shifting)
names.remove(0);Performance Characteristics:
- Access by index: O(1)
- Add to end: O(1) amortized (O(n) when resizing)
- Add/remove at index: O(n) - requires shifting elements
- Search: O(n)
- Memory overhead: Low - just array storage
Internal Mechanism:
// Simplified ArrayList internals
public class ArrayList<E> {
private Object[] elementData;
private int size;
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length) {
// Grow by ~50% (old capacity + old capacity >> 1)
int newCapacity = elementData.length + (elementData.length >> 1);
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
}Best Practices:
// ✅ DO: Specify initial capacity if known
List<Item> items = new ArrayList<>(expectedSize);
// ✅ DO: Use for random access scenarios
for (int i = 0; i < items.size(); i++) {
process(items.get(i)); // O(1) access
}
// ❌ DON'T: Frequent insertions in middle
for (int i = 0; i < 1000; i++) {
items.add(0, newItem); // O(n) each time - slow!
}LinkedList: Doubly-Linked List
Best for: Frequent insertions/deletions at both ends, or when you need a Deque.
LinkedList<String> queue = new LinkedList<>();
// O(1) - Add to either end
queue.addFirst("First");
queue.addLast("Last");
// O(1) - Remove from either end
String first = queue.removeFirst();
String last = queue.removeLast();
// O(n) - Random access by index
String item = queue.get(5); // Traverses from head or tail
// O(n) - Search
boolean found = queue.contains("item");Performance Characteristics:
- Access by index: O(n)
- Add/remove at head/tail: O(1)
- Add/remove at arbitrary position: O(n) to find + O(1) to modify
- Search: O(n)
- Memory overhead: High - each node has two pointers (next, prev)
Internal Structure:
// Simplified LinkedList node
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}ArrayList vs LinkedList: When to Use
| Operation | ArrayList | LinkedList |
|---|---|---|
| get(index) | O(1) ✅ | O(n) ❌ |
| add(element) (end) | O(1)* ✅ | O(1) ✅ |
| add(0, element) | O(n) ❌ | O(1) ✅ |
| remove(index) | O(n) ❌ | O(n) ⚠️ |
| Iterator.remove() | O(n) ❌ | O(1) ✅ |
| Memory overhead | Low ✅ | High ❌ |
*Amortized - Occasionally O(n) when resizing
Decision Matrix:
// Use ArrayList when:
// - Random access is frequent
// - Adding to end is primary operation
// - Memory efficiency matters
// - You iterate with index-based loops
List<String> names = new ArrayList<>();
// Use LinkedList when:
// - Implementing a queue/deque
// - Frequent add/remove at head
// - Using iterator to remove during traversal
Deque<Task> taskQueue = new LinkedList<>();Set Interface: Unique Elements
Sets guarantee uniqueness (no duplicates) but differ in ordering and performance.
HashSet: Fast, Unordered
Best for: Fast membership testing, no ordering required.
Set<String> uniqueUsers = new HashSet<>();
// O(1) - Add element
uniqueUsers.add("alice");
uniqueUsers.add("bob");
uniqueUsers.add("alice"); // Ignored - already exists
// O(1) - Check membership
boolean hasAlice = uniqueUsers.contains("alice");
// O(1) - Remove
uniqueUsers.remove("bob");
// Unordered iteration
for (String user : uniqueUsers) {
System.out.println(user); // Order not guaranteed
}Performance Characteristics:
- Add: O(1)
- Contains: O(1)
- Remove: O(1)
- Iteration: O(n)
- Ordering: None (unpredictable)
Internal Implementation:
Uses HashMap internally where elements are keys (values are a dummy PRESENT object).
// Simplified HashSet internals
public class HashSet<E> {
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}Important Considerations:
// ✅ DO: Use immutable or effectively immutable objects
Set<String> names = new HashSet<>();
names.add("Alice");
// ❌ DON'T: Modify objects after adding
class Person {
String name;
@Override
public int hashCode() {
return name.hashCode();
}
}
Person p = new Person("Alice");
Set<Person> people = new HashSet<>();
people.add(p);
p.name = "Bob"; // ⚠️ BREAKS SET - can't find 'p' anymore!LinkedHashSet: Ordered by Insertion
Best for: When you need predictable iteration order with Set semantics.
Set<String> orderedUsers = new LinkedHashSet<>();
orderedUsers.add("charlie");
orderedUsers.add("alice");
orderedUsers.add("bob");
// Iteration in insertion order
for (String user : orderedUsers) {
System.out.println(user); // charlie, alice, bob
}Performance Characteristics:
- Same as
HashSet(O(1) for add/contains/remove) - Additional memory: Maintains doubly-linked list for ordering
- Iteration: Predictable insertion order
TreeSet: Sorted Order
Best for: When you need elements in sorted order.
Set<Integer> scores = new TreeSet<>();
scores.add(85);
scores.add(92);
scores.add(78);
// Iteration in sorted order
for (int score : scores) {
System.out.println(score); // 78, 85, 92
}
// Range operations
SortedSet<Integer> highScores = ((TreeSet<Integer>) scores).tailSet(80);
System.out.println(highScores); // [85, 92]Performance Characteristics:
- Add: O(log n)
- Contains: O(log n)
- Remove: O(log n)
- Iteration: O(n), in sorted order
- Implementation: Red-Black Tree
Requirements:
// Elements must be Comparable OR provide Comparator
Set<Person> people = new TreeSet<>(
Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
);Set Comparison Table
| Feature | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| Ordering | None | Insertion | Sorted |
| Add/Contains/Remove | O(1) | O(1) | O(log n) |
| Memory | Low | Medium | Medium |
| Use case | Fast lookup | Predictable order | Sorted data |
Map Interface: Key-Value Pairs
Maps store key-value associations with unique keys.
HashMap: Fast, Unordered
Best for: Fast key-based lookup, no ordering required.
Map<String, Integer> ages = new HashMap<>();
// O(1) - Put key-value
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // Overwrites previous value
// O(1) - Get by key
Integer aliceAge = ages.get("Alice");
// O(1) - Check key existence
boolean hasAlice = ages.containsKey("Alice");
// O(n) - Check value existence (inefficient!)
boolean hasAge30 = ages.containsValue(30);
// O(1) - Remove by key
ages.remove("Bob");
// Iterate over entries
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}Performance Characteristics:
- Put/Get/Remove: O(1) average, O(n) worst case (hash collisions)
- ContainsKey: O(1)
- ContainsValue: O(n)
- Iteration: O(n)
Internal Mechanism:
// Simplified HashMap internals
public class HashMap<K, V> {
transient Node<K, V>[] table; // Array of buckets
static class Node<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next; // Chain for collisions
}
public V put(K key, V value) {
int hash = hash(key);
int bucket = hash & (table.length - 1);
// Handle collision with chaining or tree
// ...
}
}Hash Collision Handling:
- Java 8+: Buckets convert from linked list to red-black tree when chain length exceeds 8
- Load factor: Default 0.75 - resizes when 75% full
- Initial capacity: Default 16, doubles on resize
Best Practices:
// ✅ DO: Specify initial capacity for large maps
Map<String, User> users = new HashMap<>(10000);
// ✅ DO: Use compute methods for atomic updates
map.compute("key", (k, v) -> v == null ? 1 : v + 1);
// ✅ DO: Use getOrDefault to avoid null checks
int count = map.getOrDefault("key", 0);
// ❌ DON'T: Use containsKey + get (two lookups)
if (map.containsKey("key")) {
return map.get("key"); // Second lookup!
}
// ✅ DO: Single lookup
String value = map.get("key");
if (value != null) {
return value;
}Modern Java 8+ Methods:
Map<String, Integer> scores = new HashMap<>();
// putIfAbsent - only add if key doesn't exist
scores.putIfAbsent("Alice", 100);
// computeIfAbsent - lazy initialization
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("admin", k -> new ArrayList<>()).add("Alice");
// merge - combine values
scores.merge("Bob", 10, Integer::sum); // Adds 10 to existing valueLinkedHashMap: Insertion-Ordered Map
Best for: When you need predictable iteration order (insertion or access order).
Map<String, Integer> cache = new LinkedHashMap<>();
cache.put("first", 1);
cache.put("second", 2);
cache.put("third", 3);
// Iteration in insertion order
cache.forEach((k, v) -> System.out.println(k + ": " + v));
// Output: first: 1, second: 2, third: 3LRU Cache Implementation:
// Access-ordered LinkedHashMap (LRU cache)
Map<String, String> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > MAX_ENTRIES;
}
};
lruCache.put("A", "1");
lruCache.put("B", "2");
lruCache.put("C", "3");
lruCache.get("A"); // Moves A to end (most recently used)
lruCache.put("D", "4"); // Evicts B (least recently used)TreeMap: Sorted Map
Best for: When you need keys in sorted order or range queries.
TreeMap<Integer, String> sortedMap = new TreeMap<>();
sortedMap.put(3, "three");
sortedMap.put(1, "one");
sortedMap.put(2, "two");
// Iteration in key order
sortedMap.forEach((k, v) -> System.out.println(k + ": " + v));
// Output: 1: one, 2: two, 3: three
// Range queries
SortedMap<Integer, String> subMap = sortedMap.subMap(1, 3); // [1, 3)
Map.Entry<Integer, String> firstEntry = sortedMap.firstEntry();
Map.Entry<Integer, String> lastEntry = sortedMap.lastEntry();
// Ceiling/Floor operations
Integer ceiling = sortedMap.ceilingKey(2); // Smallest key >= 2
Integer floor = sortedMap.floorKey(2); // Largest key <= 2Performance Characteristics:
- Put/Get/Remove: O(log n)
- Iteration: O(n), in sorted order
- Range operations: O(log n + m) where m is result size
ConcurrentHashMap: Thread-Safe Map
Best for: High-concurrency scenarios with many threads.
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// Thread-safe operations without external synchronization
concurrentMap.put("key", 1);
concurrentMap.compute("key", (k, v) -> v == null ? 1 : v + 1);
// Bulk operations
concurrentMap.forEach(10, (k, v) -> {
// Parallel forEach with parallelism threshold
System.out.println(k + ": " + v);
});
// Atomic updates
concurrentMap.computeIfAbsent("counter", k -> 0);
concurrentMap.computeIfPresent("counter", (k, v) -> v + 1);Key Differences from HashMap:
- Lock-free reads: Most get operations don't require locks
- Fine-grained locking: Writes lock only specific segments
- Weak consistency: Iterators reflect state at some point, may not see concurrent updates
- No null keys/values: Unlike
HashMap, nulls are not allowed
Performance:
// ConcurrentHashMap scales better with threads
// Single thread: HashMap ~= ConcurrentHashMap
// Multiple threads: ConcurrentHashMap >> Collections.synchronizedMap(HashMap)Map Comparison Table
| Feature | HashMap | LinkedHashMap | TreeMap | ConcurrentHashMap |
|---|---|---|---|---|
| Ordering | None | Insertion/Access | Sorted | None |
| Get/Put | O(1) | O(1) | O(log n) | O(1) |
| Thread-safe | ❌ | ❌ | ❌ | ✅ |
| Null keys | ✅ (one) | ✅ (one) | ❌ | ❌ |
| Use case | General | LRU cache | Sorted keys | Concurrent |
Queue Interface: FIFO and Priority
Queues process elements in specific order (FIFO, priority, etc.).
ArrayDeque: Double-Ended Queue
Best for: Queue or stack operations (faster than LinkedList and Stack).
Deque<String> deque = new ArrayDeque<>();
// Use as Queue (FIFO)
deque.offer("first");
deque.offer("second");
String first = deque.poll(); // "first"
// Use as Stack (LIFO)
deque.push("A");
deque.push("B");
String top = deque.pop(); // "B"
// Double-ended operations
deque.addFirst("head");
deque.addLast("tail");
String head = deque.removeFirst();
String tail = deque.removeLast();Performance Characteristics:
- Add/Remove at either end: O(1) amortized
- Random access: Not supported (not intended use)
- Memory: More efficient than
LinkedList(no node overhead)
Why ArrayDeque over LinkedList?
// ArrayDeque advantages:
// 1. Lower memory overhead (no node pointers)
// 2. Better cache locality (contiguous array)
// 3. Faster insertions/removals at ends
Deque<Integer> deque = new ArrayDeque<>(); // ✅ Preferred
LinkedList<Integer> list = new LinkedList<>(); // ❌ Less efficient as dequePriorityQueue: Heap-Based Priority Queue
Best for: Processing elements by priority (not insertion order).
// Min-heap by default (smallest element first)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(1);
minHeap.offer(3);
System.out.println(minHeap.poll()); // 1
System.out.println(minHeap.poll()); // 3
System.out.println(minHeap.poll()); // 5
// Max-heap with custom comparator
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(
Comparator.reverseOrder()
);
// Custom objects with priority
class Task implements Comparable<Task> {
String name;
int priority;
@Override
public int compareTo(Task other) {
return Integer.compare(this.priority, other.priority);
}
}
PriorityQueue<Task> taskQueue = new PriorityQueue<>();
taskQueue.offer(new Task("Low", 3));
taskQueue.offer(new Task("High", 1));
taskQueue.offer(new Task("Medium", 2));
Task urgent = taskQueue.poll(); // Returns "High" (priority 1)Performance Characteristics:
- Offer (add): O(log n)
- Poll (remove): O(log n)
- Peek: O(1)
- Implementation: Binary heap (array-based)
Important Notes:
// ❌ PITFALL: Iteration order is NOT priority order!
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(1);
pq.offer(3);
for (Integer i : pq) {
System.out.println(i); // NOT guaranteed to be 1, 3, 5!
}
// ✅ CORRECT: Use poll() for priority order
while (!pq.isEmpty()) {
System.out.println(pq.poll()); // 1, 3, 5 (guaranteed)
}BlockingQueue: Thread-Safe Queues
Best for: Producer-consumer patterns with thread coordination.
import java.util.concurrent.*;
// Bounded queue - blocks when full
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// Producer thread
new Thread(() -> {
try {
queue.put("item"); // Blocks if queue is full
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// Consumer thread
new Thread(() -> {
try {
String item = queue.take(); // Blocks if queue is empty
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();Common Implementations:
- ArrayBlockingQueue: Bounded, array-based
- LinkedBlockingQueue: Optionally bounded, node-based
- PriorityBlockingQueue: Unbounded, priority-ordered
- DelayQueue: Elements available after delay
Performance Comparison: Big O Notation
Time Complexity Summary
| Collection | Add | Remove | Get | Contains | Iteration |
|---|---|---|---|---|---|
| ArrayList | O(1)* | O(n) | O(1) | O(n) | O(n) |
| LinkedList | O(1) | O(1)** | O(n) | O(n) | O(n) |
| HashSet | O(1) | O(1) | - | O(1) | O(n) |
| TreeSet | O(log n) | O(log n) | - | O(log n) | O(n) |
| HashMap | O(1) | O(1) | O(1) | O(1)† | O(n) |
| TreeMap | O(log n) | O(log n) | O(log n) | O(log n)† | O(n) |
| PriorityQueue | O(log n) | O(log n) | O(1)‡ | O(n) | O(n) |
| ArrayDeque | O(1) | O(1) | - | - | O(n) |
*Amortized - Occasionally O(n) when resizing
**At known position - O(n) to find position
†containsKey for maps
‡peek only - not arbitrary get
Space Complexity
| Collection | Space Complexity | Overhead |
|---|---|---|
| ArrayList | O(n) | Low - just array |
| LinkedList | O(n) | High - 2 pointers per node |
| HashSet/HashMap | O(n) | Medium - hash table + chains |
| TreeSet/TreeMap | O(n) | Medium - tree nodes |
Choosing the Right Collection
Decision Tree
Need key-value pairs?
├─ YES → Map
│ ├─ Need sorting? → TreeMap
│ ├─ Need insertion order? → LinkedHashMap
│ ├─ Thread-safe? → ConcurrentHashMap
│ └─ Default → HashMap
│
└─ NO → Collection
├─ Need uniqueness?
│ ├─ YES → Set
│ │ ├─ Need sorting? → TreeSet
│ │ ├─ Need insertion order? → LinkedHashSet
│ │ └─ Default → HashSet
│ │
│ └─ NO → List or Queue
│ ├─ Need FIFO/LIFO? → Queue
│ │ ├─ Priority-based? → PriorityQueue
│ │ ├─ Thread-safe? → BlockingQueue
│ │ └─ Default → ArrayDeque
│ │
│ └─ Ordered collection? → List
│ ├─ Frequent random access? → ArrayList
│ └─ Frequent head/tail ops? → LinkedListUse Case Examples
// 1. User session cache (bounded, access-order)
Map<String, Session> sessions = new LinkedHashMap<>(1000, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<String, Session> eldest) {
return size() > 1000;
}
};
// 2. Unique visitor tracking (fast membership)
Set<String> uniqueVisitors = new HashSet<>();
// 3. Leaderboard (sorted scores)
TreeMap<Integer, String> leaderboard = new TreeMap<>(Comparator.reverseOrder());
// 4. Task queue (priority-based)
PriorityQueue<Task> taskQueue = new PriorityQueue<>(
Comparator.comparing(Task::getPriority)
);
// 5. Event processing (FIFO)
Deque<Event> eventQueue = new ArrayDeque<>();
// 6. Thread-safe counter
Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
counters.computeIfAbsent("requests", k -> new AtomicInteger()).incrementAndGet();
// 7. Sorted time-series data
TreeMap<Instant, Double> timeSeries = new TreeMap<>();
SortedMap<Instant, Double> lastHour = timeSeries.tailMap(
Instant.now().minus(1, ChronoUnit.HOURS)
);
// 8. Batch processing (ArrayList for iteration performance)
List<Record> batch = new ArrayList<>(1000);
for (Record record : batch) {
process(record); // Fast sequential access
}Best Practices
1. Initialize with Capacity
// ✅ GOOD: Avoid resizing overhead
List<String> items = new ArrayList<>(expectedSize);
Map<String, Integer> map = new HashMap<>(expectedSize);
// ❌ BAD: Multiple resizes as it grows
List<String> items = new ArrayList<>(); // Default 10
for (int i = 0; i < 10000; i++) {
items.add(String.valueOf(i)); // Resizes ~14 times!
}2. Use Interface Types
// ✅ GOOD: Flexible, allows implementation change
List<String> names = new ArrayList<>();
Set<Integer> ids = new HashSet<>();
Map<String, User> users = new HashMap<>();
// ❌ BAD: Tightly coupled to implementation
ArrayList<String> names = new ArrayList<>();
HashSet<Integer> ids = new HashSet<>();3. Fail-Fast vs Fail-Safe Iteration
// ❌ PITFALL: ConcurrentModificationException
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
if (item.equals("B")) {
list.remove(item); // ❌ Throws exception!
}
}
// ✅ SOLUTION 1: Use Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if (item.equals("B")) {
it.remove(); // ✅ Safe
}
}
// ✅ SOLUTION 2: Use removeIf (Java 8+)
list.removeIf(item -> item.equals("B"));
// ✅ SOLUTION 3: Collect and remove (for complex logic)
List<String> toRemove = new ArrayList<>();
for (String item : list) {
if (shouldRemove(item)) {
toRemove.add(item);
}
}
list.removeAll(toRemove);4. Immutable vs Mutable
// ✅ Immutable collections (Java 9+)
List<String> immutable = List.of("A", "B", "C");
Set<Integer> immutableSet = Set.of(1, 2, 3);
Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2);
// Immutable collections:
// - Thread-safe by default
// - No risk of accidental modification
// - More efficient memory (no unused capacity)
// - throw UnsupportedOperationException on modification attempts
// ❌ PITFALL: Collections.unmodifiableList() is just a view
List<String> original = new ArrayList<>(Arrays.asList("A", "B"));
List<String> unmodifiable = Collections.unmodifiableList(original);
original.add("C"); // ⚠️ Also affects unmodifiable!5. Equals and HashCode
// ✅ MUST implement both equals() and hashCode() for hash-based collections
class User {
private String id;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
// ❌ PITFALL: Violating equals/hashCode contract
Set<User> users = new HashSet<>();
users.add(new User("1", "Alice"));
users.contains(new User("1", "Alice")); // May return false without proper equals/hashCode!6. Null Handling
// Collections that allow nulls
List<String> list = new ArrayList<>();
list.add(null); // ✅ OK
Map<String, Integer> map = new HashMap<>();
map.put(null, 1); // ✅ One null key allowed
map.put("key", null); // ✅ Null values allowed
// Collections that DON'T allow nulls
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add(null); // ❌ NullPointerException
TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put(null, 1); // ❌ NullPointerException
ConcurrentHashMap<String, Integer> concurrent = new ConcurrentHashMap<>();
concurrent.put(null, 1); // ❌ NullPointerException
concurrent.put("key", null); // ❌ NullPointerExceptionThread-Safety Considerations
Option 1: Synchronized Wrappers (Legacy)
// ❌ Poor performance - coarse-grained locking
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// Must manually synchronize iteration
synchronized (syncMap) {
for (Map.Entry<String, Integer> entry : syncMap.entrySet()) {
// Process entry
}
}Option 2: Concurrent Collections (Recommended)
// ✅ Better performance - fine-grained locking
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
// No external synchronization needed
concurrentMap.put("key", 1);
cowList.add("item");Option 3: Immutable Collections
// ✅ Thread-safe by nature (cannot be modified)
List<String> immutable = List.of("A", "B", "C");
// Safe to share across threads without synchronizationWhen to Use Each
| Scenario | Solution |
|---|---|
| Rare updates, many reads | CopyOnWriteArrayList/Set |
| Frequent concurrent updates | ConcurrentHashMap |
| Producer-consumer | BlockingQueue |
| Read-only data | Immutable collections |
| Legacy code | Collections.synchronized*() |
Common Pitfalls
1. Array Conversion
List<String> list = Arrays.asList("A", "B", "C");
list.add("D"); // ❌ UnsupportedOperationException - fixed-size!
// ✅ Create mutable copy
List<String> mutableList = new ArrayList<>(Arrays.asList("A", "B", "C"));
mutableList.add("D"); // ✅ OK2. Sublist Modifications
List<String> original = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sublist = original.subList(1, 3); // ["B", "C"] - a view!
sublist.clear(); // Clears sublist
System.out.println(original); // ["A", "D"] - ⚠️ Original modified!3. Type Erasure with Generics
// ❌ Cannot create generic array
List<String>[] arrayOfLists = new ArrayList<String>[10]; // Compile error
// ✅ Use List of Lists instead
List<List<String>> listOfLists = new ArrayList<>();4. Comparator Inconsistency
// ❌ PITFALL: Comparator inconsistent with equals
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
// Compares both name and age
}
}
// TreeSet uses comparator that only compares age
TreeSet<Person> people = new TreeSet<>(Comparator.comparing(Person::getAge));
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 30)); // ⚠️ Not added - same age!
// TreeSet thinks they're equal, but equals() says they're differentPerformance Benchmarks
Microbenchmark results (approximate, JVM-dependent):
ArrayList.get(index): ~2 ns
LinkedList.get(index): ~100 ns (50x slower!)
HashMap.get(key): ~10 ns
TreeMap.get(key): ~30 ns
ConcurrentHashMap.get(key): ~15 ns
ArrayList.add(end): ~5 ns
LinkedList.add(end): ~20 ns
ArrayDeque.add(end): ~5 ns
HashSet.contains(): ~10 ns
TreeSet.contains(): ~30 nsKey Takeaways:
- Array-based structures (ArrayList, ArrayDeque) have best cache locality
- Hash-based structures (HashMap, HashSet) excel at lookups
- Tree-based structures (TreeMap, TreeSet) provide ordering at O(log n) cost
- Linked structures (LinkedList) have overhead from pointer chasing
Conclusion
The Java Collections Framework provides powerful, well-designed data structures for every use case:
- Choose ArrayList for most list needs - fast, efficient, general-purpose
- Choose HashMap for fast key-value lookups without ordering
- Choose HashSet for uniqueness checks and fast membership testing
- Choose TreeMap/TreeSet when you need sorted data or range queries
- Choose ArrayDeque for queue/stack operations (not LinkedList!)
- Choose ConcurrentHashMap for high-concurrency scenarios
- Choose PriorityQueue for priority-based processing
Master these collections, understand their performance characteristics, and your Java applications will be faster, more maintainable, and more robust.
Part of the Java Learning Roadmap series
Back to: Phase 3: Core Java APIs
Related Deep Dives:
- Streams & Functional Programming
- Exception Handling Best Practices
- Java Generics Explained
- Modules and Build Tools
Next Step: Getting Started with Spring Boot
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.