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 — List và Queue đi qua các implementation tuyến tính. 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
🎯 Điều kiện kép để chuyển sang Cây đỏ đen (Red-Black Tree)
Không phải cứ có một bucket bất kỳ đạt 8 phần tử là HashMap sẽ lập tức chuyển thành cây. Java 8+ quy định một cơ chế kiểm tra kép cực kỳ khắt khe:
TREEIFY_THRESHOLD = 8: Số lượng node (độ dài linked list) trong một bucket đơn lẻ phải đạt tối thiểu là 8 phần tử.MIN_TREEIFY_CAPACITY = 64: Tổng số lượng bucket (capacity của mảngtable) phải đạt tối thiểu là 64.
💡 Tại sao cần điều kiện MIN_TREEIFY_CAPACITY = 64?
Nếu tổng số lượng bucket của map còn quá nhỏ (dưới 64, ví dụ như default ban đầu là 16) nhưng đã có một bucket bị nghẽn vượt ngưỡng 8 phần tử, nguyên nhân cao là do phân phối hash chưa tốt hoặc kích thước mảng nhỏ làm gia tăng xung đột.
Lúc này, thay vì lập tức gánh chịu chi phí bộ nhớ đắt đỏ và thuật toán phức tạp để biến linked list thành cây đỏ đen, HashMap sẽ ưu tiên giải pháp resize() (nhân đôi kích thước mảng bucket). Việc resize mảng bucket sẽ giúp:
- Tăng số lượng bucket trống.
- Kích hoạt phân phối lại (rehash) các phần tử sang các bucket mới.
- Giải tỏa áp lực collision cho bucket bị nghẽn một cách đồng đều và tự nhiên nhất.
Chỉ khi mảng bucket đã được mở rộng đủ lớn (tối thiểu là 64 bucket) mà tình trạng đụng độ cục bộ tại một bucket vẫn vượt quá ngưỡng 8, chứng tỏ việc resize không còn hiệu quả hoặc chi phí resize quá đắt, HashMap mới chính thức thực hiện chuyển đổi linked list nghẽn đó thành Cây đỏ đen (Red-Black Tree):
table[5] -> TreeNode (cấu trúc Red-Black Tree tự cân bằng)
Lookup trong cây đỏ đen có độ phức tạp là O(log n) thay vì O(n) của linked list nghẽn. Đây là tấm khiên phòng ngự vững chắc chống lại hash collision DoS attack (khi kẻ tấn công cố tình chèn hàng nghìn key được tính toán để đụng độ cùng một mã hash, kéo tụt hiệu năng tra cứu của server xuống O(n)).
Ghi chú lịch sử: Lỗ hổng bảo mật này được ghi nhận là JEP 180 và CVE-2011-4858 đối với các cấu trúc dạng Hashtable cũ.
Khi số lượng phần tử trong cây đỏ đen giảm xuống còn 6 phần tử (UNTREEIFY_THRESHOLD = 6), JVM sẽ chuyển đổi ngược cấu trúc cây thành linked list đơn giản để tối ưu hiệu năng bộ nhớ (tránh overhead quản lý cây). Khoảng cách giữa 8 và 6 (hysteresis) giúp ngăn hiện tượng đổi cấu trúc liên tục (thrashing) khi thêm/xoá liên tục quanh ngưỡng.
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
}
}
Quy luật tự cân bằng của Cây đỏ đen (Red-Black Tree)
Đối với người học Java thực tế, thay vì phải ghi nhớ máy móc cả 5 quy tắc học thuật phức tạp của Red-Black Tree (các quy luật về màu sắc nút, gốc đen, lá đen, không có hai nút đỏ kề nhau và số lượng nút đen bằng nhau trên mọi đường đi), chúng ta chỉ cần hiểu trade-off (sự đánh đổi) thực tế cực kỳ rõ ràng của nó:
- Ý nghĩa cốt lõi: Các quy tắc màu sắc và cân bằng này là công cụ giúp
TreeMapluôn giữ cho chiều cao của cây nhị phân tăng trưởng theo hàm logarithmic so với số lượng phần tử (chiều cao cây H ≈ log N). - Cam kết tuyệt đối: Dù bạn có chèn dữ liệu theo thứ tự tăng dần/giảm dần (worst-case của cây nhị phân thông thường biến thành linked list),
TreeMapnhờ các phép xoay cây (rotation) và đổi màu (recoloring) sẽ tự cân bằng lại, đảm bảo O(log N) tuyệt đối cho mọi thao tác chèn, xóa và tìm kiếm.
⚖️ Sự đánh đổi thực tế (Real-world Trade-off): Có đáng để sử dụng TreeMap?
Mặc dù cung cấp một cấu trúc tự sắp xếp hoàn hảo, TreeMap đi kèm với những cái giá rất đắt về bộ nhớ và tốc độ so với HashMap:
- Overhead bộ nhớ cực lớn: Với
HashMap, mỗi entry chỉ cần giữ tham chiếunextđơn. VớiTreeMap, mỗi node (Entry) phải gánh thêm tới 3 con trỏ tham chiếu (left,right,parent) cùng 1 biếnboolean color. Điều này làm tăng dung lượng lưu trữ trên Heap lên gấp nhiều lần. - Tốc độ chậm hơn HashMap từ 2 - 10 lần:
HashMapthực hiện tìm kiếm trực tiếp bằng chỉ số index của mảng thông qua phép toán hash (O(1)) vô cùng nhanh chóng.TreeMapphải duyệt qua cấu trúc cây (pointer chasing). Việc nhảy từ địa chỉ node này sang node khác rải rác trên Heap gây ra hiện tượng CPU cache miss liên tục. Trong các thao tác lookup đơn giản trên thực tế,TreeMapchạy chậm hơnHashMapkhoảng từ 2 đến 10 lần tùy kích thước.
Insert & Lookup — O(log n)
Mọi thao tác tìm kiếm (get) hay chèn (put) đều hoạt động bằng cách so sánh giá trị Key (compareTo hoặc thông qua Comparator) từ nút gốc (Root) rồi rẽ nhánh sang trái (nhỏ hơn) hoặc sang phải (lớn hơn) cho đến khi tìm thấy đích. Độ phức tạp là O(log N) tương ứng với độ sâu của cây đã được cân bằng.
Ưu điểm vượt trội của TreeMap
Mặc dù chậm hơn, TreeMap là sự lựa chọn duy nhất khi bạn cần:
- Sorted Iteration: Duyệt qua các Key luôn đảm bảo theo thứ tự tự nhiên (Natural Ordering) của Key hoặc Comparator tùy chỉnh.
- Range Queries (Truy vấn theo khoảng): Hỗ trợ cực tốt các hàm
subMap(from, to),headMap(to),tailMap(from)với độ phức tạp chỉ O(log N + k) (k là số kết quả trả về). - Navigation (Tìm kiếm lân cận): Dễ dàng tìm các phần tử gần nhất bằng
ceilingKey(x)(≥ x),floorKey(x)(≤ x),higherKey,lowerKeychỉ trong O(log N). - Worst-case ổn định: Hoàn toàn không sợ bị sụp đổ hiệu năng về O(n) khi gặp bad hash như
HashMap.
Nhược điểm
- Tốc độ và bộ nhớ: Như đã phân tích ở trên (sử dụng nhiều con trỏ, cache miss, JIT khó optimize bằng HashMap).
- 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 của LinkedHashMap với tham số accessOrder = true cho phép mỗi lần gọi get() hoặc put(), entry được truy cập sẽ tự động di chuyển xuống cuối linked list (được xem là Most Recently Used - được truy cập gần đây nhất):
Map<String, Data> cache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
return size() > 100; // Giới hạn kích thước cache là 100
}
};
accessOrder = true— mỗi lần truy cập phần tử sẽ cập nhật lại thứ tự.removeEldestEntry— một callback kích hoạt ngay sau mỗi lần gọiput(). Nếu trả vềtrue, phần tử lâu đời nhất ở đầu linked list (Least Recently Used) sẽ bị loại bỏ khỏi map.
Đây chính là pattern xây dựng LRU Cache tinh gọn trong 5 dòng code.
⚠️ Cảnh báo cực kỳ quan trọng về Thread-Safety trong môi trường Concurrent
Một sai lầm kinh điển dẫn đến crash hệ thống trong production khi làm việc với LRU Cache tự chế này là bỏ qua tính an toàn luồng (Thread-Safety):
LinkedHashMaphoàn toàn không thread-safe: Khi sử dụng đa luồng (concurrent environment), ngay cả các thao tác đọc dữ liệu nhưget(key)cũng thay đổi cấu trúc bên dưới (di chuyển node trong linked list nội bộ để cập nhật vị trí LRU khiaccessOrder = true).- Hậu quả thảm khốc: Nếu nhiều luồng đồng thời gọi
get()hoặcput(), các liên kết con trỏprev/nextcủa linked list sẽ bị phá hỏng (pointers corruption). Hệ quả thường gặp là:- Treo CPU 100% do rơi vào vòng lặp vô hạn ngầm (
infinite loop) khi duyệt danh sách liên kết bị lỗi vòng tròn. - Dữ liệu bị ghi đè, sai lệch hoặc ném lỗi
ConcurrentModificationException.
- Treo CPU 100% do rơi vào vòng lặp vô hạn ngầm (
🛠️ Giải pháp xử lý an toàn luồng:
-
Phương án 1: Bọc đồng bộ bằng
Collections.synchronizedMapMap<String, Data> syncCache = Collections.synchronizedMap( new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) { return size() > 100; } } );Lưu ý: Việc bọc này sẽ khóa (lock) toàn bộ map trong mọi thao tác đọc/ghi. Trong môi trường chịu tải rất lớn, nó có thể tạo ra điểm nghẽn cổ chai (lock contention). Khi duyệt (
iterate) qua map, ta vẫn phải đồng bộ thủ công:synchronized (syncCache) { for (var entry : syncCache.entrySet()) { ... } } -
Phương án 2: Sử dụng
ReentrantLockđộc lập Sử dụng một đối tượngReentrantLockhoặcReadWriteLock(tuy nhiên dogetcũng thực hiện mutation nên write lock vẫn phải áp dụng cho cảgetvàput) để đồng bộ hóa quyền truy cập một cách tường minh. -
Phương án tốt nhất cho Production: Nếu hệ thống của bạn yêu cầu hiệu năng cao, concurrency cực lớn, đừng tự chế LRU Cache bằng
LinkedHashMap. Hãy sử dụng các thư viện chuyên dụng như Caffeine Cache hoặc Guava Cache vốn được thiết kế tối ưu hóa cực tốt cho môi trường đa luồng mà không gây nghẽn cổ chai.
Pattern LRU cache này là nền tảng cho mini-challenge ở bài sau.
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 — xem khoá Java Internals | 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.
Liên kết khoá học khác
- Khoá Thuật toán — bài 3.1 Hash Function — hiểu uniform distribution và avalanche effect để biết tại sao
hashCodetốt quan trọng. - Khoá Thuật toán — bài 3.2 HashMap Internals — chaining vs treeify, load factor, resize internals của HashMap Java sâu hơn mức API.
- Khoá Thuật toán — bài 3.3 Open Addressing — phương án xử lý collision thay thế (linear/quadratic probing) để thấy rõ tradeoff với chaining.
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: Iterator, fail-fast và ConcurrentModificationException
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên