Map và Set — HashMap, TreeMap, HashSet bên dưới
HashMap bucket + chaining + treeify (JEP 180), TreeMap red-black tree, HashSet/LinkedHashSet/TreeSet wrap Map với dummy value. LinkedHashMap cho LRU cache. Load factor, resize, và vai trò equals/hashCode.
Bài trước (8.4) đi qua List và Queue. Bài này phần còn lại của Collections framework: Map và Set — các implementation key-value và tập hợp không trùng.
Điểm thú vị: HashSet không có code logic riêng — nó chỉ là HashMap với value dummy. Cùng cách, LinkedHashSet = LinkedHashMap, TreeSet = TreeMap. Hiểu 3 Map (HashMap, TreeMap, LinkedHashMap) là hiểu luôn 3 Set.
Bài này đi sâu vào:
- HashMap — bucket array, hash function, chaining collision, treeify Java 8+.
- TreeMap — red-black tree balanced.
- LinkedHashMap — HashMap + linked list cho iteration order. Nền tảng LRU cache.
- HashSet/TreeSet/LinkedHashSet — wrap Map.
1. Map — không extends Collection
flowchart TD
Map[Map - khong extends Collection]
Map --> SortedMap
SortedMap --> NavigableMap
Map -.-> HashMap
Map -.-> LinkedHashMap
NavigableMap -.-> TreeMapMap không extends Collection vì semantic khác: Map lưu cặp key-value, không phải tập element. Method signature cũng khác (put(k, v) thay vì add(e)).
3 implementation chính:
HashMap: O(1) trung bình, không sắp xếp.TreeMap: O(log n), sorted theo key.LinkedHashMap: O(1) + giữ thứ tự insert (hoặc access).
2. HashMap — hash table với bucket + treeify
Cấu trúc bên dưới (Java 8+)
public class HashMap<K, V> {
Node<K, V>[] table; // Array cua bucket
int size;
int threshold; // Size trigger resize
float loadFactor; // Mac dinh 0.75
static class Node<K, V> {
int hash;
K key;
V value;
Node<K, V> next; // Linked list cho collision
}
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
}
Cơ chế hash → bucket
public V put(K key, V value) {
int hash = hash(key); // Modify hashCode() de tranh cluster
int index = (table.length - 1) & hash; // Chi so bucket
Node<K, V> bucket = table[index];
// ... them vao bucket
}
static int hash(Object key) {
int h = key.hashCode();
return h ^ (h >>> 16); // Mix high bit xuong low bit
}
3 bước:
- Gọi
key.hashCode()→ int 32-bit. - Mix high bit xuống low bit (XOR với
h >>> 16) — giảm clustering với bad hash. & (table.length - 1)— nếutable.lengthlà power of 2, tương đươnghash % table.lengthnhưng nhanh hơn (bit-wise AND).
Kết quả là bucket index trong table array.
Collision — chaining
2 key cùng bucket (hash collision) → lưu trong linked list của bucket:
table[5] -> Node(hash=5, "apple", 100) -> Node(hash=5, "banana", 200) -> null
Tra cứu: hash key → index → duyệt linked list trong bucket, compare bằng equals để tìm key đúng.
Treeify — Java 8 optimization
Khi bucket có trên 8 entry VÀ table size tối thiểu 64, Java 8+ chuyển linked list thành red-black tree:
table[5] -> TreeNode (red-black tree voi 20 entry)
Lookup trong tree: O(log n) thay vì O(n) linked list. Đây là defense chống hash collision DoS attack — kẻ tấn công craft nhiều key có cùng hash → bucket thành linked list dài → lookup worst-case O(n) → server slow xuống. Treeify biến thành O(log n).
CVE-2011-4858 (Java Hashtable) đã ghi nhận attack này. JEP 180 Java 8 fix qua treeify.
Khi tree shrink xuống dưới 6 entry, chuyển ngược về linked list (UNTREEIFY).
Load factor + resize
if (size > threshold) { // threshold = capacity * loadFactor
resize();
}
Load factor mặc định 0.75. Khi size / capacity > 0.75, resize:
- Cấp
tablemới gấp đôi capacity (phải power of 2). - Rehash mọi entry sang bucket mới.
Java 8+ tối ưu: với table.length double, entry hoặc ở vị trí cũ hoặc ở vị trí cũ + oldCapacity — không cần tính lại hash, chỉ check 1 bit.
Resize O(n) — tốn. Biết trước size → init capacity đủ:
Map<String, Integer> m = new HashMap<>(1024);
get(key) — O(1) trung bình
public V get(Object key) {
int hash = hash(key);
int index = (table.length - 1) & hash;
Node<K, V> bucket = table[index];
if (bucket == null) return null;
// Duyet linked list hoac tree
for (Node<K, V> n = bucket; n != null; n = n.next) {
if (n.hash == hash && (n.key == key || key.equals(n.key))) {
return n.value;
}
}
return null;
}
Với hash distribution tốt: ít collision → bucket nhỏ (~1-2 entry) → O(1). Collision nhiều: linked list → O(n). Treeify khi quá đông → O(log n).
Vai trò equals + hashCode
HashMap dùng hashCode để tìm bucket, equals để tìm key trong bucket. Class làm key phải override cả 2:
class Book {
String title;
// khong override
}
Map<Book, String> m = new HashMap<>();
m.put(new Book("Java"), "review 1");
m.get(new Book("Java")); // null! 2 object khac nhau theo Object.equals default
Rule: override hashCode và equals cùng bộ field. Hàm hashCode phải nhất quán với equals — 2 object equal phải có cùng hashCode.
Null key — cho phép 1 null
HashMap cho 1 null key (đặt vào bucket 0). TreeMap không cho (vì cần compareTo).
3. TreeMap — red-black tree, sorted
Cấu trúc
Red-black tree — self-balancing binary search tree với màu node (red/black) đảm bảo height tối đa 2*log(n+1).
public class TreeMap<K, V> {
Entry<K, V> root;
int size;
Comparator<? super K> comparator;
static final class Entry<K, V> {
K key;
V value;
Entry<K, V> left;
Entry<K, V> right;
Entry<K, V> parent;
boolean color; // RED or BLACK
}
}
Rule red-black tree
- Mỗi node màu red hoặc black.
- Root luôn black.
- Leaf (null) là black.
- Red node không có child red (2 red liên tiếp cấm).
- Từ mỗi node, số black node trên đường đi xuống leaf bằng nhau.
5 rule đảm bảo tree balanced — height chỉ tăng log với n. Insert/remove giữ rule bằng rotation (left/right) và recoloring.
Insert — O(log n)
- BST insert bình thường (so compareTo để đi left/right).
- Color node mới red.
- Fix-up: nếu vi phạm rule 4 (2 red liên tiếp), rotate + recolor.
Fix-up đi từ node mới lên root, mỗi bước O(1), tổng O(log n).
Lookup — O(log n)
BST lookup: compare key với root, đi left nếu nhỏ, right nếu lớn. Tree balanced → height log n.
Ưu điểm TreeMap
- Sorted iteration:
for (key : map.keySet())theo thứ tự compareTo. - Range query:
subMap(from, to),headMap(to),tailMap(from)— O(log n + k) với k = kết quả. - Navigation:
ceilingKey(x)(smallest key ≥ x),floorKey(x)(largest key ≤ x),higherKey,lowerKey— O(log n). - Worst-case ổn định: luôn O(log n), không có worst-case O(n) như HashMap bad hash.
Nhược
- Chậm hơn HashMap cho lookup thuần:
O(log n)vsO(1). Với n = 1M, log n = 20, chênh 20x. - Key phải Comparable hoặc pass Comparator.
- Không cho null key (compareTo throw NPE).
Khi nào TreeMap
- Cần iterate theo thứ tự (sorted).
- Range query.
- Time-series data (lookup theo timestamp gần nhất).
- Pricing tier (lookup bậc giá theo số tiền).
Khi nào HashMap: lookup thuần, không quan tâm thứ tự — mặc định HashMap.
4. LinkedHashMap — HashMap + linked list
LinkedHashMap extend HashMap với thêm doubly-linked list kết nối entry theo thứ tự insert (hoặc access):
public class LinkedHashMap<K, V> extends HashMap<K, V> {
// Them linked list kieu prev/next noi cac entry
// Duyet theo linked list nay cho iteration order
}
Mỗi entry thêm 2 pointer prev/next → overhead memory nhưng bảo toàn thứ tự.
Insertion order (default)
LinkedHashMap<String, Integer> m = new LinkedHashMap<>();
m.put("c", 3);
m.put("a", 1);
m.put("b", 2);
m.forEach((k, v) -> System.out.println(k));
// Output: c, a, b (thu tu insert)
Khác với HashMap (order tuỳ hash), LinkedHashMap đảm bảo iterate theo thứ tự insert.
Access order — LRU cache base
Constructor 3-arg với accessOrder = true — mỗi get di chuyển entry xuống cuối linked list (most-recently-used):
Map<String, Data> cache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
return size() > 100;
}
};
accessOrder = true— access (get) update thứ tự.removeEldestEntry— callback sau mỗiput. Return true → remove eldest (đầu linked list = least recently used).
Pattern LRU cache trong 5 dòng. Mini-challenge bài sau xây LRU cache đầy đủ từ pattern này.
5. HashSet / LinkedHashSet / TreeSet — wrap Map
HashSet = HashMap với value dummy
public class HashSet<E> {
private HashMap<E, Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
public boolean contains(Object o) {
return map.containsKey(o);
}
}
Đơn giản là HashMap với value hằng số PRESENT. Tất cả đặc tính của HashSet (O(1), cần equals/hashCode) đều kế thừa từ HashMap.
LinkedHashSet = LinkedHashMap với value dummy
Giữ thứ tự insert. Dùng cho Set cần iterate deterministic.
TreeSet = TreeMap với value dummy
Sorted Set. Range query, navigation như TreeMap.
Bảng so sánh:
HashSet | LinkedHashSet | TreeSet | |
|---|---|---|---|
| Thứ tự iterate | Không xác định | Thứ tự insert | Sorted theo compareTo |
add, contains, remove | O(1) | O(1) | O(log n) |
| Null element | ✅ | ✅ | ❌ |
| Bên dưới | HashMap | LinkedHashMap | TreeMap |
6. Immutable collection (Java 9+)
List<Integer> list = List.of(1, 2, 3);
Set<String> set = Set.of("a", "b", "c");
Map<String, Integer> map = Map.of("a", 1, "b", 2);
- Immutable — add/remove ném
UnsupportedOperationException. - Không cho phép null.
- Thread-safe (không đổi được).
- Implementation đặc biệt (
ImmutableCollections.ListN,SetN,MapN) — tối ưu hơn ArrayList/HashMap cho size nhỏ.
List.copyOf, Set.copyOf, Map.copyOf — snapshot
List<Integer> mutable = new ArrayList<>(List.of(1, 2, 3));
List<Integer> snapshot = List.copyOf(mutable);
mutable.add(4);
// snapshot van la [1, 2, 3]
Pattern defensive copy — bảo vệ internal state khỏi mutation từ caller.
7. Bảng chọn
| Use case | Chọn | Lý do |
|---|---|---|
| Key-value fast lookup | HashMap | O(1) |
| Key-value giữ thứ tự insert | LinkedHashMap | O(1) + linked list |
| Key-value sorted theo key | TreeMap | O(log n), range query |
| LRU cache | LinkedHashMap(accessOrder=true) | Built-in access order |
| Không trùng, fast lookup | HashSet | O(1) contains |
| Không trùng, giữ thứ tự insert | LinkedHashSet | O(1) + order |
| Không trùng, sorted | TreeSet | O(log n), range query |
| Constant / immutable | Map.of, Set.of | Thread-safe, memory efficient |
| Thread-safe map | ConcurrentHashMap (bài 10.4) | Lock striping |
8. Pitfall tổng hợp
❌ Nhầm 1: Không override equals/hashCode cho class làm key HashMap / element HashSet.
class Book { String title; }
Set<Book> s = new HashSet<>();
s.add(new Book("Java"));
s.contains(new Book("Java")); // false!
✅ Luôn override cả 2 với cùng bộ field.
❌ Nhầm 2: Không init capacity cho HashMap biết trước size.
Map<K, V> m = new HashMap<>();
for (int i = 0; i < 1_000_000; i++) m.put(keys[i], values[i]); // resize ~17 lan
✅ new HashMap<>(1_500_000) — tránh resize, tăng speed 2-3x.
❌ Nhầm 3: ConcurrentModificationException khi modify trong loop.
for (var e : map.entrySet()) {
if (bad(e.getKey())) map.remove(e.getKey()); // CME
}
✅ map.entrySet().removeIf(e -> bad(e.getKey()));
❌ Nhầm 4: Tưởng HashMap.keySet() iterate theo insert order.
Map<String, Integer> m = new HashMap<>();
m.put("a", 1); m.put("b", 2);
for (String k : m.keySet()) { ... } // Thu tu KHONG xac dinh
✅ Cần order → LinkedHashMap.
❌ Nhầm 5: TreeMap với key null.
TreeMap<String, Integer> m = new TreeMap<>();
m.put(null, 1); // NullPointerException
✅ HashMap cho null; TreeMap không cho.
9. 📚 Deep Dive Oracle
Spec / reference chính thức:
- HashMap javadoc — treeify threshold, load factor, cơ chế resize.
- TreeMap javadoc — red-black tree.
- LinkedHashMap javadoc — removeEldestEntry cho LRU.
- JEP 180: Handle Frequent HashMap Collisions with Balanced Trees — motivation + impl treeify Java 8.
- OpenJDK HashMap source — đọc trực tiếp impl.
Ghi chú: Đọc source OpenJDK HashMap là bài tập đáng thời gian — comment giải thích từng design choice (vì sao load factor 0.75, vì sao treeify threshold 8, tại sao resize double). Hiểu từ source bạn biết khi nào tinh chỉnh param constructor có lợi.
10. Tóm tắt
HashMap: bucket array + chaining, treeify collision vượt 8 (Java 8+). O(1) trung bình. Cầnequals/hashCodecho key.TreeMap: red-black tree balanced, O(log n), sorted iterate + range query.LinkedHashMap: HashMap + linked list insertion/access order.accessOrder=truelàm LRU cache.HashSet/LinkedHashSet/TreeSet= wrap Map tương ứng với value dummyPRESENT.HashMapcho 1 null key;TreeMapkhông cho.new HashMap<>(n)biết trước size → tránh resize multiple.- Pattern
removeEldestEntry+accessOrder=true→ LRU cache builtin. - Immutable
Map.of,Set.of— thread-safe + optimized implementation.
11. Tự kiểm tra
Q1Vì sao HashMap Java 8+ "treeify" bucket thành red-black tree khi quá 8 entry?▸
Trước Java 8, bucket là linked list thuần. Nếu hash collision nhiều → bucket dài → lookup worst-case O(n).
Vấn đề: attacker có thể craft hàng nghìn key có cùng hashCode (vd tận dụng weakness của hash function của String) → submit vào server (form input, JSON key) → HashMap internal degrade thành linked list O(n) → server slow. Gọi là hash collision DoS attack. CVE-2011-4858 (Java Hashtable) đã ghi nhận.
Java 8 fix: khi bucket vượt 8 entry (threshold) VÀ table size tối thiểu 64, JVM chuyển linked list thành red-black tree. Lookup O(n) → O(log n).
Tại sao threshold 8? Thống kê: với hash tốt, xác suất bucket đạt k entry theo Poisson distribution — k=8 xác suất ~10^-6. Rất hiếm trong workload bình thường. Treeify chỉ trigger khi bất thường.
Shrink threshold 6 (UNTREEIFY) — hysteresis tránh thrashing.
Q2Đoạn sau khởi tạo HashMap chứa 1 triệu entry. Tối ưu bằng cách nào?Map<String, Integer> m = new HashMap<>();
for (int i = 0; i < 1_000_000; i++) {
m.put(keys[i], values[i]);
}
▸
Map<String, Integer> m = new HashMap<>();
for (int i = 0; i < 1_000_000; i++) {
m.put(keys[i], values[i]);
}Default capacity 16, load factor 0.75. Với 1M entry, table phải resize nhiều lần: 16 → 32 → 64 → ... → 2^21. Tổng ~17 lần resize. Mỗi resize rehash toàn bộ entry → O(n).
Fix: init capacity đủ:
Map<String, Integer> m = new HashMap<>(1_500_000);Công thức: initialCapacity > expectedSize / loadFactor. Safe: expectedSize * 2.
Benchmark thực tế: 1M insert với default ~500ms, với pre-sized ~200ms. 2.5x speedup.
Quan trọng cho code cold-start, batch load, cache warm-up.
Q3Khi nào chọn TreeMap thay HashMap?▸
TreeMap thay HashMap?TreeMap: red-black tree. HashMap: hash table. Chọn TreeMap khi:
- Cần key sorted: iterate theo thứ tự.
- Range query:
subMap(from, to),headMap,tailMap,ceilingKey,floorKey. Hữu ích cho time-series (tìm entry gần timestamp nhất), pricing tier (lookup bậc giá theo số tiền). - Ổn định worst-case: O(log n) đảm bảo.
Chọn HashMap (mặc định):
- Chỉ cần lookup/insert/remove — O(1) trung bình.
- Không cần thứ tự iterate.
- Không cần range query.
Performance: HashMap get/put ~10-50ns, TreeMap ~100-500ns tuỳ size. Hầu hết app không thấy khác biệt, hot loop 1M ops mới đáng cân nhắc.
Q4Đoạn sau in gì, và tại sao?Map<String, Integer> m = new HashMap<>();
m.put("a", 1); m.put("b", 2); m.put("c", 3);
for (var e : m.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}
▸
Map<String, Integer> m = new HashMap<>();
m.put("a", 1); m.put("b", 2); m.put("c", 3);
for (var e : m.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}In 3 entry nhưng thứ tự không xác định — có thể a=1, b=2, c=3 hoặc c=3, a=1, b=2 hoặc khác, phụ thuộc hash của key và table capacity.
HashMap lưu entry trong bucket theo (hash(key) & (table.length - 1)). Iterate duyệt từng bucket từ index 0 → end. Thứ tự phụ thuộc:
- Hash function của String (có thể đổi giữa Java version).
- Table capacity (đổi khi resize).
Muốn thứ tự: LinkedHashMap (insert order) hoặc TreeMap (sorted).
Bug thường gặp: test local pass, assertion dựa vào thứ tự; deploy prod JDK khác → order đổi → test fail. Không bao giờ assume thứ tự HashMap.
Q5Làm sao build LRU cache 100 entry với LinkedHashMap?▸
LinkedHashMap?Map<String, Data> cache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
return size() > 100;
}
};Cơ chế:
accessOrder = true: mỗigetdi chuyển entry xuống cuối linked list (most-recently-used). Đầu list = least-recently-used.removeEldestEntry: callback sau mỗiput. Return true → JVM remove eldest (đầu list). Return false → giữ.
Khi cache đạt 101 entry sau put, removeEldestEntry return true → eldest bị evict. Cache luôn ≤ 100.
Pattern này base cho mini-challenge LRU cache bài sau — xây wrapper class với thread-safety.
Production thường dùng Caffeine library — tối ưu concurrency, có TTL, metrics. LinkedHashMap approach phù hợp đơn giản, single-thread hoặc externally synchronized.
Bài tiếp theo: Mini-challenge: LRU Cache với LinkedHashMap
Bài này có giúp bạn hiểu bản chất không?