Array, linked nodes và circular buffer
Cùng implement List hoặc Deque, nhưng layout khác nhau làm cache locality, allocation và operation cost khác hẳn.
ArrayList: size khác capacity
ArrayList<String> names = new ArrayList<>();
names.add("An"); // writes elementData[size], then size++
names.add(0, "Binh"); // shifts existing range to the right
ArrayList giữ một Object[] elementData và size. Capacity là độ dài array; size là số phần tử logic. get(i) bounds-check rồi đọc trực tiếp array. Append chỉ grow khi array đầy; vì nhiều append rẻ xen một lần copy O(n), cost amortized là O(1).
Growth và mutation
- Growth tạo array lớn hơn rồi copy references; công thức tăng capacity là implementation detail.
- Insert giữa dùng array copy để shift suffix; remove cũng shift trái và null slot cuối để GC có thể thu object.
ensureCapacityhữu ích khi biết trước kích thước lớn; tránh nhiều lần grow, nhưng reserve quá mức lãng phí memory.trimToSizecopy lại array và không nên gọi máy móc trong hot path.
Invariant · OpenJDK 21 0 ≤ size ≤ elementData.length. Chỉ khoảng [0, size) là phần tử logic; reserve capacity không tạo thêm phần tử. Copy references không phải clone object. Null slot sau remove chỉ bỏ một đường giữ reference, không bảo đảm object được GC ngay hoặc không còn reference ở nơi khác. [B-R2]
Phân biệt độ phức tạp: get/set là O(1); append có thể gặp grow O(n), nhưng amortized O(1). Insert ở index i dịch size - i references; remove dịch phần phía sau, còn remove cuối không phải dịch. Nếu capacity tăng theo cấp số nhân, tổng số reference copy qua chuỗi grow bị chặn bởi một bội số của n; đây là trực giác cho amortized, không phải bảo đảm latency của từng call. [B-R1] [B-R2]
trimToSize() chỉ cần tạo storage khác khi có dung lượng dư; không nên hiểu mọi call đều copy. [B-R2]Production trade-off: biết trước lượng dữ liệu thì reserve hợp lý để giảm lần grow; reserve quá lớn giữ bộ nhớ không dùng. Đừng gọi trimToSize() sau mỗi append vì có thể tạo vòng copy–grow. Tách profile allocation khỏi thời gian xử lý item để biết bottleneck thật.
LinkedList: O(1) chỉ sau khi đã có node
first ↔ Node(item,next,prev) ↔ Node ↔ last
Mỗi element nằm trong node có item/next/prev. get(index) chọn đi từ first hoặc last tùy index nhưng vẫn O(n). Add/remove ở hai đầu O(1). Insert/remove tại index vẫn phải traverse O(n); chỉ unlink O(1) sau khi tìm thấy node.
- Mỗi element thêm object node và references, tăng memory/GC.
- Pointer chasing có locality kém.
- Implement cả
ListvàDeque, nhưngArrayList/ArrayDequethường là mặc định tốt hơn. - Iterator đang đứng tại node có thể add/remove quanh vị trí hiệu quả, nhưng use case này ít hơn người ta tưởng.
API và implementation Public API không expose Node. “Đã có node” trong use case thực tế thường là một ListIterator đã được định vị. Chi phí định vị chưa tự biến mất; tạo iterator ở giữa vẫn có thể phải traverse. [B-R3]
// Đoạn lệnh trong một method; cần import java.util.*;
LinkedList<String> tasks = new LinkedList<>(List.of("A", "B", "C"));
ListIterator<String> cursor = tasks.listIterator(1); // Tính cả traversal
cursor.add("X"); // Chèn trước B tại vị trí đã có
String current = cursor.next(); // B
cursor.remove(); // Unlink B, không tìm lại theo index
System.out.println(tasks); // [A, X, C]
Bẫy workload: vòng for (i = 0; i < list.size(); i++) list.get(i) trên LinkedList có thể thành O(n²), trong khi iterator duyệt tuần tự là O(n). Đo “insert giữa” bằng add(index, value) khác đo mutation qua iterator đã có; phải đặt tên benchmark theo phần việc thực sự đo. Đây là suy luận từ đường traversal và link/unlink. [B-R3]
ArrayDeque: circular array
index: 0 1 2 3 4 5 6
elements: [D, E, _, _, A, B, C]
head = 4 -> A (phan tu dau)
tail = 2 -> _ (slot se them o cuoi)
logical order: A -> B -> C -> D -> E
_ = null; day la state minh hoa, khong phai capacity mac dinh.
Sơ đồ nguồn được căn lại theo index: head ở A, tail ở ô trống sau E. Tail không trỏ phần tử cuối hiện tại.
head trỏ phần tử đầu; tail trỏ slot sẽ insert tiếp. Index wrap quanh array bằng arithmetic/bit operations tùy implementation. Add/remove hai đầu không shift toàn bộ array; khi đầy, deque grow và rearrange/copy.
- Không chấp nhận
null, nhờ đópolltrả null biểu diễn empty rõ ràng. - Dùng làm stack qua
push/pop/peek, thay legacyStack. - Dùng làm queue qua
offer/poll/peek. - Không thread-safe; shared producer/consumer cần concurrent/blocking deque/queue phù hợp.
Implementation · OpenJDK 21 Ở state ổn định, các ô không chứa phần tử là null và có ít nhất một ô trống tại tail. Snapshot này dùng helper tăng/giảm index có kiểm tra biên; không mặc định dùng index & (length - 1), vì phép mask chỉ phù hợp với thiết kế capacity lũy thừa hai. Khi grow phải giữ encounter order ngay cả khi head > tail. [B-R5]
| Mục đích | Thao tác | Điều cần nhớ |
|---|---|---|
| FIFO queue | offerLast(e) / pollFirst() | offer/poll/peek tương ứng thêm cuối/lấy đầu/xem đầu. |
| LIFO stack | push(e) / pop() | Thêm/lấy ở đầu; peek() chỉ xem, không remove. |
| Đọc khi empty | peekFirst/peekLast | Trả null; không nhầm với việc lưu một null element. |
| Remove khi empty | pollFirst/pollLast hoặc removeFirst/removeLast | Poll trả null; remove và pop ném NoSuchElementException. |
| Tìm/xóa theo giá trị | contains, removeFirstOccurrence | Phải scan; không có cùng cost với thao tác hai đầu. |
Đối chiếu method semantics trong Javadoc, không suy từ tên queue/stack ra chính sách concurrency. [B-R4]
CopyOnWriteArrayList: snapshot bằng copy
Copy-on-write tạo/publish backing array mới khi cần thay đổi nội dung list, với writer được đồng bộ; readers đọc array hiện có mà không lấy writer lock. Iterator giữ snapshot của array tại lúc được tạo nên không thấy các lần add/remove/set trên list sau đó. Đây là mô hình chính; không nên diễn giải mọi lời gọi mutation đều bắt buộc allocate/copy.
| Phù hợp | Không phù hợp |
|---|---|
| Listener/config list nhỏ, reads/iterations áp đảo writes, cần snapshot traversal. | Large list, write thường xuyên, element mutation cần consistency hoặc memory budget chặt. |
“Thread-safe list” không có nghĩa compound business invariant atomic. Hai calls riêng vẫn có thể interleave; snapshot chỉ bảo vệ structure, không làm element mutable trở nên thread-safe.
Scope guarantee Snapshot giữ dãy references, không deep-copy từng object. Một iterator giữ references cũ dù list đã remove/replace phần tử; object mutable mà nó trỏ tới vẫn có thể thay đổi. Iterator/listIterator của CopyOnWriteArrayList không hỗ trợ remove, set, add; các thao tác đó ném UnsupportedOperationException. [B-R6]
jdk-21-ga, set(i, sameReference) có nhánh không clone array; addIfAbsent khi phần tử đã tồn tại cũng có thể không thay đổi storage. Với mutation thực sự làm thay đổi dãy phần tử, chi phí copy/allocation vẫn là điểm cần tính. [B-R7]Business invariant: if (!listeners.contains(x)) listeners.add(x) là hai call riêng, có thể bị interleave. Dùng addIfAbsent(x) cho đúng yêu cầu “thêm nếu chưa tồn tại”, nhưng một API atomic không tự bao phủ transaction bên ngoài list. Tương tự, size() rồi get(i) không phải cùng một snapshot nếu có writer chen vào. [B-R6] [B-R7]
Memory trade-off: iterator còn sống có thể giữ array cũ và references tới elements đã bị xóa khỏi list hiện tại. Đây là hệ quả của snapshot, không tự động là memory leak. Kiểm tra lifetime khi giữ iterator lâu, list lớn hoặc writers chạy nhiều; tránh biến “read không lấy writer lock” thành “mọi nghiệp vụ không cần đồng bộ”. [B-R7]
subList và structural modification
subList(from,to) trả backed view, không phải copy; khoảng lấy là [from, to). Ở ArrayList, view giữ offset/size và liên hệ list gốc. Mutation qua view, nếu operation được hỗ trợ, phản ánh vào backing list. Structural mutation trực tiếp trên backing list ngoài view làm semantics của view không xác định theo contract; trong các implementation fail-fast thường gặp ConcurrentModificationException.
Ownership Trên ArrayList, set thay element không phải structural change; view.set và view.clear là cách sửa có chủ đích qua view. Sau parent.add ngoài view, đừng tiếp tục dùng view như thể luôn được tự đồng bộ lại. Hành vi không xác định của view không biến CME thành một giao thức recovery đáng tin cậy. [B-R1] [B-R8]
// Đoạn lệnh trong một method; cần import java.util.*;
List<String> parent = new ArrayList<>(List.of("A", "B", "C", "D"));
List<String> view = parent.subList(1, 3); // [B, C]
view.set(0, "X"); // parent: [A, X, C, D]
List<String> detached = new ArrayList<>(view); // Shallow copy: [X, C]
view.clear(); // parent: [A, D]
parent.add("E"); // Không dùng lại view sau bước này
System.out.println(detached); // [X, C]
Khi cần tách ownership: new ArrayList<>(view) tạo list độc lập về cấu trúc nhưng còn chia sẻ references. List.copyOf(view) tạo list không cho sửa, không phản ánh sửa đổi sau đó của source và từ chối null; cũng không deep-copy elements. Với source có writers, thao tác tạo copy vẫn cần một cách đọc source an toàn. [B-R8]
Retention: một subList ArrayList nhỏ có thể giữ root list và backing array lớn còn reachable. Khi chỉ cần vài phần tử lâu dài, cân nhắc copy riêng và kiểm tra heap ownership thay vì giữ view không chủ đích. Đây là suy luận từ field liên hệ root của implementation, không phải số byte cố định cho mọi JVM. [B-R2]
Iterator semantics: fail-fast, snapshot và weak consistency
Bổ sung · Tách cơ chế duyệt khỏi thread-safety
| Nhóm | Quan sát mutation | Client được dựa vào điều gì? |
|---|---|---|
| ArrayList / LinkedList | Fail-fast khi phát hiện structural modification không hợp lệ. | CME là best effort. Dùng remove của chính iterator; dùng add/remove qua ListIterator khi phù hợp. |
| ArrayDeque | Fail-fast, không phải snapshot và không thread-safe. | Không giả định cùng cơ chế modCount với mọi collection; xem contract và source class cụ thể. |
| CopyOnWriteArrayList | Iterator đọc snapshot lúc tạo; không thấy list writes về sau. | Duyệt không cần writer lock; iterator mutation không được hỗ trợ; mutable element không được đóng băng. |
| Concurrent collections có weakly consistent iterator | Có thể thấy một số thay đổi trong khi duyệt, không hứa global snapshot. | Đọc guarantee của từng class; không coi toàn bộ java.util.concurrent đều có semantics này. |
Cơ chế fail-fast thuộc class/iterator cụ thể; weak consistency không đồng nghĩa snapshot. [B-R1] [B-R3] [B-R4] [B-R6] [B-R9]
list.remove trực tiếp cũng có thể vi phạm quy tắc iterator. Ngược lại, không thấy CME không chứng minh không có data race. Với ArrayList, modCount là cơ chế phát hiện thay đổi, không phải lock hay memory-visibility guarantee. [B-R1] [B-R2]Review rule: trước khi thêm retry/catch CME, xác định ai sở hữu collection, có writers không, cần snapshot nào và operation nào phải atomic. Không retry mù trên một cấu trúc đang bị sửa không có protocol.
Interview quick answers
Vì sao ArrayList append là amortized O(1)?
Bằng chứng nên đưa: vẽ chuỗi capacity tăng và tổng references được copy; tách cost của một lần grow khỏi cost phân bổ. Không gắn amortized O(1) với latency cố định.
Khi nào LinkedList hơn ArrayList?
Bằng chứng nên đưa: so hai workload riêng — tìm index rồi insert, và mutation qua iterator đã định vị. Đo cả allocation/GC; LinkedList hỗ trợ null nhưng null trong queue dễ làm mơ hồ empty semantics.
ArrayDeque hơn Stack thế nào?
Stack là legacy subclass của Vector với synchronized API và List inheritance không cần thiết. ArrayDeque là Deque chuyên biệt, thường ít overhead và API rõ cho stack/queue.Giới hạn: ArrayDeque không thread-safe; đây không phải drop-in replacement giữ nguyên đặc tính đồng bộ của Stack. Với shared stack, phải thiết kế lại ownership/synchronization. [B-R4]
Practice
Bốn mục tiêu nguồn được giữ nguyên dưới đây. Mỗi lab tiếp theo cụ thể hóa cách kiểm tra và artifact tương ứng; không tăng số lab của chuyên đề.
- Viết dynamic array nhỏ có
add/get/remove/grow; assert slot bị remove được null. - Benchmark traversal/append/middle insert của ArrayList và LinkedList với JMH, có warmup và không dead-code.
- Implement circular deque, test wrap-around và grow khi head lớn hơn tail.
- Tạo CopyOnWrite iterator, mutate list rồi chứng minh iterator thấy snapshot cũ.
Lab 01 · Dynamic array: thấy được slot và grow
Goal. Tự viết một dynamic array nhỏ có add/get/remove/grow, giữ đúng thứ tự, bounds và không giữ reference thừa ở phần ngoài size.
Việc phải làm. Dùng Object[] và size; public methods kiểm tra index trước khi sửa state. Chọn và ghi rõ growth policy của riêng lab, kể cả capacity bằng 0; kiểm tra overflow khi tính capacity mới thay vì để phép cộng int wrap âm. Khi remove, lưu item trả về, shift phần suffix, giảm size và xóa reference ở slot cuối cũ.
// Pseudocode cho remove(index), KHONG la toan bo class:
check 0 <= index < size
removed = elements[index]
copy [index + 1, size) sang [index, size - 1)
size = size - 1
elements[size] = null
return removed
Failure-first. Bắt đầu bằng test cố tình làm lộ lỗi: capacity = 0 rồi add; grow qua nhiều boundary; remove phần tử đầu, giữa, cuối; get(-1), get(size); remove trên empty. Tạm bỏ câu null slot để thấy test retention fail, sau đó khôi phục.
Acceptance. Sau mỗi operation, 0 ≤ size ≤ capacity; contents khớp model ArrayList; các slot từ size đến capacity đều null sau remove. Test ít nhất một grow giữ đủ thứ tự và object identity. Dùng accessor package-private trong class tự viết để assert storage; không cần reflection vào JDK, không dùng “GC chạy ngay” làm assertion.
Artifact. Source class + self-test, log boundary cases, một bảng state trước/sau grow và remove, giải thích vì sao xóa reference không đồng nghĩa object lập tức biến mất. Với self-test tên MiniArrayTest, chạy:
javac --release 21 MiniArray.java MiniArrayTest.java
java -ea MiniArrayTest
Hai file trên do người học triển khai; yêu cầu lab không nhúng sẵn một đáp án hoàn chỉnh. Expected result là toàn bộ assertions qua, không phải một thời gian chạy cố định.
Lab 02 · JMH: traversal, append và middle insert
Goal. So ArrayList/LinkedList bằng workload được định nghĩa, có warmup và chống dead-code elimination. Không dùng một lần nanoTime() để kết luận class nào luôn nhanh hơn.
Setup. Ví dụ dùng JDK 21 và JMH 1.37 như một baseline tái lập, không tuyên bố đây là phiên bản mới nhất. JMH/Maven chỉ là công cụ người học cài để chạy lab; hai trang HTML không tải chúng. Tạo project benchmark theo hướng dẫn chính thức, rồi đặt class bên dưới tại src/main/java/org/example/ListDequeBench.java. [B-R10]
mvn archetype:generate -B -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DarchetypeVersion=1.37 -DgroupId=org.example -DartifactId=collections-bench -Dversion=1.0 -Dpackage=org.example
cd collections-bench
mvn clean package
java -jar target/benchmarks.jar "org.example.ListDequeBench.*" -prof gc -rf json -rff results.json
Đặt class vào project trước lệnh build. Mỗi command là một dòng, dùng được trong terminal Windows hoặc POSIX có Java/Maven đúng PATH. Ghi version JDK, JMH và môi trường trong báo cáo.
package org.example;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class ListDequeBench {
@State(Scope.Thread)
public static class Data {
@Param({"array", "linked"})
public String implementation;
@Param({"100", "10000"})
public int size;
public Integer[] seed;
public List<Integer> values;
public Integer token;
public List<Integer> fresh() {
if ("array".equals(implementation)) return new ArrayList<>();
if ("linked".equals(implementation)) return new LinkedList<>();
throw new IllegalArgumentException(implementation);
}
@Setup(Level.Trial)
public void setup() {
seed = new Integer[size];
for (int i = 0; i < size; i++) seed[i] = i;
values = fresh();
Collections.addAll(values, seed);
token = -1;
}
}
@Benchmark
public long traverse(Data data) {
long sum = 0;
for (Integer value : data.values) sum += value;
return sum;
}
@Benchmark
public List<Integer> appendBatch(Data data) {
List<Integer> result = data.fresh();
for (Integer value : data.seed) result.add(value);
return result;
}
@Benchmark
public Integer middleInsertRemovePair(Data data) {
int middle = data.size / 2;
data.values.add(middle, data.token);
return data.values.remove(middle);
}
}
Giới hạn phép đo. Một operation của traverse là duyệt cả list; appendBatch gồm tạo list và append cả batch, có allocation nhưng tái dùng Integer payload đã chuẩn bị. middleInsertRemovePair đo cả insert lẫn remove để khôi phục kích thước; không gắn nhãn kết quả là “pure insert latency”. LinkedList ở benchmark này còn phải tìm index. Returned result giúp JMH giữ lại công việc thay vì loại bỏ như dead code. [B-R11]
Failure-first và biến thể. Viết một bản cố tình bỏ consume/return để nhận diện benchmark sai, nhưng không nộp số đó như kết luận. Thêm case ArrayList pre-sized, so index traversal với iterator traversal, và tách case LinkedList mutation qua iterator đã định vị. Nếu đo pure insert bằng @Setup(Level.Invocation), ghi chi phí setup/timer/GC và tác động đến đo thao tác ngắn; không mặc định cho rằng setup ngoài vùng timing thì hoàn toàn không ảnh hưởng. [B-R12]
Acceptance. Có kết quả cho cả hai implementations, hai sizes và ba workloads; warmup, measurement, forks rõ; không để list tăng vô hạn qua nhiều invocation. Báo score/error/unit và allocation với đúng đơn vị “mỗi traversal”, “mỗi batch” hoặc “mỗi pair”. Không trừ máy móc hai score để bịa latency riêng của insert.
Artifact. Benchmark source, nguyên lệnh chạy, raw JSON, version/CPU/heap/JVM flags, nhận xét về variance và phạm vi kết luận. Không có threshold “ArrayList phải nhanh hơn X lần”; nếu kết quả trái dự đoán, điều tra workload, allocation và measurement trước.
Lab 03 · Circular deque: wrap-around rồi grow
Goal. Implement deque có hai đầu; không làm mất, lặp hoặc đảo thứ tự khi head lớn hơn tail và array phải grow.
Thiết kế lab. Chọn mô hình spare slot: capacity tối thiểu 2, null không được chấp nhận, head == tail là empty, size ≤ capacity - 1. Khi sắp hết chỗ, grow trước lần ghi tiếp theo; đây là thiết kế học tập, không sao chép nguyên thứ tự lệnh của OpenJDK.
// Pseudocode: grow giu nguyen logical order
oldSize = size
newCapacity = growCapacitySafely(oldCapacity)
newElements = array(newCapacity)
for k in [0, oldSize):
newElements[k] = elements[(head + k) mod oldCapacity]
elements = newElements
head = 0
tail = oldSize
// Them tiep o tail; cac slot chua dung phai la null.
Failure-first trace. Bắt đầu với array length 7. OfferLast A…F; pollFirst bốn lần để còn E,F. OfferLast G,H,I,J: lúc này head = 4, tail = 3, size = 6. OfferLast K phải grow; sau grow, pollFirst đến empty phải cho E,F,G,H,I,J,K. Một bản copy theo physical array order thay vì logical order sẽ fail ở đây.
Acceptance. Test empty/singleton, poll/peek không làm sai size, hai đầu xen kẽ, null rejection, wrap nhiều vòng và grow khi head > tail. Sau mỗi bước kiểm tra head/tail hợp lệ, size không âm, unused slots null. Chạy thêm tối thiểu 10.000 operation ngẫu nhiên với seed được ghi lại, so contents với ArrayDeque làm model; so qua list contents, không dùng deque.equals như một content-equality contract.
Artifact. Implementation, self-test, trace các index/state của ca trên và seed/log của differential test. Chạy javac --release 21 MiniDeque.java MiniDequeTest.java rồi java -ea MiniDequeTest sau khi triển khai. Không dùng bit-mask nếu thiết kế không bảo đảm power-of-two capacity.
Lab 04 · CopyOnWrite: chứng minh snapshot cũ
Goal. Tạo iterator, mutate list rồi chỉ ra iterator và list hiện tại đang quan sát hai cấu trúc khác nhau; đồng thời chứng minh snapshot không freeze mutable object.
Steps. Khởi tạo list [A,B], tạo iterator cũ; remove A và add C qua list; collect iterator cũ và tạo traversal mới. Tiếp theo gọi iterator.remove để thấy operation không được hỗ trợ. Cuối cùng lặp thí nghiệm với một mutable element, đổi field sau khi tạo iterator trong cùng thread.
Expected observations. Iterator cũ thấy [A,B]; list hiện tại là [B,C]; iterator.remove ném UnsupportedOperationException. Snapshot mutable element vẫn thấy field vừa đổi vì reference trỏ cùng object. Thử nghiệm cùng thread có chủ đích để không đánh đồng visibility/race với shallow-copy semantics. [B-R6] [B-R7]
Acceptance và artifact. Nộp assertions cho cả bốn quan sát, output thực tế, một object graph chứa array cũ/array mới và giải thích memory lifetime. Không dùng sleep để “chứng minh atomicity”, không suy từ test snapshot rằng compound workflow là atomic. Code kiểm chứng bên dưới có thể dùng làm điểm xuất phát cho lab này.
Code kiểm chứng chung · Java 21
Lưu đoạn này thành CollectionsChecks.java. Đây là harness bổ sung cho các ví dụ và lab 04, không thay thế implementation phải tự viết ở lab 01/03 hay thí nghiệm đo ở lab 02. CME case chỉ là kiểm tra chẩn đoán trên implementation đang chạy.
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public final class CollectionsChecks {
static void check(boolean condition, String label) {
if (!condition) throw new AssertionError(label);
}
static void expect(Class<? extends Throwable> type, Runnable action) {
try {
action.run();
} catch (Throwable error) {
if (type.isInstance(error)) return;
throw new AssertionError("Expected " + type.getSimpleName(), error);
}
throw new AssertionError("Missing " + type.getSimpleName());
}
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("An");
names.add(0, "Binh");
names.ensureCapacity(64);
check(names.size() == 2, "capacity is not size");
check(names.remove(0).equals("Binh"), "remove shifts suffix");
check(names.equals(List.of("An")), "remaining element");
expect(IndexOutOfBoundsException.class, () -> names.get(1));
System.out.println("PASS ArrayList: order, capacity, bounds");
LinkedList<String> tasks =
new LinkedList<>(List.of("A", "B", "C"));
ListIterator<String> cursor = tasks.listIterator(1);
cursor.add("X");
check(cursor.next().equals("B"), "cursor position");
cursor.remove();
check(tasks.equals(List.of("A", "X", "C")), "local mutation");
System.out.println("PASS LinkedList: positioned ListIterator");
Deque<String> queue = new ArrayDeque<>();
queue.offer("A");
queue.offer("B");
check(queue.poll().equals("A"), "FIFO");
queue.push("X");
check(queue.pop().equals("X"), "LIFO at first");
check(queue.poll().equals("B"), "remaining FIFO item");
check(queue.poll() == null, "empty poll");
expect(NoSuchElementException.class, queue::pop);
expect(NullPointerException.class, () -> queue.offer(null));
System.out.println("PASS ArrayDeque: ends, empty, null");
List<String> parent =
new ArrayList<>(List.of("A", "B", "C", "D"));
List<String> view = parent.subList(1, 3);
view.set(0, "X");
check(parent.equals(List.of("A", "X", "C", "D")), "backed view");
List<String> detached = new ArrayList<>(view);
view.clear();
check(parent.equals(List.of("A", "D")), "clear range");
parent.add("E"); // Do not use view after this structural change.
check(detached.equals(List.of("X", "C")), "detached structure");
List<String> readOnly = List.copyOf(detached);
expect(UnsupportedOperationException.class, () -> readOnly.add("Y"));
expect(NullPointerException.class,
() -> List.copyOf(Arrays.asList("X", null)));
System.out.println("PASS subList: write-through and shallow copy");
CopyOnWriteArrayList<String> listeners =
new CopyOnWriteArrayList<>(List.of("A", "B"));
Iterator<String> old = listeners.iterator();
listeners.remove("A");
listeners.add("C");
List<String> observed = new ArrayList<>();
old.forEachRemaining(observed::add);
check(observed.equals(List.of("A", "B")), "old snapshot");
check(listeners.equals(List.of("B", "C")), "current list");
expect(UnsupportedOperationException.class, old::remove);
check(!listeners.addIfAbsent("C"), "absent check is one operation");
int[] box = {1};
CopyOnWriteArrayList<int[]> boxes = new CopyOnWriteArrayList<>();
boxes.add(box);
Iterator<int[]> snapshot = boxes.iterator();
box[0] = 2; // Same thread: deterministic shallow-snapshot observation.
check(snapshot.next()[0] == 2, "snapshot does not freeze object");
System.out.println("PASS CopyOnWrite: snapshot, UOE, mutable element");
ArrayList<String> safe = new ArrayList<>(List.of("A", "B"));
Iterator<String> valid = safe.iterator();
check(valid.next().equals("A"), "first iterator element");
valid.remove();
check(safe.equals(List.of("B")), "iterator-owned remove");
Iterator<String> stale = safe.iterator();
safe.add("C");
// Diagnostic example for the tested OpenJDK 21 implementation only.
expect(ConcurrentModificationException.class, stale::next);
System.out.println("PASS iterator: legal remove; diagnostic CME");
}
}
javac --release 21 CollectionsChecks.java
java -ea CollectionsChecks
Expected output: sáu dòng PASS bên dưới; nếu có assertion fail, giữ nguyên stack trace và kiểm tra assumptions/version thay vì sửa expected value để che lỗi.
PASS ArrayList: order, capacity, bounds
PASS LinkedList: positioned ListIterator
PASS ArrayDeque: ends, empty, null
PASS subList: write-through and shallow copy
PASS CopyOnWrite: snapshot, UOE, mutable element
PASS iterator: legal remove; diagnostic CME