Part 01 · Java Core · 1.1.03.05

Collections Internals / Interview & practice

Từ câu hỏi internals tới quyết định production

Không chỉ kể data structure: hãy nối operation path với complexity, memory, correctness và workload.

15interview scenarios
04labs thực hành
06tiêu chí lựa chọn
Cách trả lời: đi từ operation path → complexity và memory → correctness invariant → workload → evidence. Phần Cốt lõi giữ câu trả lời nền; phần Đi sâu bổ sung phạm vi guarantee, trade-off và cách kiểm chứng.

Baseline: Java 21, không cần preview feature. Các chi tiết HashMap được gắn riêng với OpenJDK jdk-21+35; khi làm lab, đối chiếu source đúng build đang chạy. Không có kết quả benchmark cố định được giả định trong bài.

15 câu hỏi phỏng vấn

Tự trả lời trước, sau đó mở từng câu để đối chiếu. Các khối mở/đóng dùng chức năng gốc của trình duyệt.

1. HashMap hoạt động bên dưới thế nào?
Cốt lõi

Mảng buckets; spread hash; mask chọn index; hash + equals tìm key; collision list/tree; resize khi vượt threshold. Average O(1), phụ thuộc hash distribution và key stability. [1] [2]

Đi sâu · scope, trade-off & evidence

API guarantee và scope. Chi phí trung bình không phải cam kết worst-case hoặc latency SLA. HashMap không tự đồng bộ; iteration không có thứ tự được đảm bảo. Dung lượng dư còn làm tăng chi phí quét bảng. Đường đi bucket/tree và các ngưỡng cụ thể là chi tiết implementation, không phải contract của mọi Map. [1] [2]

Kiểm chứng. Với cùng số key, so hash phân bố tốt và constant hash; ghi số phép so sánh, allocation, thời điểm resize. Bài giải tốt phải nói được key có ổn định không và workload có nhiều lookup hay traversal.

2. Tại sao capacity HashMap là power of two?
Cốt lõi

Cho phép dùng (n-1)&hash thay modulo và resize split entry theo một bit. Hash spreading giúp high bits ảnh hưởng bucket index. [2]

Đi sâu · scope, trade-off & evidence

Implementation · OpenJDK 21. Dùng hash đã spread h ^ (h >>> 16), không dùng trực tiếp raw hash trong phép mask. Khi capacity tăng từ n lên 2n, bit hash & n quyết định giữ index cũ hay chuyển sang oldIndex + n. [2]

Chứng minh ngắn. Mask mới có thêm đúng một bit so với n - 1. Ví dụ capacity 16 → 32: hash 1 vẫn ở bucket 1; hash 17 đi từ bucket 1 sang 17. Đây là phép suy luận trên bit, không phải benchmark chứng minh mask luôn nhanh hơn modulo trong mọi chương trình.

3. Nếu hai key cùng hashCode?
Cốt lõi

Chúng vào cùng bucket nhưng equals phân biệt. Collision không làm mất dữ liệu; nó tăng traversal/tree cost. [1] [3]

Đi sâu · scope, trade-off & evidence

Correctness. Hai key equal phải có cùng hash; chiều ngược lại không đúng. Key không equal có cùng hash vẫn là hai mapping. put bằng một key equal cập nhật value của mapping hiện có, không tăng số mapping. Đừng dùng hashCode như một unique ID. [3]

Failure case. Test hai key khác nhau cùng hash rồi lookup cả hai; tiếp tục put key equal và kiểm tra size không đổi. Hash spreading không phân biệt được các raw hash vốn đã giống hệt nhau.

4. Mutable key gây gì?
Cốt lõi

Sau khi field trong hash/equality đổi, lookup tính bucket mới trong khi entry ở bucket cũ; get/remove có thể thất bại. Dùng immutable key/value object. [3] [4]

Đi sâu · scope, trade-off & evidence

Giới hạn của ví dụ. Contract Map không xác định hành vi khi key bị sửa theo cách ảnh hưởng equals. “Lookup thất bại” là một failure có thể tái hiện, không phải mọi lần mutate đều chắc chắn thất bại. Lab A chọn hash 1 → 2 để tạo tình huống cụ thể trên OpenJDK. [3]

Thiết kế key. record StableKey(String id) phù hợp vì component ở đây là immutable. Một record chứa List mutable vẫn chỉ shallow-immutable; cần defensive copy và component equality ổn định. Value không bắt buộc immutable để HashMap tìm đúng key, nhưng shared mutable value có bài toán đồng bộ riêng. [4]

5. HashSet kiểm tra duplicate ra sao?
Cốt lõi

Element là key trong backing HashMap với giá trị sentinel dùng chung. add dựa vào hash/equality và trả false nếu mapping đã tồn tại. [5]

Đi sâu · scope, trade-off & evidence

API guarantee. HashSet không bảo đảm iteration order và cho phép phần tử null. Giá trị sentinel của backing map chỉ đánh dấu sự hiện diện; uniqueness được quyết định bởi element/key, không phải sentinel. [5]

Evidence. Kiểm tra add(a) trả true lần đầu, false với một object equal; hai object cùng hash nhưng không equal đều được giữ. Với deduplication nghiệp vụ, phải xác định identity, vòng đời và phạm vi dữ liệu: một set trong một process không tự bảo đảm uniqueness giữa nhiều instance hoặc sau restart.

6. ArrayList hay LinkedList?
Cốt lõi

ArrayList thường mặc định tốt hơn vì O(1) index, locality và ít allocation. LinkedList chỉ có O(1) insert/remove khi đã có node/iterator; tìm index vẫn O(n). [6] [7]

Đi sâu · scope, trade-off & evidence

Trade-off. ArrayList lưu mảng reference liền nhau, không bảo đảm bản thân các object phần tử nằm liền nhau. LinkedList phải lần theo node để đến index; O(1) splice chỉ có ý nghĩa khi iterator đã ở đúng vị trí. Đừng bỏ chi phí định vị khỏi kết luận end-to-end. [6] [7]

Chọn theo operation. Với FIFO/LIFO trong một thread, đưa ArrayDeque vào tập ứng viên thay vì mặc định dùng LinkedList; nó không nhận null và không thread-safe. Lab B phải tách random get, traversal và thao tác qua iterator đã định vị. [8]

7. ArrayList resize mỗi lần add?
Cốt lõi

Không. Nó grow khi capacity hết; nhiều adds chỉ write slot. Một số lần O(n) copy nhưng append amortized O(1). [6]

Đi sâu · scope, trade-off & evidence

API guarantee. Append amortized O(1) không có nghĩa mỗi lần add đều O(1). API không cố định hệ số tăng capacity; không biến một con số đọc từ source JDK thành contract. ensureCapacity hoặc capacity dự kiến có thể giảm tái cấp phát, đổi lại giữ thêm bộ nhớ. [6]

Failure case. Một đợt burst chạm ngưỡng grow có thể có latency khác steady-state. Tách phép đo “còn slot” và “phải grow”; ghi capacity policy, size và GC. Insert ở giữa còn phải dịch reference, không hưởng cam kết append amortized O(1).

8. TreeMap equality khác HashMap?
Cốt lõi

HashMap xác định key bằng hash + equals. TreeMap định vị/equivalence theo comparator hoặc compareTo trả 0; comparator inconsistent with equals có thể gây semantics bất ngờ. [9]

Đi sâu · scope, trade-off & evidence

API guarantee. TreeMap dùng thứ tự comparator/natural ordering để xác định key tương đương và hỗ trợ get/put/remove O(log n). Comparator cần có ordering hợp lệ; nhất quán với equals nếu muốn tuân thủ đầy đủ contract của Map. [9]

Failure case. Comparator chỉ so độ dài khiến "aa""bb" trả 0 dù không equal: lần put sau thay value. Thêm tie-breaker chỉ khi nghiệp vụ thực sự muốn phân biệt hai key. Lab C kiểm tra cả overwrite và range/floor/ceiling.

9. PriorityQueue có iteration sorted?
Cốt lõi

Không. Heap chỉ bảo đảm root có priority cao nhất/thấp nhất và parent-child invariant. Muốn sorted output phải poll lặp lại hoặc copy rồi sort. [10]

Đi sâu · scope, trade-off & evidence

API guarantee. Với natural ordering, head là phần tử nhỏ nhất; comparator có thể đảo thứ tự ưu tiên. Các phần tử đồng hạng không được bảo đảm FIFO. peek không sắp xếp toàn bộ heap; iteration có thể tình cờ sorted trên dữ liệu nhỏ, nhưng không có guarantee đó. [10]

Correctness. Không sửa trực tiếp field dùng cho priority khi phần tử còn ở trong queue. Xóa rồi chèn lại, hoặc dùng thiết kế phù hợp với update priority. Khi cần thứ tự deterministic, thêm tie-breaker như sequence/id và so bằng comparator thay vì phép trừ dễ overflow.

10. LinkedHashMap làm LRU thế nào?
Cốt lõi

Access-order linked list đưa entry vừa access về cuối; removeEldestEntry evict đầu sau insert. Nó không tự thread-safe, TTL/weight-aware hoặc distributed. [11]

Đi sâu · scope, trade-off & evidence

Scope. Ví dụ sau giới hạn số entry, không giới hạn bytes. Trong access-order map, get có thể làm thay đổi thứ tự, vì vậy không thể coi mọi read là không sửa cấu trúc. Shared cache cần chiến lược đồng bộ phù hợp cho cả access và eviction. [11]

LruDemo.java · Java 21 · chạy với -ea
import java.util.*;

public class LruDemo {
    static final class Lru<K, V> extends LinkedHashMap<K, V> {
        private static final long serialVersionUID = 1L;
        private final int limit;

        Lru(int limit) {
            super(16, 0.75f, true); // true = access order
            if (limit <= 0) throw new IllegalArgumentException("limit > 0");
            this.limit = limit;
        }

        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > limit;
        }
    }

    public static void main(String[] args) {
        var cache = new Lru<String, Integer>(2);
        cache.put("A", 1);
        cache.put("B", 2);
        Integer accessed = cache.get("A"); // B is now least recently used
        assert accessed == 1;
        cache.put("C", 3);
        assert !cache.containsKey("B");
        assert new ArrayList<>(cache.keySet()).equals(List.of("A", "C"));
        System.out.println("PASS LruDemo: B evicted; order = [A, C]");
    }
}

Acceptance. Sau A, B, truy cập A rồi thêm C, B bị evict và thứ tự còn A, C. TTL, cân theo weight, load coalescing, distributed consistency và thống kê hit/miss là yêu cầu bổ sung; đoạn code không cung cấp chúng.

11. Fail-fast iterator có thread-safe?
Cốt lõi

Không. Nó best-effort phát hiện structural modification, không synchronization guarantee. Concurrent traversal cần external synchronization hoặc concurrent collection semantics phù hợp. [6] [13]

Đi sâu · scope, trade-off & evidence

API guarantee. ConcurrentModificationException là tín hiệu phát hiện lỗi theo best effort, không phải lock, memory barrier hay bằng chứng không có race khi exception không xuất hiện. Dùng mutation qua iterator khi API hỗ trợ; với synchronized wrapper, giữ cùng lock trong toàn bộ traversal. [6] [13]

Assertion đúng scope. Controlled single-thread example có thể minh họa fail-fast; concurrent stress test không được yêu cầu “luôn phải ném CME”. Snapshot và weakly consistent cũng không đồng nghĩa toàn bộ object graph là immutable. Xem bảng oracle ở Lab D.

12. List.of khác unmodifiableList?
Cốt lõi

List.of tạo unmodifiable collection không null. Wrapper unmodifiable chặn mutation qua wrapper nhưng backing có thể đổi. Cả hai không deep-immutable nếu elements mutable. [12] [13]

Đi sâu · scope, trade-off & evidence

View và copy. List.copyOf tạo cấu trúc unmodifiable không phản ánh các thay đổi cấu trúc sau đó của input, nhưng vẫn chia sẻ reference phần tử. Nó từ chối null; wrapper unmodifiableList có thể chứa null nếu backing list có. Không suy luận deep immutability hoặc thread safety chỉ từ từ khóa unmodifiable. [12] [13]

ViewsDemo.java · Java 21 · chạy với -ea
import java.util.*;

public class ViewsDemo {
    public static void main(String[] args) {
        var backing = new ArrayList<>(List.of("A"));
        List<String> view = Collections.unmodifiableList(backing);
        List<String> copy = List.copyOf(backing);

        backing.add("B");
        assert view.equals(List.of("A", "B")); // live view
        assert copy.equals(List.of("A"));      // detached structure

        boolean blocked = false;
        try {
            view.add("C");
        } catch (UnsupportedOperationException expected) {
            blocked = true;
        }
        assert blocked;

        boolean nullRejected = false;
        try {
            List.of("A", (String) null);
        } catch (NullPointerException expected) {
            nullRejected = true;
        }
        assert nullRejected;

        var shallow = List.of(new StringBuilder("A"));
        shallow.get(0).append("B");
        assert shallow.get(0).toString().equals("AB");
        System.out.println("PASS ViewsDemo: view, copy, null, shallow");
    }
}

Version caveat. List.of có từ Java 9, List.copyOf từ Java 10. Các ví dụ trong trang lấy Java 21 làm baseline; copy một collection đang bị sửa vẫn cần snapshot/locking protocol của chính collection đó. [12]

13. ConcurrentHashMap có atomic cho contains-then-put?
Cốt lõi

Không qua hai calls riêng. Dùng putIfAbsent, compute, merge hoặc computeIfAbsent theo invariant. [14]

Đi sâu · scope, trade-off & evidence

Atomic boundary. Guarantee của phương thức ConcurrentHashMap không biến hai lời gọi riêng, nhiều key hoặc database side effect thành một transaction. Mapping function cần ngắn, không sửa chính map; không nhét remote I/O vào callback. Null, exception hoặc removal có thể khiến lần gọi sau phải tính lại, nên không coi callback là “chỉ một lần vĩnh viễn”. [14]

Counter. Frequency map có thể dùng computeIfAbsent(k, ...).increment() với LongAdder. sum() không là atomic snapshot khi writer đang chạy; lab chỉ assert tổng chính xác sau khi các worker kết thúc, không remove/reset/clear. Quota hoặc quyết định check-and-increment cần atomic protocol riêng. [15]

14. Queue nào cho producer-consumer?
Cốt lõi

Thường bounded BlockingQueue để có backpressure và rõ reject/block timeout. ConcurrentLinkedQueue non-blocking nhưng unbounded, không tự bảo vệ overload. [16] [18]

Đi sâu · scope, trade-off & evidence

Capacity là lựa chọn cụ thể. Chẳng hạn new ArrayBlockingQueue<Job>(capacity) có bound rõ. Không suy luận rằng mọi BlockingQueue đều bounded chỉ vì cùng interface. offer có thể báo không nhận được; put chờ; timed offer chờ trong giới hạn và cần xử lý kết quả/interruption. [16] [17]

Production evidence. Cho consumer chậm lại: đo queue depth/age, số offer thất bại, thời gian producer bị chặn và bộ nhớ. ConcurrentLinkedQueue không tự thêm backpressure. Bound theo số item cũng chưa phải bound theo bytes; FIFO lấy việc không bảo đảm thứ tự hoàn thành khi nhiều consumer xử lý song song. [18]

15. Top 100 trong 100 triệu records?
Cốt lõi

Giữ min-heap PriorityQueue kích thước 100: O(n log 100), memory O(100), thay vì full sort O(n log n). [10]

Đi sâu · scope, trade-off & evidence

Giả định. Đang chọn 100 record có score lớn nhất. Min-heap giữ phần tử “kém nhất trong nhóm đang giữ” ở head; chỉ thay head khi candidate tốt hơn. Với k ≥ 2, xử lý input là O(n log k), bộ nhớ phụ O(k); nếu cần output sorted, thêm O(k log k). Input phải được đọc streaming, không materialize 100 triệu record trước. [10]

TopKDemo.java · Java 21 · chạy với -ea
import java.util.*;

public class TopKDemo {
    record Row(long id, long score) {}
    // Higher score wins; for equal scores, higher id wins.
    static final Comparator<Row> ORDER =
        Comparator.comparingLong(Row::score).thenComparingLong(Row::id);

    static List<Row> topK(Iterator<Row> input, int k) {
        Objects.requireNonNull(input);
        if (k < 0) throw new IllegalArgumentException("k >= 0");
        if (k == 0) return List.of(); // does not consume input

        var heap = new PriorityQueue<Row>(ORDER);
        while (input.hasNext()) {
            Row row = Objects.requireNonNull(input.next());
            if (heap.size() < k) {
                heap.offer(row);
            } else if (ORDER.compare(row, heap.peek()) > 0) {
                heap.poll();
                heap.offer(row);
            }
        }
        var result = new ArrayList<>(heap);
        result.sort(ORDER.reversed());
        return List.copyOf(result);
    }

    public static void main(String[] args) {
        var random = new Random(7);
        var input = new ArrayList<Row>();
        for (int i = 0; i < 1_000; i++) {
            input.add(new Row(i, random.nextInt(21) - 10));
        }
        for (int k : new int[]{0, 1, 100, 1_001}) {
            var expected = input.stream()
                .sorted(ORDER.reversed()).limit(k).toList();
            assert topK(input.iterator(), k).equals(expected);
        }
        assert topK(Collections.emptyIterator(), 100).isEmpty();
        System.out.println("PASS TopKDemo: top-K equals full-sort oracle");
    }
}

Acceptance. So với full-sort oracle trên tập nhỏ; test empty input, k = 0, k = 1, k > n, số âm và score trùng. Code định nghĩa tie-breaker là id lớn hơn thắng; xử lý từng record, không tự deduplicate theo id. Đổi yêu cầu “100 nhỏ nhất” thì phải đảo heap/comparator tương ứng.

Lab A · HashMap source flow

Các bước bắt buộc

  1. Tạo keys có controlled hash: unique, cùng low bits và constant hash.
  2. Insert qua các ngưỡng capacity/collision; dùng debugger theo putVal, resize, treeifyBin.
  3. Ghi bucket index trước/sau resize và chứng minh oldIndex/oldIndex+oldCapacity.
  4. Mutate key sau insert; assert lookup failure rồi sửa bằng record immutable.

Evidence: JDK version, key/hash table, debugger snapshots, operation counts và invariant không mất mappings.

Setup và quan sát có kiểm soát

Chọn JDK 21, ghi đầy đủ java -version, vendor và build. Attach source JDK tương ứng trong IDE; đặt breakpoint ở putVal, resizetreeifyBin. Đoạn bên dưới không dùng reflection hoặc mở module: bucket array và loại node được xem trong debugger, không phải public API.

Version caveat · không học thuộc ngưỡng như contract. Trong source OpenJDK jdk-21+35, TREEIFY_THRESHOLD = 8MIN_TREEIFY_CAPACITY = 64. Ở đường putVal đang append vào list bin, thêm key khác biệt thứ 9 mới đi vào lời gọi treeify; nếu bảng nhỏ hơn 64 thì nhánh này ưu tiên resize. Không suy ra “key thứ 8 luôn thành tree” cho mọi đường API hoặc mọi JDK. [2]
HashMapFlowLab.java · Java 21 · chạy với -ea
import java.util.*;

public class HashMapFlowLab {
    record Key(int id, int rawHash) {
        @Override public int hashCode() { return rawHash; }
    }
    record StableKey(String id) {}

    static final class MutableKey {
        int id;
        MutableKey(int id) { this.id = id; }
        @Override public int hashCode() { return id; }
        @Override public boolean equals(Object other) {
            return other instanceof MutableKey k && id == k.id;
        }
    }

    static int spread(int raw) { return raw ^ (raw >>> 16); }

    public static void main(String[] args) {
        // Case 1: unique hashes; observe the insertion at size 13.
        var unique = new HashMap<Key, Integer>(16);
        for (int i = 0; i < 13; i++) unique.put(new Key(i, i), i);
        for (int i = 0; i < 13; i++)
            assert unique.get(new Key(i, i)) == i;

        // Case 2: raw hashes 1 and 17 have equal low four bits.
        var split = new HashMap<Key, Integer>(16);
        split.put(new Key(1, 1), 1);
        split.put(new Key(17, 17), 17);
        for (int i = 2; i <= 12; i++) split.put(new Key(i, i), i);
        assert split.size() == 13;
        for (var entry : split.entrySet())
            assert Objects.equals(split.get(entry.getKey()), entry.getValue());

        for (int raw : new int[]{1, 17, 33, 65_536, -1}) {
            int h = spread(raw);
            int oldIndex = h & (16 - 1);
            int newIndex = h & (32 - 1);
            assert newIndex == oldIndex + ((h & 16) == 0 ? 0 : 16);
            System.out.printf("raw=%d spread=%d old=%d new=%d%n",
                raw, h, oldIndex, newIndex);
        }

        // Case 3: distinct keys, identical hash; inspect treeifyBin.
        var collision = new HashMap<Key, Integer>(64);
        for (int i = 0; i < 9; i++) collision.put(new Key(i, 0), i);
        assert collision.size() == 9;
        for (int i = 0; i < 9; i++)
            assert collision.get(new Key(i, 0)) == i;

        // Controlled OpenJDK experiment, NOT a portable mutable-key contract.
        var broken = new HashMap<MutableKey, String>(16);
        var key = new MutableKey(1);
        broken.put(key, "value");
        key.id = 2;
        assert broken.get(key) == null;
        assert broken.remove(key) == null;
        assert broken.size() == 1; // entry still exists
        key.id = 1;
        assert "value".equals(broken.get(key));

        var fixed = new HashMap<StableKey, String>();
        fixed.put(new StableKey("1"), "value");
        assert "value".equals(fixed.get(new StableKey("1")));
        System.out.println("PASS HashMapFlowLab");
    }
}
java -version
javac --release 21 HashMapFlowLab.java
java -ea HashMapFlowLab
Giá trị suy ra từ phép mask — không phải kết quả đo hiệu năng
Raw hashSpread hashIndex, capacity 16Index, capacity 32Bit split
1111Không có bit 16
1717117Có bit 16 → cộng 16
333311Bit 32 chưa tham gia mask mới
65 53665 53711High bit đã ảnh hưởng low bit

Expected observations. Với hai map khởi tạo capacity 16 và load factor mặc định trong ví dụ, kiểm tra lần thêm mapping thứ 13 đi qua resize. Với map capacity 64 và constant hash, theo dõi bin trước/sau lần put thứ 9. Bổ sung counters cho hashCode/equals theo dataset; ghi số operation đã thực hiện, không biến số đếm quan sát trên một build thành guarantee chung.

Acceptance gate. Cả 13 mapping ở nhóm unique và nhóm split đều lookup được; 9 key constant-hash không bị gộp. Bảng index khớp quy tắc split. Case mutable key vẫn có size 1 nhưng get/remove với hash đã đổi thất bại trong thí nghiệm này; bản StableKey lookup bằng một key equal thành công. Đây là thí nghiệm ngoài contract mutable-key, không phải lời hứa mọi mutation đều cho cùng kết quả. [3]

Artifact cần nộp. Source test; JDK/build; bảng key/raw hash/spread hash/old index/new index; ảnh debugger tại ba breakpoint; số operation/so sánh và assertion log chứng minh không mất mapping ở các case key ổn định.

Lab B · List locality và complexity

Các bước bắt buộc

  1. Dùng JMH so ArrayList/LinkedList cho traversal, random get, append và insert qua iterator.
  2. Có warmup/fork, consume result, cùng data size và GC profiler.
  3. Không dùng một microbenchmark để kết luận mọi workload; giải thích allocation/cache locality.

Gate: kết luận kèm workload, variance, allocation và profiler—not chỉ một con số milliseconds.

Thiết kế benchmark trước khi chạy

Workload definition. Ví dụ JMH sau dùng cùng size, cùng chuỗi chỉ số random và state riêng mỗi thread. Return value được JMH consume. Hai benchmark ghi có bước undo để giữ size ổn định; phải báo cáo cả cặp thao tác, không đặt tên kết quả là latency của một lần add. [21]

Bốn nhóm workload bắt buộc và phần chi phí thực sự được đo
Nhóm nguồnMethod trong ví dụScope / điều cần bổ sung
TraversaltraversalMột lượt duyệt toàn bộ n phần tử, không phải một phần tử.
Random getrandomGetMột lookup theo chuỗi index định trước; không đo random generator.
AppendappendAndUndoAppend + remove cuối. ArrayList có slot dự phòng, chưa đo grow.
Insert qua iteratoriteratorInsertAndUndoAdd + previous + remove; không tính chi phí định vị iterator ban đầu.
CollectionsBench.java · Java 21 · chạy bằng JMH harness
package org.example;

import java.util.*;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@Fork(2)
public class CollectionsBench {
    @Param({"array", "linked"}) public String kind;
    @Param({"1000", "10000"}) public int n;
    private List<Integer> list;
    private ListIterator<Integer> middle;
    private final int[] indices = new int[1024];
    private int cursor;
    private final Integer marker = Integer.MIN_VALUE;

    @Setup(Level.Trial)
    public void setup() {
        // ArrayList gets one spare slot: this is NOT a resize benchmark.
        list = kind.equals("array")
            ? new ArrayList<>(n + 1) : new LinkedList<>();
        for (int i = 0; i < n; i++) list.add(i);
        middle = list.listIterator(n / 2); // locating cost excluded
        var random = new Random(7);
        for (int i = 0; i < indices.length; i++)
            indices[i] = random.nextInt(n);
    }

    @Benchmark public long traversal() {
        long total = 0;
        for (int value : list) total += value;
        return total; // JMH consumes the returned result
    }

    @Benchmark public int randomGet() {
        return list.get(indices[cursor++ & (indices.length - 1)]);
    }

    @Benchmark public Integer appendAndUndo() {
        list.add(marker);
        return list.remove(n); // restore size; measures an operation pair
    }

    @Benchmark public Integer iteratorInsertAndUndo() {
        middle.add(marker);
        Integer value = middle.previous();
        middle.remove(); // restores the original cursor position and size
        return value;    // measures add + previous + remove, not add alone
    }
}

Chạy bằng project JMH riêng. Cần JDK và Maven/JMH trong môi trường lab; đây không phải dependency của trang HTML. Tạo project, lưu class ở src/main/java/org/example/CollectionsBench.java, rồi build và chạy. Ghi lại version JMH thực tế được resolve và cấu hình build; không mặc định một version là mới nhất. [21]

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=org.example -DartifactId=collections-bench -Dversion=1.0
cd collections-bench
# Save CollectionsBench.java in src/main/java/org/example/
mvn clean verify
java -jar target/benchmarks.jar "org.example.CollectionsBench.*" -wi 3 -i 5 -f 2 -prof gc -rf json -rff results.json
Không suy diễn quá phần đã đo. Giữ hai biến thể bổ sung: single-append khi còn capacity và single-append gây grow; đồng thời đo end-to-end gồm định vị iterator nếu workload thật làm việc đó. Reset state ngoài phép đo phải được mô tả rõ. @Setup(Level.Invocation) có thể ảnh hưởng timer overhead, cache và allocation profiler; không coi nó là cách tách chi phí hoàn hảo. [22]

Failure injection. Chạy thêm indexed traversal get(i) trên LinkedList để thấy việc “duyệt” có thể vô tình thành nhiều lần tìm node. So với iterator traversal; không gộp hai workload dưới cùng nhãn. Thử size vượt vùng cache thuận lợi, nhưng không dự đoán trước bên nào thắng ở mọi size.

Acceptance gate. Báo cáo đủ bốn nhóm với size, operation unit, warmup, measurement, forks, JDK, JMH, CPU và GC settings. Đưa score/error hoặc phân bố qua các fork, allocation/op, GC metrics và profiler evidence khi khả dụng. -prof gc cho biết allocation, không tự chứng minh cache misses; không so số microsecond của cả lượt traversal với một get như thể cùng đơn vị công việc. [23]

Artifact cần nộp. Source benchmark và các biến thể; build/version manifest; command line; raw results; GC/profiler report; nhận xét về locality/allocation và giới hạn áp dụng. Không chốt “ArrayList/LinkedList luôn nhanh hơn” từ một số milliseconds.

Lab C · Heap và ordered structures

Các bước bắt buộc

  1. Implement binary min-heap bằng array với sift-up/down.
  2. Giải top-K bằng heap; so output/complexity với full sort.
  3. Dùng TreeMap giải range/floor/ceiling; tạo comparator trả 0 cho keys không equal và quan sát overwrite.

Heap invariant trước, hiệu năng sau

Setup. Cài min-heap trên int[] theo parent (i - 1) / 2, children 2i + 1/2i + 2. Ví dụ dùng capacity cố định để làm rõ full/empty policy; nó không nhằm thay thế toàn bộ API của PriorityQueue. Đối chiếu mỗi poll với PriorityQueue, không dùng thứ tự iterator của PriorityQueue làm oracle. [10]

MinHeapLab.java · Java 21 · chạy với -ea
import java.util.*;

public class MinHeapLab {
    // Educational bounded heap. Capacity is an explicit contract.
    static final class MinHeap {
        private final int[] data;
        private int size;

        MinHeap(int capacity) {
            if (capacity < 1) throw new IllegalArgumentException("capacity > 0");
            data = new int[capacity];
        }

        void offer(int value) {
            if (size == data.length) throw new IllegalStateException("full");
            int child = size++;
            while (child > 0) {
                int parent = (child - 1) / 2;
                if (data[parent] <= value) break;
                data[child] = data[parent];
                child = parent;
            }
            data[child] = value;
        }

        int poll() {
            if (size == 0) throw new NoSuchElementException("empty");
            int root = data[0];
            int last = data[--size];
            int parent = 0;
            while (2 * parent + 1 < size) {
                int child = 2 * parent + 1;
                if (child + 1 < size && data[child + 1] < data[child])
                    child++;
                if (last <= data[child]) break;
                data[parent] = data[child];
                parent = child;
            }
            if (size > 0) data[parent] = last;
            return root;
        }

        boolean valid() {
            for (int i = 1; i < size; i++)
                if (data[(i - 1) / 2] > data[i]) return false;
            return true;
        }
    }

    public static void main(String[] args) {
        var heap = new MinHeap(1_024);
        var oracle = new PriorityQueue<Integer>();
        var random = new Random(7);
        for (int i = 0; i < 1_000; i++) {
            if (oracle.isEmpty() || random.nextBoolean()) {
                int value = random.nextInt();
                heap.offer(value);
                oracle.offer(value);
            } else {
                int actual = heap.poll();
                int expected = oracle.remove();
                assert actual == expected;
            }
            assert heap.valid();
        }
        while (!oracle.isEmpty()) {
            int actual = heap.poll();
            int expected = oracle.remove();
            assert actual == expected;
            assert heap.valid();
        }
        boolean emptyRejected = false;
        try { heap.poll(); }
        catch (NoSuchElementException expected) { emptyRejected = true; }
        assert emptyRejected;

        var range = new TreeMap<Integer, String>();
        range.put(10, "A"); range.put(20, "B"); range.put(30, "C");
        assert range.floorKey(25) == 20;
        assert range.ceilingKey(25) == 30;
        assert range.subMap(10, true, 30, false).keySet()
            .equals(Set.of(10, 20));

        Comparator<String> byLength = Comparator.comparingInt(String::length);
        var wrong = new TreeMap<String, Integer>(byLength);
        wrong.put("aa", 1); wrong.put("bb", 2);
        assert !"aa".equals("bb");
        assert wrong.size() == 1 && wrong.get("aa") == 2;
        var fixed = new TreeMap<String, Integer>(
            byLength.thenComparing(Comparator.naturalOrder()));
        fixed.put("aa", 1); fixed.put("bb", 2);
        assert fixed.size() == 2;
        System.out.println("PASS MinHeapLab: heap + range + comparator");
    }
}
javac --release 21 MinHeapLab.java TopKDemo.java
java -ea MinHeapLab
java -ea TopKDemo

Failure injection. Bổ sung dataset toàn phần tử bằng nhau, tăng dần, giảm dần, số âm và Integer.MIN_VALUE/MAX_VALUE. Kiểm tra queue rỗng, queue đầy; cố ý đổi một dấu so sánh trong sift-up/down để xác nhận invariant test thực sự bắt được lỗi. Với top-K, đưa score trùng và kiểm tra tie-breaker thống nhất với full-sort oracle.

Expected observations. Sau mỗi thao tác, mọi parent không lớn hơn child; poll liên tiếp cho dãy không giảm. Comparator chỉ so độ dài làm “aa” và “bb” dùng chung một key slot ở TreeMap; tie-breaker khôi phục hai mapping nếu đó là semantics cần có. Range [10, 30) chứa 10 và 20; floor/ceiling của 25 là 20/30. Submap là backed view, không phải bản sao độc lập. [9]

Acceptance gate. Heap khớp oracle qua seed cố định và các edge case; top-K khớp full sort với cùng comparator ở tập nhỏ. Báo cáo O(log n) cho sift path của heap n phần tử, O(n log k) cho input top-K với k ≥ 2 và O(k) bộ nhớ phụ; giải thích cả chi phí sort phần output. Chạy bài 100 triệu record bằng input streaming, không bắt buộc nạp toàn bộ để chứng minh kết quả.

Artifact cần nộp. Heap implementation, tests/seed, trace của một sift-up và một sift-down, đối chiếu top-K/full-sort, log comparator-overwrite và range/floor/ceiling. Nếu đo hiệu năng, lưu số liệu thực tế thay vì gán trước một tỉ lệ nhanh hơn.

Lab D · Concurrent compound operations

Các bước bắt buộc

  1. Nhiều threads chạy contains-then-put/increment trên synchronized start barrier.
  2. Đếm lost construction/update; sửa bằng computeIfAbsentLongAdder.
  3. Đưa sleep/remote-call giả vào mapping function để quan sát contention; chuyển I/O khỏi atomic map operation.
  4. So fail-fast, snapshot và weakly consistent iteration bằng assertions phù hợp semantics.

Tái hiện race bằng phối hợp thread, không bằng may rủi

Setup. Hai barrier bảo đảm hai thread cùng quan sát trạng thái cũ trước khi ghi; start barrier đơn lẻ chỉ giúp bắt đầu cùng lúc, chưa ép được interleaving cần kiểm tra. Chương trình tách redundant construction khỏi lost update, rồi sửa counter bằng computeIfAbsent + LongAdder. Mọi wait đều có timeout để lỗi lab không treo vô hạn.

CompoundRaceLab.java · Java 21 · chạy với -ea
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class CompoundRaceLab {
    static void await(CyclicBarrier barrier) throws Exception {
        barrier.await(5, TimeUnit.SECONDS);
    }

    static void runTwo(Callable<Void> task) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        try {
            Future<Void> first = pool.submit(task);
            Future<Void> second = pool.submit(task);
            first.get(10, TimeUnit.SECONDS);
            second.get(10, TimeUnit.SECONDS);
        } finally {
            pool.shutdownNow();
            if (!pool.awaitTermination(5, TimeUnit.SECONDS))
                throw new IllegalStateException("workers did not terminate");
        }
    }

    public static void main(String[] args) throws Exception {
        var start1 = new CyclicBarrier(2);
        var afterCheck = new CyclicBarrier(2);
        var bad = new ConcurrentHashMap<String, Object>();
        var builds = new AtomicInteger();
        runTwo(() -> {
            await(start1);
            boolean missing = !bad.containsKey("key");
            await(afterCheck); // both checks finish before either put
            if (missing) {
                builds.incrementAndGet();
                bad.put("key", new Object());
            }
            return null;
        });
        assert bad.size() == 1 && builds.get() == 2;

        var start2 = new CyclicBarrier(2);
        var afterRead = new CyclicBarrier(2);
        var count = new ConcurrentHashMap<String, Integer>();
        count.put("key", 0);
        runTwo(() -> {
            await(start2);
            int old = count.get("key");
            await(afterRead); // both threads read 0
            count.put("key", old + 1);
            return null;
        });
        assert count.get("key") == 1; // intended total is 2

        var start3 = new CyclicBarrier(2);
        var fixed = new ConcurrentHashMap<String, LongAdder>();
        var fixedBuilds = new AtomicInteger();
        runTwo(() -> {
            await(start3);
            for (int i = 0; i < 10_000; i++) {
                fixed.computeIfAbsent("key", k -> {
                    fixedBuilds.incrementAndGet();
                    return new LongAdder();
                }).increment();
            }
            return null;
        });
        // Both futures completed; no remove/reset/clear occurs in this test.
        assert fixedBuilds.get() == 1;
        assert fixed.get("key").sum() == 20_000;

        var ordinary = new ArrayList<>(List.of(1, 2));
        Iterator<Integer> failFast = ordinary.iterator();
        ordinary.add(3);
        boolean detected = false;
        try { failFast.next(); }
        catch (ConcurrentModificationException expected) { detected = true; }
        assert detected; // controlled single-thread OpenJDK illustration

        var cow = new CopyOnWriteArrayList<>(List.of(1, 2));
        Iterator<Integer> snapshot = cow.iterator();
        cow.add(3);
        var snapshotSeen = new ArrayList<Integer>();
        snapshot.forEachRemaining(snapshotSeen::add);
        assert snapshotSeen.equals(List.of(1, 2));

        var concurrent = new ConcurrentHashMap<Integer, String>();
        concurrent.put(1, "A"); concurrent.put(2, "B");
        Iterator<Integer> weak = concurrent.keySet().iterator();
        concurrent.put(3, "C");
        var weakSeen = new HashSet<Integer>();
        while (weak.hasNext()) {
            int key = weak.next();
            assert Set.of(1, 2, 3).contains(key);
            boolean newKey = weakSeen.add(key);
            assert newKey; // each stable key at most once
        }
        assert weakSeen.containsAll(Set.of(1, 2));
        // Do not demand that 3 is present, absent, or in any fixed order.
        System.out.println("PASS CompoundRaceLab: reproduced races; fixed=20000");
    }
}
javac --release 21 CompoundRaceLab.java
java -ea CompoundRaceLab
Đọc đúng các số trong assertion. 2 construction / 1 mapping và lost-update cuối bằng 1 là interleaving được chủ động dựng. Tổng 20 000 đến từ 2 worker × 10 000 increment sau khi cả hai future hoàn thành; đó không phải throughput, benchmark hoặc guarantee snapshot trong lúc ghi. [15] [20]

Thí nghiệm callback chậm bắt buộc

Trong một biến thể riêng, đặt sleep hoặc remote-call giả có timeout vào callback; dùng latch báo “callback đã vào”, rồi từ controller gửi thêm request cùng key và key khác. Ghi thời gian chờ theo operation và chụp thread dump/JFR khi có contention. Một số update có thể phải đợi callback; không suy ra mọi read đều khóa hay mọi key khác đều độc lập. [14]

Chuyển I/O ra ngoài atomic map operation và đo lại. putIfAbsent chỉ quyết định value thắng; nó không tự ngăn nhiều request cùng tải dữ liệu trước khi put. Nếu cần một load đang chạy cho mỗi key, thiết kế publication của future ngắn gọn, chỉ owner thực hiện I/O ngoài callback, kèm cleanup/retry/cancellation khi lỗi. Không đặt barrier chờ hai callback cùng-key cùng đi vào bên trong compute: thread thứ hai có thể đang bị chặn bởi chính atomic operation.

Oracle cho iteration

Ba semantics nguồn — assertion phải theo contract, không theo một lịch chạy
LoạiCó thể kiểm traKhông được kết luận
Fail-fast · ArrayListControlled example sửa cấu trúc rồi gọi next để minh họa CME trên build đã chọn; test mutation hợp lệ qua iterator riêng.Không có CME không chứng minh thread-safe; không yêu cầu mọi data race đều ném CME.
Snapshot · CopyOnWriteArrayListIterator giữ tập reference lúc tạo; thêm/xóa sau đó không đổi sequence của iterator đó.Không suy ra object phần tử được deep-copy; iterator mutation không được hỗ trợ.
Weakly consistent · ConcurrentHashMapKhông CME; trong test chỉ thêm key mới, các key ban đầu giữ nguyên phải được duyệt không lặp; key thêm sau có thể thấy hoặc không.Không bắt buộc sorted order, một global point-in-time snapshot, hoặc thấy mọi update vừa xảy ra.

Nguồn cho các oracle: ArrayList, CopyOnWriteArrayList và java.util.concurrent. [6] [19] [20]

Acceptance gate. Bản lỗi tái hiện được cả construction dư và lost update; bản sửa cho tổng đúng sau khi join, kể cả nhiều lần chạy và hot-key contention. Giữ rõ precondition: không có remove/reset/clear trong lúc increment. Nếu thêm eviction, phải thiết kế lại để không increment một LongAdder đã bị tách khỏi map. Mapping lifecycle đúng không tự chứng minh business side effect chỉ xảy ra một lần.

Artifact cần nộp. Source/barrier schedule, construction/update counts, repeated-run assertion logs, callback latency cùng thread dump/JFR, before/after I/O placement và bảng assertion cho cả ba iterator semantics. Phân biệt lỗi thuật toán với một lần chạy chưa kích hoạt được race.

Checklist chọn collection

Chỉ đánh dấu hoàn thành khi có test, số liệu hoặc quyết định thiết kế đi kèm — không chỉ vì nhớ tên class.

Định hướng nhanh, chưa thay thế workload test

Ứng viên để kiểm chứng, không phải một lựa chọn đúng cho mọi tình huống
Nhu cầu chínhỨng viênĐiều phải chốt trước
Index access / sequential traversalArrayList; đối chiếu LinkedList khi đã có iteratorChi phí tìm vị trí, shifting, allocation và grow. [6] [7]
FIFO/LIFO nội bộ một threadArrayDequeKhông null; không có thread-safety tự động. [8]
Lookup / membership theo equalityHashMap / HashSetKey ổn định, hash distribution, null và order policy. [1] [5]
Sorted keys / range navigationTreeMapComparator, equivalence và backed view. [9]
Priority / streaming top-KPriorityQueueMin hay max, tie-breaker, có cần sorted output không. [10]
Shared counter / producer–consumerConcurrentHashMap + LongAdder / bounded BlockingQueueAtomic boundary, lifecycle, overload và snapshot scope. [14] [15] [16]
Read-mostly snapshot / read-only APICopyOnWriteArrayList / unmodifiable view / List.copyOfWrite/copy cost, live backing và mutable elements. [12] [13] [19]
Điểm dừng trước production: không có một collection đơn lẻ nào tự giải quyết cùng lúc business transaction, overload, deep immutability và distributed correctness. Câu trả lời cần nêu chính xác điều class bảo đảm, điều ứng dụng phải bảo đảm và bằng chứng đã kiểm tra.

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

API references là Java SE 21. Source HashMap được pin theo tag; các trang JMH phục vụ thiết kế lab. Đối chiếu ngày 12/09/2026; không dùng nhãn “mới nhất” để thay cho version cụ thể.

  1. [1] HashMap · API, complexity, synchronization
  2. [2] HashMap.java · OpenJDK tag jdk-21+35
  3. [3] Map · equality và mutable keys
  4. [4] Record · shallow immutability
  5. [5] HashSet · backing map và add
  6. [6] ArrayList · growth và fail-fast
  7. [7] LinkedList · indexed access và iterator
  8. [8] ArrayDeque · deque contract
  9. [9] TreeMap · comparator, range, navigation
  10. [10] PriorityQueue · heap, order, complexity
  11. [11] LinkedHashMap · access order và eviction
  12. [12] List · of và copyOf
  13. [13] Collections · unmodifiable/synchronized wrappers
  14. [14] ConcurrentHashMap · atomic methods và callbacks
  15. [15] LongAdder · sum và reset boundaries
  16. [16] BlockingQueue · operation families
  17. [17] ArrayBlockingQueue · bounded capacity
  18. [18] ConcurrentLinkedQueue · unbounded queue
  19. [19] CopyOnWriteArrayList · snapshot iterator
  20. [20] java.util.concurrent · iterator, happens-before
  21. [21] OpenJDK JMH · project setup và chạy benchmark
  22. [22] JMH sample 38 · per-invocation setup caveats
  23. [23] JMH sample 35 · profilers và allocation metrics