Part 01 · Java Core · Collections · 1.1.03.03

Hash table, linked order, red-black tree và binary heap

Đây là phần trả lời trực tiếp các câu “HashMap.put chạy thế nào?”, “HashSet lưu gì?” và “vì sao PriorityQueue iteration không sorted?”.

11 mục nguồn5 code blocks nguồn1 interview answer
Cách đọc: nội dung nền được giữ từ BK; phần mở rộng có nhãn Bổ sung. API contract theo Java SE 21; implementation detail đối chiếu OpenJDK tag jdk-21-ga. Không coi hằng số, layout nội bộ hoặc kết quả đo là guarantee cho mọi JDK. Các labs bổ sung tách assertion contract khỏi quan sát implementation.

1. HashMap fields và invariants

transient Node<K,V>[] table;
transient int size;
int threshold;
final float loadFactor;

Đối chiếu nguồn: [M1] [M2]

Bổ sung · Implementation · OpenJDK 21

Allocation là lazy: constructor thường chưa tạo mảng table. Trước allocation, threshold có thể giữ mục tiêu capacity; sau allocation nó mới đóng vai trò ngưỡng tăng bảng. Vì vậy không đọc trường này như một invariant tuyệt đối ở mọi thời điểm. Số bucket power-of-two và các tên field là chi tiết triển khai, không phải giao kèo chung của Map.

[M2]

2. put(key,value) từng bước

  1. Tính hash đã spread: implementation trộn high bits vào low bits để low-bit mask dùng thêm entropy.
  2. Nếu table chưa được khởi tạo, allocate theo initial/default capacity.
  3. Tính index bằng (n - 1) & hash; power-of-two capacity làm phép mask tương đương modulo trong miền phù hợp.
  4. Bucket rỗng: đặt node mới.
  5. Bucket có entry: so hash, sau đó key identity hoặc equals. Cùng key thì replace value.
  6. Nếu khác key: đi qua list hoặc tree bin; append/insert và treeify khi điều kiện đạt.
  7. Nếu thêm mapping mới làm size vượt threshold, resize.
static int spread(Object key) {
    int h;
    return key == null ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

int index = (table.length - 1) & hash;
So sánh hai tầng: hash giúp tìm bucket/candidate nhanh; equals xác nhận logical key. Cùng hash không có nghĩa equal.

Đối chiếu nguồn: [M1] [M2]

Bổ sung · Trace · Phân biệt replace và insert

Thay value của key đã tồn tại không tăng size. put trả value cũ; kết quả null có thể là key mới hoặc key từng map tới null. Snippet spread dưới đây là hàm minh họa cùng công thức, không phải tên API public. Mask cho chỉ số không âm; với hash âm, đừng thay máy móc bằng toán tử Java %.

[M1] [M2]

3. get(key)

Tính cùng spread hash và bucket index; kiểm tra first node rồi traverse list/tree, so hash trước và equality sau. Vì vậy mutable key đổi field tham gia hashCode/equals sau insert có thể nằm ở bucket cũ nhưng được tìm bằng hash mới, khiến get trả null.

Đối chiếu nguồn: [M1] [M2]

Bổ sung · Failure window · Mutable key

Insert → đổi dữ liệu equality/hash → lookup/remove là cửa sổ lỗi. Entry vẫn giữ hash đã tính ở lúc insert; resize không “chữa” key đã bị sửa. Phương án an toàn là key value-object bất biến; khi cần đổi identity, remove bằng trạng thái cũ trước, rồi tạo/insert key mới. Không dựa vào một lần get tình cờ thành công để kết luận key mutable là hợp lệ.

[M1] [M2]

4. Collision, treeification và complexity

Collision là bình thường. Java 8+ có thể chuyển bucket dài thành red-black tree khi bin đạt threshold (thường 8) và table có capacity tối thiểu (thường 64); nếu table còn nhỏ, resize được ưu tiên. Tree bin có lookup O(log n) trong điều kiện phù hợp; average map operation vẫn O(1). Các con số này là OpenJDK implementation detail, không phải Map contract.

Đối chiếu nguồn: [M2]

Bổ sung · Version caveat · Đọc đúng 8 / 6 / 64

TREEIFY_THRESHOLD = 8, UNTREEIFY_THRESHOLD = 6, MIN_TREEIFY_CAPACITY = 64 trong tag jdk-21-ga. Riêng đường putVal vào linked bin, thêm key thứ 9 vào bin đã có 8 node mới đi vào kiểm tra treeify; bảng nhỏ hơn 64 thì resize trước. Không suy rộng thời điểm này sang mọi đường compute/merge. Ngưỡng 6 tham gia split khi resize; removal còn có kiểm tra hình dạng cây.

Gọi b là số entry trong một bin: lookup tree thường O(log b) khi có phân biệt thứ tự phù hợp. Với nhiều hash giống hệt và không có thứ tự so sánh phân biệt được keys, quá trình tìm có thể phải dò nhiều nhánh, tới O(b). Red-black tree không biến mọi key tệ thành lookup logarithmic được bảo đảm.

[M2]

5. Resize không đơn giản là “rehash tất cả”

Khi capacity gấp đôi, một entry hoặc giữ index cũ hoặc chuyển tới oldIndex + oldCapacity tùy bit hash & oldCapacity. OpenJDK split bucket thành low/high lists, tránh gọi lại user hashCode. Resize vẫn O(n), allocation lớn và có latency/memory spike; initial capacity hợp lý giúp workload lớn.

Đối chiếu nguồn: [M1] [M2]

Bổ sung · Trade-off · Sizing và latency

Ví dụ capacity 16 → 32: hash 5 ở bucket 5 vẫn ở 5; hash 21 đang ở bucket 5 chuyển sang 21 vì bit hash & 16 khác 0. “Rehash” trong Javadoc nói về tái tổ chức bảng, không bắt buộc gọi lại hàm hashCode() của user.

Chi phí quét resize có thành phần O(capacity + size); cách nói O(n) trong nguồn dùng giả định capacity cùng bậc size. Java 19+ có HashMap.newHashMap(expectedMappings); initial capacity của constructor không đồng nghĩa số mapping dự kiến. Sizing quá lớn cũng tốn memory và làm iteration đắt hơn. Đo allocation, GC, p95/p99 và thời gian insert quanh ngưỡng trên workload thật; không suy ra throughput từ Big-O.

[M1] [M2]

6. equals/hashCode contract

record AccountKey(String bankCode, String accountNumber) {}

Map<AccountKey, Account> accounts = new HashMap<>();
// Record phù hợp vì equality/hashCode theo immutable components.

Đối chiếu nguồn: [M1] [M9]

Bổ sung · Scope guarantee · Record không deep-immutable

Ví dụ AccountKey bên trên an toàn vì hai components là String bất biến. record Key(List<String> tags) chỉ giữ reference final: sửa nội dung list có thể đổi equality/hash của record. Defensive copy như List.copyOf(tags) giúp ổn định danh sách khi elements cũng bất biến; không tự deep-copy object lồng bên trong. Account trong snippet là kiểu domain của ứng dụng, không phải lớp JDK.

[M9] [M10]

7. HashSet thực chất lưu gì?

HashSet<E> dùng một HashMap<E,Object> bên dưới. Element trở thành map key; value là cùng một sentinel object dùng chung. add(e) tương đương put key và trả true khi trước đó chưa có mapping. Vì thế uniqueness, null support, collision và mutable-element risk theo HashMap key semantics.

Đối chiếu nguồn: [M5]

Bổ sung · Trace · Uniqueness không dựa vào hash một mình

Sentinel là object dùng chung làm value đánh dấu sự hiện diện, không phải dữ liệu của từng element. Hai object khác identity nhưng equal sẽ chỉ có một phần tử trong set; hai object chỉ trùng hash vẫn có thể cùng tồn tại. Một null element được chấp nhận. Set vẫn không thread-safe; iteration cũng không có thứ tự được hứa trước.

[M5]

8. LinkedHashMap và LRU

Ngoài hash table, entries tham gia doubly linked order. Insertion-order giữ thứ tự thêm; access-order di chuyển entry được access về cuối. Override removeEldestEntry tạo bounded in-process LRU đơn giản.

Map<K,V> lru = new LinkedHashMap<>(capacity, 0.75f, true) {
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > capacity;
    }
};

Nó không thread-safe, không có TTL/weight/distributed consistency và eviction chạy khi insert. Production cache thường cần Caffeine/Redis tùy boundary.

Đối chiếu nguồn: [M6]

Bổ sung · Failure window · “Read” cũng có thể đổi thứ tự

Với access-order, get có thể thay đổi linked order và là structural modification; containsKey không phải access làm mới LRU. Ví dụ capacity 2: put A, put B, get A, put C → B bị loại; thứ tự còn lại A, C. Khi thêm mapping mới, hook eviction được xét sau insertion, nên có thể có trạng thái nội bộ tạm thời vượt capacity. Đây là giới hạn số entry, không phải giới hạn byte.

Nếu dùng Collections.synchronizedMap, mọi truy cập phải qua wrapper và traversal/compound logic cần lock thống nhất. Không đồng nhất LRU với TTL hoặc distributed cache: lựa chọn Caffeine/Redis mà nguồn nêu phải dựa trên boundary, expiry, invalidation và consistency thực sự cần.

[M6] [M11]

Trong snippet nguồn, K/V là type parameters và capacity là giới hạn entry do caller cung cấp; dùng giá trị dương. Bản runnable ở cuối trang dùng kiểu cụ thể.

9. TreeMap/TreeSet: red-black tree

TreeMap là self-balancing binary search tree. Search/put/remove O(log n); rotation/recolor giữ tree height bounded. Key ordering đến từ natural ordering hoặc Comparator. Nếu comparison trả 0, map coi keys là cùng vị trí dù equals false; comparator nên consistent with equals nếu muốn Map/Set behavior trực giác.

Đối chiếu nguồn: [M3] [M7]

Bổ sung · Contract · Comparator quyết định identity trong sorted map

Comparator trả 0 cho hai keys không equal làm chúng đại diện một mapping; behavior vẫn xác định, nhưng không tuân thủ general Map contract dựa trên equals. Ví dụ String.CASE_INSENSITIVE_ORDER gộp “java” và “JAVA”. Comparator phải nhất quán, transitive và không phụ thuộc field bị đổi khi key còn trong cây.

floor/ceiling cho phép bằng; lower/higher là nhỏ hơn/lớn hơn nghiêm ngặt. subMap(from, true, to, false) có biên trái đóng, phải mở; insert qua view ngoài khoảng bị từ chối. Natural-order TreeMap không nhận null key; comparator riêng có thể hỗ trợ null. TreeSet thường dùng TreeMap làm backing map.

[M7] [M3]

10. PriorityQueue: binary heap

PriorityQueue giữ heap trong array. Với min-heap, parent không lớn hơn children; không yêu cầu toàn array sorted. offer đặt cuối rồi siftUp; poll lấy root, đưa phần tử cuối lên root rồi siftDown.

parent(i) = (i - 1) / 2
left(i)   = 2 * i + 1
right(i)  = 2 * i + 2

Đối chiếu nguồn: [M4] [M8]

Bổ sung · Contract và trade-off · Priority khác sorted iteration

Heap invariant áp dụng theo comparator, không phải quan hệ số học cố định. Công thức parent chỉ dùng với i > 0; root không có parent. remove(Object)contains tuyến tính, còn remove() không đối số lấy head như poll nhưng khác cách báo rỗng.

Không có FIFO guarantee cho priority bằng nhau; thêm sequence number vào comparator nếu cần tie-break xác định. Không sửa priority của object đang nằm trong heap; remove rồi insert lại hoặc thiết kế entry bất biến. PriorityQueue không thread-safe và không nhận null. Muốn sorted output, poll lặp trên bản sao hoặc sort bản sao; không assert một layout iterator cụ thể.

[M8] [M4]

Suy luận thuật toán: lấy K phần tử lớn nhất với K > 0 bằng min-heap tối đa K: thêm khi chưa đủ, hoặc thay root nếu phần tử mới lớn hơn root. Chi phí O(n log K), memory O(K); kết quả trong heap chưa sorted. Muốn K phần tử nhỏ nhất thì đảo hướng heap. Khi K = 0, trả kết quả rỗng ngay.

11. Interview answer: HashMap internals trong 60 giây

HashMap lưu entries trong mảng buckets. Nó spread hashCode, dùng bitmask với capacity power-of-two để chọn bucket, rồi so hash và equals để tìm key. Collision nằm trong linked list và có thể treeify thành red-black tree khi bucket/table đủ lớn. Khi size vượt capacity × load factor, mặc định 0.75, table resize thường gấp đôi và entries tách sang index cũ hoặc oldIndex + oldCapacity. Average get/put O(1), nhưng phụ thuộc hash distribution; mutable key và concurrent writes là lỗi phổ biến.

Đối chiếu nguồn: [M1] [M2]

Bổ sung · Cách chốt câu trả lời

Contract: key equality và null support. Implementation: spread/mask, bin, resize. Trade-off: distribution, allocation và iteration. Evidence: một test key bất biến, một trace split bucket, một profile dưới collision. Không hứa worst-case O(log n) cho mọi loại key, hoặc thread safety chỉ vì một test chưa gặp lỗi.

[M1] [M2]

12. Mini-labs: tự kiểm chứng implementation và contract

Bổ sung thực hành · Hai chương trình độc lập, không có thư viện ngoài. Java 21; lưu đúng tên file hiển thị. Assertion dùng kiểm tra tường minh, không phụ thuộc cờ -ea.

Lab C1 · Collision và ranh giới treeification

Failure-first: thay phỏng đoán “8 node chắc chắn là tree” bằng trace trên HashMap initial capacity 64, mọi key cùng hash. Các key vẫn có equality và ordering phân biệt được nhau.

TreeifyLab.java

import java.lang.reflect.Field;
import java.util.HashMap;

public class TreeifyLab {
    record Key(int id) implements Comparable<Key> {
        @Override public int hashCode() { return 42; }
        @Override public int compareTo(Key other) {
            return Integer.compare(id, other.id);
        }
    }

    public static void main(String[] args) throws Exception {
        var map = new HashMap<Key, Integer>(64);
        Field field = HashMap.class.getDeclaredField("table");
        field.setAccessible(true); // Lab-only: requires --add-opens.
        for (int i = 1; i <= 9; i++) {
            map.put(new Key(i), i);
            if (i == 8 || i == 9) {
                Object[] buckets = (Object[]) field.get(map);
                int hash = 42 ^ (42 >>> 16);
                Object bin = buckets[(buckets.length - 1) & hash];
                String kind = bin.getClass().getSimpleName();
                System.out.printf("size=%d capacity=%d bin=%s%n",
                    map.size(), buckets.length, kind);
                String expected = i == 8 ? "Node" : "TreeNode";
                if (buckets.length != 64 || !kind.equals(expected)) {
                    throw new AssertionError("Different implementation: " + kind);
                }
            }
        }
        for (int i = 1; i <= 9; i++) {
            if (!Integer.valueOf(i).equals(map.get(new Key(i)))) {
                throw new AssertionError("Missing key " + i);
            }
        }
        System.out.println("PASS: treeification and equality lookup");
    }
}
javac --release 21 TreeifyLab.java
java --add-opens java.base/java.util=ALL-UNNAMED TreeifyLab

Quan sát trên OpenJDK 21: sau 8 mappings: capacity=64, bin=Node; sau mapping thứ 9: bin=TreeNode. Lookup bằng key mới nhưng equal vẫn đúng. Thử capacity 16 và trace từng lần put để thấy resize được ưu tiên trước khi treeify.

Acceptance: trace khớp nhánh source putVal/treeifyBin trên bản JDK đã ghi nhận và không mất mapping. Nộp: java -version, stdout, initial capacity và giải thích vì sao hằng số 8 không đồng nghĩa tree xuất hiện ở lần insert thứ 8.

Chỉ dùng reflection trong lab. --add-opens mở truy cập nội bộ cho thí nghiệm; không là dependency hợp lệ của application. Tên Node/TreeNode, trường table và thời điểm chuyển đổi có thể thay đổi ở JDK khác. Assertion implementation fail phải dẫn đến đọc source đúng phiên bản, không kết luận API hỏng.
[M2]

Lab C2 · Equality, comparator, LRU và heap

Failure-first: dự đoán sai rằng hai instances là hai keys, case-insensitive tree giữ cả hai cách viết, get không ảnh hưởng LRU, hoặc iterator heap luôn sorted. Chạy các checks để phân biệt từng invariant.

MapHeapLab.java

import java.util.*;

public class MapHeapLab {
    record AccountKey(String bankCode, String accountNumber) {}

    static void check(boolean ok, String message) {
        if (!ok) throw new AssertionError(message);
    }

    public static void main(String[] args) {
        var first = new AccountKey("BK", "001");
        var equal = new AccountKey("BK", "001");
        var accounts = new HashMap<AccountKey, Integer>();
        accounts.put(first, 10);
        check(accounts.put(equal, 20) == 10, "put must return old value");
        check(accounts.size() == 1, "equal keys must share one mapping");
        check(new HashSet<>(List.of(first, equal)).size() == 1,
            "set uniqueness");

        var tree = new TreeMap<String, Integer>(
            String.CASE_INSENSITIVE_ORDER);
        tree.put("java", 1);
        tree.put("JAVA", 2);
        check(!"java".equals("JAVA") && tree.size() == 1,
            "comparator identity differs from equals");

        final int capacity = 2;
        Map<String, Integer> lru =
            new LinkedHashMap<>(capacity, 0.75f, true) {
                @Override
                protected boolean removeEldestEntry(
                        Map.Entry<String, Integer> eldest) {
                    return size() > capacity;
                }
            };
        lru.put("A", 1);
        lru.put("B", 2);
        lru.get("A");
        lru.put("C", 3);
        check(new ArrayList<>(lru.keySet()).equals(List.of("A", "C")),
            "B must be evicted after accessing A");

        var heap = new PriorityQueue<>(List.of(9, 1, 8, 2, 7));
        System.out.println("Iterator (not a sorted-order contract): " + heap);
        var ordered = new ArrayList<Integer>();
        while (!heap.isEmpty()) ordered.add(heap.poll());
        check(ordered.equals(List.of(1, 2, 7, 8, 9)), "poll order");
        System.out.println("PASS: equality, set, comparator, LRU, heap");
    }
}
javac --release 21 MapHeapLab.java
java MapHeapLab

Acceptance: một mapping cho hai equal keys; comparator gộp hai cách viết; B bị eviction; poll trả thứ tự 1, 2, 7, 8, 9. Không assert chuỗi iterator: có thể tình cờ sorted ở một input. Nộp: stdout và giải thích cái nào là contract, cái nào là policy/comparator do test chọn.

[M1] [M5] [M6] [M7] [M8]

13. Selection guide và review có bằng chứng

Bổ sung · Chọn theo query pattern và ownership; Big-O chỉ là bước đầu.

Cấu trúc dữ liệu, ràng buộc và evidence
Nhu cầuĐiểm xuất phátBằng chứng cần có
Lookup key / uniquenessHashMap / HashSetTests equality/hash, key bất biến, null và collision distribution.
Encounter order / LRU localLinkedHashMapTest access-order, eviction và giới hạn entry; xác định thread ownership.
Sorted keys / range queriesTreeMap / TreeSetComparator laws, range boundaries, expected floor/ceiling.
Lấy phần tử ưu tiên / Top-KPriorityQueueTest poll order, ties, K=0/K>n; đo chi phí remove tùy ý.
Chia sẻ qua nhiều threadsChuyển sang bài Concurrent/ViewsChỉ rõ invariant theo key/nhiều key và failure interleaving; không bọc HashMap tùy tiện.
Tự kiểm: vẽ được put/get và split low/high; phân biệt equality với collision và comparator; giải thích vì sao record chỉ shallow-immutable; kiểm được LRU và poll order; chỉ ra nơi cần synchronization. Khi đo performance, ghi JDK, heap, key distribution, warm-up, thread count và percentile; không dùng một timing run để hứa throughput production.

14. Tài liệu chính thức và phạm vi tham chiếu

Kiểm chứng ngày 12/09/2026. API được ghim Java SE 21; source được ghim tag jdk-21-ga, không phải nhánh phát triển mới nhất. Các liên kết chỉ để đọc tham khảo; trang không tải tài nguyên ngoài.

  1. [M1] HashMap API — API: complexity, null, fail-fast, newHashMap. Giữ từ nguồn BK.
  2. [M2] OpenJDK 21 HashMap — Implementation: putVal, resize, treeifyBin, TreeNode.find. Giữ từ nguồn BK.
  3. [M3] TreeMap source — Implementation: red-black tree và range views. Giữ từ nguồn BK.
  4. [M4] PriorityQueue source — Implementation: heap array, siftUp/siftDown. Giữ từ nguồn BK.
  5. [M5] HashSet API — API: map-backed set, null, iterator. Tham khảo bổ sung.
  6. [M6] LinkedHashMap API — API: access order và removeEldestEntry. Tham khảo bổ sung.
  7. [M7] TreeMap API — API: comparator, navigable/range operations. Tham khảo bổ sung.
  8. [M8] PriorityQueue API — API: priority, ties, iteration, complexity. Tham khảo bổ sung.
  9. [M9] Record API — Scope: record là shallowly immutable. Tham khảo bổ sung.
  10. [M10] List API — Scope: copyOf và snapshot nông. Tham khảo bổ sung.
  11. [M11] Collections API — Scope: synchronized wrappers và traversal. Tham khảo bổ sung.