Collections Framework¶
Collections Hierarchy Diagram (Note: Refer to Java documentation for visual collections hierarchy)
- Iterable(I)
- Collection(I)
-
List(I)
- ArrayList(C)
- LinkedList(C)
- Vector(C)
- Stack(C)
-
Queue(I)
- PriorityQueue(C)
- Dque(I)
- ArrayDque(C)
-
Set(I)
- HashSet(C)
-
LinkedHashSet(C)
-
SortedSet(I)
- TreeSet(C)
-
Map(I)
- HashMap(C)
- HashTable(C)
- LinkedHashMap(C) *
- SortedMap(I)
- TreeMap(C)
Collections Utility Class
The java.util.Collections class provides static utility methods for common operations on collections:
- addAll(Collection<? super T> c, T... elements) - Add all elements to collection
- sort(List
list) - Sort list in ascending order (natural ordering) - sort(List
list, Comparator<? super T> c) - Sort with custom comparator - shuffle(List<?> list) - Randomize order of elements
- binarySearch(List<? extends Comparable<? super T>> list, T key) - Search in sorted list
- reverse(List<?> list) - Reverse order
- rotate(List<?> list, int distance) - Rotate elements
- fill(List<? super T> list, T obj) - Fill all positions with same value
- copy(List<? super T> dest, List<? extends T> src) - Copy source to destination
- min(Collection<? extends T> coll) - Find minimum element
- max(Collection<? extends T> coll) - Find maximum element
- disjoint(Collection<?> c1, Collection<?> c2) - Check if collections have no common elements
- frequency(Collection<?> c, Object o) - Count occurrences of element
- replaceAll(List
list, T oldVal, T newVal) - Replace all occurrences
Collections.sort() Algorithm: Uses merge sort (stable, O(n log n) guaranteed)
ArrayList¶
https://www.baeldung.com/java-list-capacity-array-size
When an ArrayList is created in Java, it is backed by an array. Initially, this array has a default capacity, often 10. When elements are added to the ArrayList, they are stored in this underlying array. If the ArrayList grows beyond its current capacity, a new, larger array is created, typically with a size that is 1.5 or 2 times the original size. The elements from the old array are then copied to the new array, and the old array is discarded, a process known as resizing. This ensures that ArrayList can accommodate a dynamic number of elements.
The memory occupied by an ArrayList includes the object header, the size of the internal array, and the memory used by the elements themselves. The header typically takes 12 bytes, and the array size depends on the number of elements and the data type. For example, an ArrayList of integers with 10 elements would require 40 bytes for the elements themselves (4 bytes per integer). When the ArrayList is resized, the memory usage increases to accommodate the new capacity.
ArrayList stores elements contiguously in memory, which allows for efficient access using indices. However, frequent resizing can lead to performance overhead due to the copying of elements. To mitigate this, it is often beneficial to initialize an ArrayList with an appropriate initial capacity if the number of elements is known in advance.
Hashmap¶
https://anmolsehgal.medium.com/java-hashmap-internal-implementation-21597e1efec3 https://www.geeksforgeeks.org/internal-working-of-hashmap-java/
In Java hashing, several key concepts govern the performance and behavior of hash-based data structures like HashMap. These concepts include capacity, threshold, rehashing, and collision.
Capacity: The initial size of the internal array (bucket array) in a HashMap. The default capacity is typically 16, but it can be specified during HashMap instantiation.
Threshold: A value that triggers the rehashing process. It's calculated as the product of the capacity and the load factor (default 0.75). When the number of entries in the HashMap exceeds the threshold, the HashMap resizes. For example, with a capacity of 16 and a load factor of 0.75, the threshold is 12.
Rehashing: The process of resizing the internal array of a HashMap when the number of entries exceeds the threshold. It involves creating a new array with double the capacity, recalculating the hash codes for existing entries, and redistributing them into the new array. This ensures efficient retrieval times as the HashMap grows.
Collision: Occurs when two or more keys produce the same hash code, resulting in them mapping to the same index in the internal array. HashMap handles collisions using techniques like separate chaining (using linked lists or trees) to store multiple entries at the same index.
Load Factor¶
In Java, the load factor is a crucial parameter associated with hash-based data structures like HashMap and HashSet. It determines when the data structure's internal capacity should be increased (rehashing). The load factor is a number between 0 and 1 (inclusive), with a default value of 0.75 in Java.
It represents the ratio of the number of elements stored in the hash table to the total number of buckets (capacity). When the load factor is reached, the capacity of the hash table is doubled, and all existing elements are rehashed into the new buckets. This process helps maintain the efficiency of the hash table's operations, such as insertion, deletion, and retrieval.
A lower load factor means that the hash table will be resized more frequently, leading to less collision and faster access times but also higher memory consumption. Conversely, a higher load factor means that the hash table will be resized less frequently, saving memory but potentially increasing collision and slowing down operations. The default value of 0.75 is generally considered a good balance between time and space efficiency.
List
- The List is an indexed sequence. - maintain order
- List allows duplicate elements
- Elements by their position can be accessed
- Multiple null elements can be stored.
- Implementations are ArrayList, LinkedList, Vector, Stack
Set
- The Set is an non-indexed sequence. - does not maintain order
- Set doesn’t allow duplicate elements.
- Position access to elements is not allowed.
- Null element can store only once.
- Implementations are HashSet, LinkedHashSet.
HashMap - Key Characteristics
| Property | Detail |
|---|---|
| Hashing | Uses hash codes to map keys to bucket positions |
| Collisions | Handled via chaining (linked list) or tree (since Java 8) |
| Bucket Conversion | When chain length > 8, converts to Red-Black tree for O(log n) |
| Thread Safety | Not synchronized (use ConcurrentHashMap or Collections.synchronizedMap) |
| Null Keys | Allows one null key |
| Null Values | Allows multiple null values |
| Iteration Order | No guaranteed order (insertion order: LinkedHashMap; sorted: TreeMap) |
| Performance | O(1) average case, O(n) worst case (hash collision) |
HashMap vs ConcurrentHashMap vs Hashtable: - HashMap: Fast, not thread-safe, preferred for single-threaded - ConcurrentHashMap: Thread-safe with segment-level locking, best for multi-threaded - Hashtable: Legacy, fully synchronized (slow), avoid in modern code
ConcurrentHashMap: Thread-Safe HashMap¶
ConcurrentHashMap provides thread-safe operations without locking the entire map. Uses segment-level locking (or bucket-level in Java 8+).
Internal Structure (Java 8+)¶
ConcurrentHashMap segments the map into buckets:
HashMap (single lock - blocks entire map):
[===LOCK===]
[Bucket 0] [Bucket 1] [Bucket 2] [Bucket 3] [Bucket 4]
ConcurrentHashMap (locks per bucket - allows concurrent access):
[LOCK A] [LOCK B] [LOCK C] [LOCK D] [LOCK E]
Bucket 0 Bucket 1 Bucket 2 Bucket 3 Bucket 4
Multiple threads can read/write different buckets simultaneously!
Basic Usage¶
// Create ConcurrentHashMap
Map<String, Integer> map = new ConcurrentHashMap<>(16, 0.75f, 4);
// Capacity: 16, Load Factor: 0.75, Concurrency Level: 4
map.put("key1", 100);
map.put("key2", 200);
Integer value = map.get("key1"); // 100
map.remove("key2");
map.replace("key1", 150);
// Iteration is safe (doesn't require external synchronization)
for (String key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
Atomic Operations¶
// putIfAbsent: Set only if key doesn't exist
map.putIfAbsent("counter", 0); // Returns 0
// replace: Update if value matches
map.replace("key1", 100, 200); // Update 100 to 200 if key1 == 100
// getOrDefault: Get with fallback
Integer count = map.getOrDefault("missing", -1); // Returns -1
// computeIfPresent: Conditional update
map.computeIfPresent("key1", (k, v) -> v + 10); // Add 10 to existing
// computeIfAbsent: Conditional insert
map.computeIfAbsent("key2", k -> 999); // Set to 999 if absent
Practical: Thread-Safe Counter¶
// ❌ Bad: Not thread-safe
Map<String, Integer> badCounter = new HashMap<>();
badCounter.put("count", 0);
// Multiple threads increment (race condition!)
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
Integer val = badCounter.get("count");
badCounter.put("count", val + 1); // NOT atomic!
}).start();
}
// Final value may be < 1000 due to race condition
// ✅ Good: ConcurrentHashMap
Map<String, Integer> safeCounter = new ConcurrentHashMap<>();
safeCounter.put("count", 0);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
safeCounter.compute("count", (k, v) -> (v == null ? 0 : v) + 1);
}).start();
}
// Final value will be exactly 1000 (atomic operation)
Performance Comparison¶
// Benchmark: 4 threads, 10k operations each
// HashMap + synchronized (serialize all access)
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// Time: ~500ms (bottleneck: single lock)
// ConcurrentHashMap (parallel access)
Map<String, Integer> concMap = new ConcurrentHashMap<>();
// Time: ~100ms (4× faster - less contention)
// Under high contention:
// - HashMap: All threads wait for same lock (serial)
// - ConcurrentHashMap: Threads access different buckets (parallel)
ConcurrentHashMap vs Synchronized HashMap¶
| Aspect | SynchronizedMap | ConcurrentHashMap |
|---|---|---|
| Locking | Single lock on entire map | Segment/bucket-level locks |
| Concurrency | Only 1 thread at a time | Multiple threads in parallel |
| Performance | Slower under contention | Fast under contention |
| Iteration | Requires external sync | Safe (weakly consistent) |
| null keys/values | Allows | ❌ Rejects |
| Best for | Rare writes, frequent reads (low contention) | Frequent concurrent access |
Iteration (Weakly Consistent)¶
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Safe to iterate while others modify
// (May see partial updates, but won't throw ConcurrentModificationException)
new Thread(() -> {
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
}).start();
Thread.sleep(100);
for (String key : map.keySet()) {
System.out.println(key); // May see 1, 2, or 3 keys (not synchronized)
}
Example: Cache with ConcurrentHashMap¶
public class Cache<K, V> {
private final Map<K, V> data = new ConcurrentHashMap<>();
private final Map<K, Long> timestamps = new ConcurrentHashMap<>();
private final long ttlMillis;
public Cache(long ttlMillis) {
this.ttlMillis = ttlMillis;
}
public void put(K key, V value) {
data.put(key, value);
timestamps.put(key, System.currentTimeMillis());
}
public V get(K key) {
Long timestamp = timestamps.get(key);
if (timestamp == null) return null;
// Check if expired
if (System.currentTimeMillis() - timestamp > ttlMillis) {
data.remove(key);
timestamps.remove(key);
return null;
}
return data.get(key);
}
public int size() {
return data.size();
}
}
// Usage: Multiple threads can safely access concurrently
Cache<String, User> userCache = new Cache<>(3600000); // 1 hour TTL
for (int i = 0; i < 100; i++) {
final int userId = i;
new Thread(() -> {
userCache.put("user:" + userId, new User(userId, "User" + userId));
User user = userCache.get("user:" + userId);
}).start();
}
List vs Set - Comparison
| Feature | List | Set |
|---|---|---|
| Order | ✅ Maintains insertion order | ❌ No order guarantee (except LinkedHashSet, TreeSet) |
| Duplicates | ✅ Allows duplicates | ❌ No duplicates |
| Indexing | ✅ Access by index (get, set) | ❌ No index-based access |
| Null | ✅ Multiple nulls allowed | ⚠️ One null (only HashSet/LinkedHashSet) |
| Common Impls | ArrayList, LinkedList, Vector | HashSet, LinkedHashSet, TreeSet |
| Use Case | Maintaining sequence | Unique values, membership checking |
| Performance | ArrayList: O(1) access, O(n) insert | HashSet: O(1) average, O(n) worst |
When to use: - List: You need sequence order or indexed access (users in order, student records) - Set: You need uniqueness and fast lookup (unique emails, tags, categories)
Stack and queue
Hash Collision Binary tree - use cases Heap DS
clob , blob Hashing tokenization