- Đọc được cây phân cấp
Iterable→Collection→List/Set/Queue. - Phân biệt bốn nhóm: List (có thứ tự, cho trùng), Set (duy nhất), Queue (FIFO), Map (khoá-giá trị).
- Biết khi nào
ArrayListvà khi nàoLinkedList— và vì saoArrayListgần như luôn thắng. - Chọn đúng giữa
HashSet,LinkedHashSet,TreeSet. - Dùng
ArrayDequelàm cả ngăn xếp lẫn hàng đợi; hiểuPriorityQueue. - Hiểu
Iteratorvà nguồn gốc củaConcurrentModificationException. - Phân biệt
ComparablevàComparator; viết bộ so sánh nhiều tiêu chí. - Thuộc bảng độ phức tạp — công cụ ra quyết định quan trọng nhất.
- Biết các collection bất biến (
List.of) và cạm bẫy của chúng. - Tránh các lỗi kinh điển: chọn sai cấu trúc, sửa khi đang duyệt, quên
equals/hashCode.
1. Vì sao cần Collections Framework
// ❌ Với mảng: thêm một phần tử là cả một thủ tục
String[] ds = new String[3];
ds[0] = "a"; ds[1] = "b"; ds[2] = "c";
// Muốn thêm "d"? Phải tạo mảng mới và chép lại toàn bộ:
String[] moi = Arrays.copyOf(ds, 4);
moi[3] = "d";
// Muốn xoá phần tử giữa? Tự dịch chuyển mọi phần tử phía sau.
// Muốn kiểm tra "đã có chưa"? Tự viết vòng lặp.
// Muốn sắp xếp theo tiêu chí riêng? Tự cài thuật toán.
// ✅ Với Collections: tất cả đã có sẵn
List<String> ds2 = new ArrayList<>();
ds2.add("a"); ds2.add("b"); ds2.add("c");
ds2.add("d"); // tự nới rộng
ds2.remove("b"); // tự dịch chuyển
ds2.contains("c"); // có sẵn
Collections.sort(ds2); // có sẵn
System.out.println(ds2); // [a, c, d] — toString() tử tế
Collection chỉ chứa đối tượng, không chứa kiểu nguyên thủy
List<int> là bất hợp lệ — phải viết List<Integer>. Java tự
autoboxing khi bạn add(5), nhưng phải trả giá: mỗi số là một object trên heap, tốn bộ nhớ
hơn nhiều và tạo áp lực cho garbage collector.
Với dữ liệu số lượng lớn và thuần số, mảng nguyên thủy int[] vẫn nhanh hơn
List<Integer> khoảng 4-6 lần và tốn bộ nhớ ít hơn khoảng 4 lần. Lý do sâu xa là
type erasure — Chương 9 sẽ giải thích.
2. Cây phân cấp
Map không nằm trong Collection
Vì Collection<E> mô tả "một nhóm phần tử đơn lẻ" — phương thức
add(E) nhận một giá trị. Map lưu cặp khoá-giá trị, nên
put(K, V) có chữ ký hoàn toàn khác. Ép chúng vào cùng một interface sẽ khiến cả hai đều
xấu.
Nhưng chúng liên hệ chặt chẽ: map.keySet() trả về Set<K>,
map.values() trả về Collection<V>, và map.entrySet() trả về
Set<Map.Entry<K,V>> — đó là ba cây cầu nối hai thế giới.
Chương 8 dành trọn cho Map.
3. Bảng so sánh bốn nhóm
| Tiêu chí | List | Set | Queue | Map |
|---|---|---|---|---|
| Định nghĩa | Dãy phần tử có thứ tự | Tập phần tử duy nhất | Xử lý theo thứ tự (thường FIFO) | Tập cặp khoá – giá trị |
| Cho trùng lặp | ✅ Có | ❌ Không | ✅ Có | Khoá: ❌ · Giá trị: ✅ |
| Thứ tự | Đúng thứ tự thêm vào | HashSet: khôngLinkedHashSet: thứ tự thêmTreeSet: đã sắp xếp |
FIFO (vào trước ra trước)PriorityQueue: theo ưu tiên |
HashMap: khôngLinkedHashMap: thứ tự thêmTreeMap: sắp theo khoá |
| Cách truy cập | Theo chỉ số: get(i)/set(i,v) |
Không có chỉ số — chỉ duyệt hoặc contains |
Ở hai đầu: poll()/peek() |
Theo khoá: get(k)/put(k,v) |
| Class tiêu biểu | ArrayList, LinkedList |
HashSet, LinkedHashSet, TreeSet |
ArrayDeque, PriorityQueue |
HashMap, LinkedHashMap, TreeMap |
| Dùng khi | Thứ tự quan trọng; cần truy cập theo vị trí | Cần tính duy nhất; kiểm tra "đã có chưa" thật nhanh | Lập lịch, bộ đệm, hàng đợi công việc | Tra cứu nhanh theo khoá |
| Ví dụ thực tế | Danh sách sinh viên theo thứ tự điểm danh | Tập email đã đăng ký, URL đã thăm | Hàng đợi in, hàng đợi tác vụ | maSV → SinhVien |
4. List
// ⭐ Khai báo theo INTERFACE, khởi tạo bằng CLASS cụ thể
List<String> ds = new ArrayList<>();
// Nhờ vậy đổi sang LinkedList sau này chỉ cần sửa MỘT dòng
// --- Thêm ---
ds.add("b"); // thêm vào cuối
ds.add(0, "a"); // chèn vào vị trí 0 (dịch chuyển phần còn lại)
ds.addAll(List.of("c", "d")); // thêm nhiều
// --- Đọc / sửa ---
ds.get(0); // "a"
ds.set(0, "A"); // thay thế, trả về giá trị cũ
ds.size(); // 4
ds.isEmpty(); // false
// --- Tìm ---
ds.contains("c"); // true — O(n), duyệt tuần tự
ds.indexOf("c"); // 2 — -1 nếu không có
ds.lastIndexOf("c");
// --- Xoá: CẨN THẬN với List<Integer> ---
ds.remove(0); // xoá theo CHỈ SỐ
ds.remove("c"); // xoá theo GIÁ TRỊ
ds.removeIf(s -> s.startsWith("x")); // ⭐ Java 8+, an toàn khi duyệt
ds.clear();
// --- Khung nhìn con (view) — chia sẻ dữ liệu với list gốc! ---
List<String> con = ds.subList(1, 3); // [1, 3) — sửa `con` là sửa `ds`
// --- Sắp xếp ---
ds.sort(null); // thứ tự tự nhiên
ds.sort(Comparator.reverseOrder());
Collections.sort(ds); // cách cũ, tương đương
remove() trên List<Integer>List<Integer> so = new ArrayList<>(List.of(10, 20, 30));
so.remove(1); // gọi remove(int index) → xoá phần tử ở vị trí 1 (số 20)
System.out.println(so); // [10, 30]
so.remove(Integer.valueOf(10)); // gọi remove(Object) → xoá GIÁ TRỊ 10 ✅
System.out.println(so); // [30]
Hai phương thức remove(int) và remove(Object) là nạp chồng, và với
List<Integer> thì cả hai đều nhận được số nguyên. Java chọn
remove(int) vì khớp chính xác được ưu tiên hơn autoboxing (chương 4). Muốn xoá theo giá
trị, phải ép tường minh: remove(Integer.valueOf(10)).
4.1. ArrayList vs LinkedList
[A][B][C][D][ ][ ][ ]Bộ nhớ liền kề, có chỗ dự phòng.
Đầy thì tạo mảng mới gấp 1,5× và chép sang.
A ⇄ B ⇄ C ⇄ DMỗi node nằm rải rác, chứa giá trị
+ 2 con trỏ (trước/sau).
| Thao tác | ArrayList | LinkedList | Giải thích |
|---|---|---|---|
get(i) / set(i,v) | O(1) | O(n) | Mảng tính địa chỉ trực tiếp; liên kết phải đi từng node |
add(e) cuối | O(1)* | O(1) | * trung bình — thỉnh thoảng phải nới rộng |
add(0, e) đầu | O(n) | O(1) | Mảng phải dịch chuyển mọi phần tử |
remove(i) giữa | O(n) | O(n) | LinkedList tốn O(n) để tìm, dù nối lại chỉ O(1) |
contains(e) | O(n) | O(n) | Cả hai đều duyệt tuần tự |
| Bộ nhớ cho mỗi phần tử | 1 tham chiếu | 1 node = 3 tham chiếu | LinkedList tốn gấp ~3 lần |
| Thân thiện với cache CPU | ✅ Rất tốt | ❌ Kém | Yếu tố quyết định trong thực tế |
ArrayList. Gần như luôn luôn. Về lý thuyết LinkedList
thắng ở việc chèn/xoá đầu danh sách, nhưng trên phần cứng thật, ArrayList vẫn thường nhanh
hơn ngay cả ở những thao tác đó — vì bộ nhớ liền kề tận dụng được cache CPU, còn con trỏ nhảy lung tung
thì không. Nếu bạn cần chèn/xoá ở hai đầu, hãy dùng ArrayDeque, không phải
LinkedList.
// Mặc định dung lượng 10; đầy thì tạo mảng mới 1,5× và chép lại
List<String> a = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) a.add("x");
// → khoảng 30 lần cấp phát lại + chép toàn bộ
// ✅ Biết trước thì nói luôn
List<String> b = new ArrayList<>(1_000_000);
// → một lần cấp phát duy nhất, nhanh hơn rõ rệt
4.2. List bất biến — và cạm bẫy
// ✅ Java 9+: List.of() — BẤT BIẾN hoàn toàn
List<String> a = List.of("a", "b", "c");
// a.add("d"); // UnsupportedOperationException
// a.set(0, "x"); // UnsupportedOperationException
// List.of("a", null) // NullPointerException — KHÔNG cho null!
// ⚠️ Arrays.asList() — KÍCH THƯỚC cố định nhưng SỬA được phần tử
List<String> b = Arrays.asList("a", "b", "c");
b.set(0, "x"); // ✅ được — nó là KHUNG NHÌN của mảng gốc
// b.add("d"); // UnsupportedOperationException
// ✅ Muốn list sửa được từ giá trị có sẵn
List<String> c = new ArrayList<>(List.of("a", "b"));
c.add("c"); // ✅
// ✅ Bản sao bất biến từ list đang có (Java 10+)
List<String> d = List.copyOf(c);
// ⚠️ unmodifiableList chỉ là KHUNG NHÌN chỉ đọc, không phải bản sao
List<String> goc = new ArrayList<>(List.of("a"));
List<String> view = Collections.unmodifiableList(goc);
goc.add("b"); // sửa list GỐC
System.out.println(view); // [a, b] ⚠️ view thấy thay đổi!
5. Set
Set<String> s = new HashSet<>();
s.add("a"); // true — thêm thành công
s.add("a"); // false — đã có, không thêm nữa
s.contains("a"); // true — O(1) ⚡ đây là lý do dùng Set
s.remove("a"); // true
// --- Phép toán tập hợp ---
Set<Integer> A = new HashSet<>(List.of(1, 2, 3, 4));
Set<Integer> B = new HashSet<>(List.of(3, 4, 5, 6));
Set<Integer> hop = new HashSet<>(A);
hop.addAll(B); // A ∪ B = [1,2,3,4,5,6]
Set<Integer> giao = new HashSet<>(A);
giao.retainAll(B); // A ∩ B = [3,4]
Set<Integer> hieu = new HashSet<>(A);
hieu.removeAll(B); // A \ B = [1,2]
// Ứng dụng thường gặp nhất: loại trùng lặp
List<String> coTrung = List.of("a", "b", "a", "c", "b");
List<String> duyNhat = new ArrayList<>(new LinkedHashSet<>(coTrung));
System.out.println(duyNhat); // [a, b, c] — giữ nguyên thứ tự xuất hiện
HashSet | LinkedHashSet | TreeSet | |
|---|---|---|---|
| Thứ tự | Không xác định | Thứ tự thêm vào | Đã sắp xếp |
| Cấu trúc bên dưới | Bảng băm | Bảng băm + liên kết đôi | Cây đỏ đen |
add/contains/remove | O(1) | O(1) | O(log n) |
Cho null | ✅ Một cái | ✅ Một cái | ❌ NullPointerException |
| Yêu cầu phần tử | equals+hashCode | equals+hashCode | Comparable hoặc Comparator |
| Bộ nhớ | Ít nhất | Nhiều hơn (thêm liên kết) | Trung bình |
| Dùng khi | Mặc định | Cần giữ thứ tự xuất hiện | Cần duyệt theo thứ tự, tìm khoảng |
TreeSet<Integer> t = new TreeSet<>(List.of(10, 20, 30, 40, 50));
// Truy vấn thứ tự — HashSet KHÔNG làm được
t.first(); // 10
t.last(); // 50
t.floor(35); // 30 — phần tử lớn nhất ≤ 35
t.ceiling(35); // 40 — phần tử nhỏ nhất ≥ 35
t.lower(30); // 20 — nhỏ hơn hẳn
t.higher(30); // 40 — lớn hơn hẳn
// Khoảng giá trị
t.headSet(30); // [10, 20] — nhỏ hơn 30
t.tailSet(30); // [30, 40, 50] — từ 30 trở lên
t.subSet(20, 40); // [20, 30] — [20, 40)
t.descendingSet(); // [50, 40, 30, 20, 10]
// Sắp xếp theo tiêu chí riêng
TreeSet<SinhVien> theoDiem =
new TreeSet<>(Comparator.comparingDouble(SinhVien::getDiem).reversed());
TreeSet dùng compareTo, không dùng equalsrecord SinhVien(String ma, String ten, double diem) { }
TreeSet<SinhVien> set =
new TreeSet<>(Comparator.comparingDouble(SinhVien::diem));
set.add(new SinhVien("SV1", "An", 8.0));
set.add(new SinhVien("SV2", "Bình", 8.0)); // điểm TRÙNG
System.out.println(set.size()); // 1 ⚠️ Bình bị coi là TRÙNG với An!
TreeSet coi hai phần tử là "bằng nhau" khi compare(a, b) == 0 — hoàn toàn bỏ
qua equals(). Bộ so sánh chỉ dựa trên điểm nên hai sinh viên khác nhau bị gộp làm một.
Sửa: luôn thêm tiêu chí phụ để bộ so sánh nhất quán với equals:
Comparator.comparingDouble(SinhVien::diem).thenComparing(SinhVien::ma).
6. Queue và Deque
// ⭐ ArrayDeque — dùng làm CẢ hàng đợi lẫn ngăn xếp, nhanh nhất cho cả hai
// --- Dùng như QUEUE (FIFO — vào trước ra trước) ---
Queue<String> q = new ArrayDeque<>();
q.offer("A"); // thêm vào cuối (add() cũng được nhưng ném lỗi khi đầy)
q.offer("B");
q.offer("C");
q.peek(); // "A" — xem đầu, KHÔNG xoá
q.poll(); // "A" — lấy và XOÁ khỏi đầu
System.out.println(q); // [B, C]
// --- Dùng như STACK (LIFO — vào sau ra trước) ---
Deque<String> st = new ArrayDeque<>();
st.push("A"); // thêm vào ĐẦU
st.push("B");
st.push("C");
st.peek(); // "C"
st.pop(); // "C"
System.out.println(st); // [B, A]
// --- Hai đầu đầy đủ ---
Deque<Integer> d = new ArrayDeque<>();
d.offerFirst(1); d.offerLast(2);
d.peekFirst(); d.peekLast();
d.pollFirst(); d.pollLast();
| Thao tác | Ném ngoại lệ khi lỗi | Trả giá trị đặc biệt ⭐ |
|---|---|---|
| Thêm | add(e) → IllegalStateException | offer(e) → false |
| Xoá | remove() → NoSuchElementException | poll() → null |
| Xem | element() → NoSuchElementException | peek() → null |
Stack — dùng ArrayDeque
Class java.util.Stack có từ Java 1.0 và mắc ba vấn đề: nó kế thừa Vector nên
đồng bộ hoá (chậm) dù bạn không cần; nó lộ luôn cả API của List nên có thể
get(0) vào giữa ngăn xếp — phá vỡ khái niệm; và thứ tự duyệt của nó
ngược với trực giác (từ đáy lên đỉnh).
Chính Javadoc của JDK khuyến nghị dùng ArrayDeque thay thế. Tương tự,
Vector và Hashtable đều là di sản — dùng ArrayList và
HashMap.
// PriorityQueue — luôn lấy ra phần tử "nhỏ nhất" theo thứ tự sắp xếp
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(30); pq.offer(10); pq.offer(20);
pq.poll(); // 10 — nhỏ nhất trước
pq.poll(); // 20
// ⚠️ toString() KHÔNG theo thứ tự sắp xếp — nó in cấu trúc heap bên trong
System.out.println(pq); // đừng tin cái này
// Ưu tiên ngược (lớn nhất trước)
PriorityQueue<Integer> maxPq = new PriorityQueue<>(Comparator.reverseOrder());
// Ứng dụng thật: hàng đợi công việc theo độ ưu tiên
record CongViec(String ten, int uuTien) { }
PriorityQueue<CongViec> hangDoi =
new PriorityQueue<>(Comparator.comparingInt(CongViec::uuTien));
hangDoi.offer(new CongViec("Sao lưu", 5));
hangDoi.offer(new CongViec("Khẩn cấp", 1));
hangDoi.poll(); // CongViec[ten=Khẩn cấp, uuTien=1]
// Mẫu "top K phần tử lớn nhất" — chỉ giữ K phần tử trong bộ nhớ
PriorityQueue<Integer> topK = new PriorityQueue<>(); // min-heap
for (int x : duLieuRatLon) {
topK.offer(x);
if (topK.size() > K) topK.poll(); // loại phần tử nhỏ nhất
} // Bộ nhớ O(K) thay vì O(n) — quan trọng với dữ liệu hàng tỷ phần tử
7. Iterator và ConcurrentModificationException
List<String> ds = new ArrayList<>(List.of("a", "b", "c", "d"));
// ❌ Xoá khi đang duyệt bằng for-each
for (String s : ds) {
if (s.equals("b")) ds.remove(s);
}
// 💥 ConcurrentModificationException
// ✅ Cách 1: Iterator.remove() — cách chuẩn từ Java 1.2
Iterator<String> it = ds.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove();
}
// ✅ Cách 2: removeIf() — ngắn gọn nhất (Java 8+)
ds.removeIf(s -> s.equals("b"));
// ✅ Cách 3: duyệt ngược bằng chỉ số
for (int i = ds.size() - 1; i >= 0; i--) {
if (ds.get(i).equals("b")) ds.remove(i);
}
// ✅ Cách 4: gom vào list mới rồi xoá một lượt
List<String> canXoa = ds.stream().filter(s -> s.equals("b")).toList();
ds.removeAll(canXoa);
modCount — cơ chế "fail-fast" bên dưới
Mỗi collection giữ một bộ đếm modCount, tăng lên mỗi khi cấu trúc thay đổi (thêm/xoá phần
tử). Khi bạn tạo iterator, nó chụp lại giá trị hiện tại vào expectedModCount. Ở mỗi lần
next(), iterator so sánh hai số — khác nhau nghĩa là ai đó đã sửa collection sau lưng nó,
và nó ném ConcurrentModificationException ngay.
Đây gọi là hành vi fail-fast: thà báo lỗi ngay còn hơn duyệt sai âm thầm và trả kết quả
không đúng. Iterator.remove() hoạt động được vì nó cập nhật cả hai bộ đếm.
Tên gọi hơi gây nhầm: ngoại lệ này xảy ra ngay cả trong chương trình một luồng — nó không chỉ liên quan tới đa luồng.
8. Comparable và Comparator
Comparable<T> | Comparator<T> | |
|---|---|---|
| Ý nghĩa | Thứ tự tự nhiên của chính lớp | Thứ tự bên ngoài, tuỳ ngữ cảnh |
| Phương thức | int compareTo(T o) | int compare(T a, T b) |
| Cài ở đâu | Trong chính lớp đó | Lớp riêng / lambda |
| Số lượng | Một | Bao nhiêu cũng được |
| Ví dụ | String, Integer, LocalDate | Sắp theo tên, theo điểm, theo ngày… |
// --- Comparable: thứ tự tự nhiên, gắn liền với lớp ---
record SinhVien(String ma, String ten, double diem, int tuoi)
implements Comparable<SinhVien> {
@Override
public int compareTo(SinhVien khac) {
return ma.compareTo(khac.ma); // mặc định sắp theo mã
}
}
Collections.sort(danhSach); // dùng compareTo()
// --- Comparator: nhiều cách sắp xếp khác nhau ---
danhSach.sort(Comparator.comparing(SinhVien::ten));
danhSach.sort(Comparator.comparingDouble(SinhVien::diem).reversed());
// ⭐ Nhiều tiêu chí: điểm giảm dần, cùng điểm thì tên tăng dần, rồi tới tuổi
danhSach.sort(
Comparator.comparingDouble(SinhVien::diem).reversed()
.thenComparing(SinhVien::ten)
.thenComparingInt(SinhVien::tuoi)
);
// Xử lý null an toàn
Comparator<SinhVien> anToan =
Comparator.nullsFirst(Comparator.comparing(SinhVien::ten));
// Không phân biệt hoa thường
danhSach.sort(Comparator.comparing(SinhVien::ten, String.CASE_INSENSITIVE_ORDER));
compare bằng phép trừ// ❌ SAI — tràn số nguyên
Comparator<Integer> xau = (a, b) -> a - b;
xau.compare(Integer.MIN_VALUE, 1);
// -2147483648 - 1 = 2147483647 (tràn) → trả DƯƠNG
// → nói rằng MIN_VALUE > 1 → sắp xếp SAI hoàn toàn
// ✅ ĐÚNG
Comparator<Integer> tot = Integer::compare;
// hoặc: Comparator.comparingInt(x -> x)
// ❌ Với double thì còn tệ hơn — mất chính xác
Comparator<SinhVien> xau2 = (a, b) -> (int)(a.diem() - b.diem());
// 8.5 - 8.2 = 0.3 → ép về int = 0 → coi là BẰNG NHAU!
Comparator<SinhVien> tot2 = Comparator.comparingDouble(SinhVien::diem); // ✅
compareTo- Đối xứng dấu:
sgn(a.compareTo(b)) == -sgn(b.compareTo(a)) - Bắc cầu:
a > bvàb > c⇒a > c - Nhất quán:
a.compareTo(b) == 0⇒ mọi so sánh vớicđều cho kết quả như nhau - Khuyến nghị mạnh:
a.compareTo(b) == 0nên tương đươnga.equals(b)
Vi phạm ba điều đầu khiến Collections.sort ném
IllegalArgumentException: Comparison method violates its general contract!.
Vi phạm điều cuối gây ra bẫy TreeSet ở mục 5.
9. Lớp tiện ích Collections
import java.util.Collections;
List<Integer> ds = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9));
Collections.sort(ds); // sắp xếp tại chỗ
Collections.reverse(ds); // đảo ngược
Collections.shuffle(ds); // xáo trộn ngẫu nhiên
Collections.swap(ds, 0, 1); // hoán đổi hai vị trí
Collections.max(ds); // 9
Collections.min(ds); // 1
Collections.frequency(ds, 1); // 2 — số lần xuất hiện
Collections.nCopies(3, "x"); // [x, x, x] — bất biến
Collections.binarySearch(ds, 4); // ⚠️ list PHẢI đã sắp xếp
Collections.disjoint(ds1, ds2); // true nếu KHÔNG có phần tử chung
// Collection rỗng bất biến — tránh trả về null
List<String> rong = Collections.emptyList();
Set<String> rong2 = Collections.emptySet();
// Khung nhìn chỉ đọc
List<Integer> chiDoc = Collections.unmodifiableList(ds);
// Bọc đồng bộ — thô sơ; chương 12 có lựa chọn tốt hơn
List<Integer> dongBo = Collections.synchronizedList(ds);
10. Bảng độ phức tạp — công cụ ra quyết định
| Cấu trúc | Thêm cuối | Thêm đầu | Lấy theo chỉ số | Tìm giá trị | Xoá | Thứ tự |
|---|---|---|---|---|---|---|
ArrayList |
O(1)* | O(n) | O(1) | O(n) | O(n) | Thêm vào |
LinkedList |
O(1) | O(1) | O(n) | O(n) | O(n) | Thêm vào |
ArrayDeque |
O(1) | O(1) | — | O(n) | O(1) ở hai đầu | Thêm vào |
HashSet |
O(1) | — | — | O(1) | O(1) | Không |
LinkedHashSet |
O(1) | — | — | O(1) | O(1) | Thêm vào |
TreeSet |
O(log n) | — | — | O(log n) | O(log n) | Đã sắp xếp |
PriorityQueue |
O(log n) | — | — | O(n) | O(log n) ở đỉnh | Theo ưu tiên |
HashMap |
O(1) | — | O(1) theo khoá | O(1) | O(1) | Không |
TreeMap |
O(log n) | — | O(log n) | O(log n) | O(log n) | Sắp theo khoá |
* Trung bình khấu hao (amortized): phần lớn lần thêm là O(1), thỉnh thoảng phải cấp phát lại mảng và chép — nhưng chi phí đó trải đều ra thì vẫn là hằng số.
// ❌ Dùng List để kiểm tra tồn tại — O(n) cho MỖI lần kiểm tra
List<String> daThay = new ArrayList<>();
for (String url : 100_000 URL) {
if (!daThay.contains(url)) { // O(n) — duyệt cả list
daThay.add(url);
xuLy(url);
}
}
// Tổng: O(n²) = 10 tỷ phép so sánh → khoảng vài phút
// ✅ Dùng Set — O(1) cho mỗi lần kiểm tra
Set<String> daThay2 = new HashSet<>();
for (String url : 100_000 URL) {
if (daThay2.add(url)) { // add trả false nếu đã có — gộp 2 bước làm 1
xuLy(url);
}
}
// Tổng: O(n) = 100 nghìn phép → vài mili giây
// Chênh lệch: hàng NGHÌN lần, chỉ vì đổi một từ khoá.
11. Cây quyết định — chọn cấu trúc nào
Dữ liệu là cặp KHOÁ → GIÁ TRỊ?
├── CÓ → Map (Chương 8)
│ ├── Cần sắp theo khoá? → TreeMap
│ ├── Cần giữ thứ tự thêm? → LinkedHashMap
│ └── Còn lại → HashMap ⭐
│
└── KHÔNG → Cần loại trùng lặp?
├── CÓ → Set
│ ├── Cần sắp xếp / tìm khoảng? → TreeSet
│ ├── Cần giữ thứ tự thêm? → LinkedHashSet
│ └── Còn lại → HashSet ⭐
│
└── KHÔNG → Xử lý theo thứ tự vào/ra?
├── CÓ → Cần độ ưu tiên?
│ ├── CÓ → PriorityQueue
│ └── KHÔNG → ArrayDeque ⭐ (cả FIFO lẫn LIFO)
│
└── KHÔNG → List
└── ArrayList ⭐ (gần như luôn luôn)
ArrayList cho danh sách, HashSet cho tập
duy nhất, HashMap cho tra cứu. Chỉ đổi khi có yêu cầu cụ thể về thứ tự hoặc thao tác
hai đầu — và hãy đo trước khi tối ưu.
12. Cheat sheet chương 7
| Tình huống | Cách viết |
|---|---|
| Khai báo chuẩn | List<X> ds = new ArrayList<>(); — interface bên trái |
| List bất biến | List.of("a", "b") — không cho null |
| List sửa được từ giá trị có sẵn | new ArrayList<>(List.of(...)) |
| Loại trùng, giữ thứ tự | new ArrayList<>(new LinkedHashSet<>(ds)) |
| Kiểm tra "đã có chưa" | Set.contains() — không List.contains() |
| Thêm nếu chưa có | if (set.add(x)) { … } — add trả false nếu trùng |
| Xoá khi đang duyệt | ds.removeIf(x -> …) |
| Ngăn xếp | Deque<X> st = new ArrayDeque<>(); + push/pop |
| Hàng đợi | Queue<X> q = new ArrayDeque<>(); + offer/poll |
| Top-K phần tử lớn nhất | PriorityQueue min-heap giữ đúng K phần tử |
| Sắp nhiều tiêu chí | Comparator.comparing(A).thenComparing(B) |
| Sắp giảm dần | .reversed() hoặc Comparator.reverseOrder() |
Viết compare | Integer.compare(a,b) — không bao giờ a - b |
Xoá theo giá trị trong List<Integer> | remove(Integer.valueOf(10)) |
| Trả về "không có gì" | List.of() hoặc Collections.emptyList() — không null |
| Biết trước kích thước | new ArrayList<>(soLuong) |
| Cần Stack | ArrayDeque, không java.util.Stack |
Bài tập
Đo hiệu năng ArrayList vs LinkedList
Viết benchmark so sánh hai cấu trúc với n = 10.000 / 100.000 / 1.000.000:
- Thêm n phần tử vào cuối.
- Thêm n phần tử vào đầu (
add(0, x)). - Truy cập ngẫu nhiên n lần bằng
get(i). - Duyệt tuần tự toàn bộ bằng for-each.
- Xoá n/2 phần tử ở giữa.
Lập bảng kết quả và trả lời: kết quả nào khớp với bảng độ phức tạp lý thuyết, kết quả nào không khớp? Giải thích chỗ không khớp.
Gợi ý
Bạn sẽ thấy phép "duyệt tuần tự" của ArrayList nhanh hơn LinkedList
đáng kể dù cả hai đều là O(n) — vì bộ nhớ liền kề tận dụng được
cache CPU, còn LinkedList phải nhảy tới địa chỉ ngẫu nhiên ở mỗi node
(cache miss). Big-O không mô tả được yếu tố này, và đó là lý do ArrayList thắng
trong thực tế nhiều hơn lý thuyết dự đoán.
Nhớ warm-up JIT: chạy vài lượt và bỏ kết quả đầu tiên.
Hệ thống quản lý thư viện
Xây dựng ThuVien dùng đúng cấu trúc cho từng nhu cầu:
// Với MỖI yêu cầu, chọn cấu trúc và GIẢI THÍCH lý do:
// a) Danh sách sách theo thứ tự nhập kho
// b) Tập ISBN duy nhất, kiểm tra trùng thật nhanh
// c) Hàng đợi người đăng ký mượn (ai đăng ký trước được trước)
// d) Danh sách sách sắp theo năm xuất bản, luôn tự sắp
// e) Hàng đợi yêu cầu ưu tiên (giáo viên trước sinh viên)
// f) Lịch sử 10 thao tác gần nhất (để hoàn tác)
// g) Tập thể loại của mỗi cuốn sách, giữ thứ tự nhập
Cài đặt đầy đủ với các thao tác: thêm sách, mượn, trả, tìm theo ISBN, liệt kê sách quá hạn, hoàn tác thao tác cuối.
Gợi ý
(f) là bài toán "cửa sổ trượt cố định" — ArrayDeque với
if (size() > 10) pollLast(); sau mỗi addFirst().
(e) cần PriorityQueue với comparator hai tầng: ưu tiên trước, rồi tới thời điểm
đăng ký — nếu chỉ so ưu tiên thì hai người cùng mức sẽ có thứ tự không xác định (heap không ổn
định).
Tự cài MyArrayList
Cài đặt MyArrayList<E> implements Iterable<E> từ đầu, chỉ dùng mảng:
public class MyArrayList<E> implements Iterable<E> {
private Object[] mang; // vì sao Object[] chứ không phải E[]? (chương 9)
private int soLuong;
private int modCount;
public void add(E e);
public void add(int i, E e);
public E get(int i);
public E set(int i, E e);
public E remove(int i);
public int size();
public Iterator<E> iterator();
}
- Tự nới rộng khi đầy — nhân dung lượng với 1,5 (giống JDK) và so sánh với chiến lược nhân đôi.
- Kiểm tra chỉ số, ném
IndexOutOfBoundsExceptionvới thông điệp rõ ràng. - Iterator phải là fail-fast: dùng
modCountđể némConcurrentModificationException. - Hỗ trợ
Iterator.remove(). - Viết test: thêm 1 triệu phần tử, so tốc độ với
ArrayListthật.
Gợi ý
Nới rộng: Arrays.copyOf(mang, mang.length + (mang.length >> 1)) —
>> 1 là chia đôi, nên đây là nhân 1,5.
Xoá phần tử i: System.arraycopy(mang, i+1, mang, i, soLuong-i-1) rồi đặt
mang[--soLuong] = null — bước gán null rất quan trọng, nếu quên
thì object bị giữ tham chiếu và không được GC thu hồi (rò rỉ bộ nhớ).
Vì sao Object[]: không tạo được new E[n] do type erasure — chương 9
sẽ giải thích, và bản thân JDK cũng làm y hệt.
Bộ so sánh nhiều tiêu chí
Cho record NhanVien(String hoTen, String phongBan, long luong, LocalDate ngayVao, Integer tuoi) (chú ý tuoi có thể null), viết các bộ so sánh:
- Theo lương giảm dần.
- Theo phòng ban (A→Z), cùng phòng thì lương giảm dần.
- Theo thâm niên (vào lâu nhất trước), cùng ngày thì theo tên.
- Theo tuổi tăng dần,
nullxếp cuối. - Theo họ (từ cuối trong
hoTen), rồi tới tên. - Theo độ dài tên, cùng độ dài thì theo bảng chữ cái, không phân biệt hoa thường.
Với mỗi bộ so sánh, viết test khẳng định thứ tự đúng — kể cả trường hợp có giá trị bằng nhau.
Gợi ý
(4): Comparator.comparing(NhanVien::tuoi, Comparator.nullsLast(Comparator.naturalOrder())).
(5): trích họ bằng hoTen.substring(hoTen.lastIndexOf(' ') + 1) — cẩn thận trường
hợp tên chỉ có một từ (lastIndexOf trả −1, cộng 1 thành 0, vẫn đúng).
(6): Comparator.comparingInt(String::length).thenComparing(String.CASE_INSENSITIVE_ORDER)
áp lên NhanVien::hoTen.
Kiểm tra hợp đồng: với mỗi comparator, khẳng định
sgn(cmp(a,b)) == -sgn(cmp(b,a)) trên vài cặp.
Máy tính biểu thức bằng ngăn xếp
Dùng ArrayDeque cài đặt:
boolean canBang(String s)— kiểm tra ngoặc()[]{}cân bằng.List<String> sangHauTo(String trungTo)— chuyển trung tố sang hậu tố (thuật toán shunting-yard).double tinhHauTo(List<String> hauTo)— tính giá trị biểu thức hậu tố.double tinh(String bieuThuc)— ghép cả ba, hỗ trợ+ - * / ^, ngoặc, số thực và số âm.
Xử lý lỗi tử tế: ngoặc không cân bằng, chia cho 0, ký tự lạ — mỗi loại một exception riêng với thông điệp chỉ rõ vị trí.
Gợi ý
Shunting-yard: duyệt token; số thì đẩy thẳng ra output; toán tử thì pop mọi toán tử có độ ưu
tiên lớn hơn hoặc bằng ra output trước khi push (riêng ^ kết hợp phải sang
trái nên chỉ pop khi lớn hơn hẳn); ( thì push; ) thì pop tới
khi gặp (.
Số âm là ca khó nhất: dấu - là phép trừ hay dấu âm? Quy tắc: nếu nó ở đầu biểu
thức, hoặc ngay sau (, hoặc ngay sau một toán tử khác → đó là dấu âm.
Quiz
List, Set, Queue, Map khác nhau thế nào? Vì sao Map không kế thừa Collection?
Xem đáp án
- List — dãy có thứ tự, cho trùng lặp, truy cập theo chỉ số. Dùng khi thứ tự quan trọng.
- Set — tập phần tử duy nhất, không có chỉ số. Dùng khi cần tính duy nhất và kiểm tra tồn tại nhanh.
- Queue — xử lý theo thứ tự, thường FIFO; truy cập ở hai đầu. Dùng cho hàng đợi công việc, lập lịch.
- Map — tập cặp khoá-giá trị; khoá duy nhất, giá trị được trùng. Dùng để tra cứu theo khoá.
Map không kế thừa Collection vì hai interface có hình dạng dữ
liệu khác nhau: Collection<E> là nhóm phần tử đơn lẻ với
add(E), còn Map lưu cặp với put(K, V). Ép chung một
interface sẽ khiến cả hai đều gượng ép.
Chúng vẫn nối với nhau qua ba khung nhìn: keySet() → Set<K>,
values() → Collection<V>, entrySet() →
Set<Map.Entry>.
Khi nào dùng LinkedList thay ArrayList?
Xem đáp án
Trong thực tế: gần như không bao giờ. Đây là câu trả lời gây bất ngờ nhưng đúng.
Về lý thuyết, LinkedList thắng ở chèn/xoá đầu danh sách
(O(1) so với O(n)). Nhưng trên phần
cứng thật, ArrayList vẫn thường nhanh hơn ngay cả ở đó, vì:
- Cache CPU — mảng nằm liền kề, đọc một phần tử là kéo luôn cả vùng lân cận vào cache.
LinkedListnhảy tới địa chỉ ngẫu nhiên ở mỗi node, gây cache miss liên tục. - Bộ nhớ — mỗi node
LinkedListtốn 3 tham chiếu (giá trị + trước + sau) thay vì 1. System.arraycopyđược tối ưu ở mức lệnh máy, dịch chuyển hàng nghìn phần tử rất nhanh.
Nếu bạn cần thêm/xoá ở hai đầu, hãy dùng ArrayDeque — nó
nhanh hơn LinkedList ở mọi mặt cho mục đích đó.
Chính Joshua Bloch (tác giả Collections Framework) từng nói đùa rằng ông không nghĩ ra được lý do
nào để dùng LinkedList.
HashSet, LinkedHashSet, TreeSet khác nhau ra sao?
Xem đáp án
| HashSet | LinkedHashSet | TreeSet | |
|---|---|---|---|
| Thứ tự | Không xác định | Thứ tự thêm | Đã sắp xếp |
| Bên dưới | Bảng băm | Bảng băm + liên kết đôi | Cây đỏ đen |
| Tốc độ | O(1) | O(1) | O(log n) |
null | ✅ | ✅ | ❌ NPE |
| Yêu cầu | hashCode+equals | hashCode+equals | Comparable/Comparator |
Mặc định HashSet — nhanh nhất và tốn ít bộ nhớ nhất.
LinkedHashSet khi cần kết quả ổn định để hiển thị hoặc test.
TreeSet khi cần thứ tự hoặc các truy vấn khoảng
(floor, ceiling, subSet) — thứ hai loại kia không làm được.
Bẫy TreeSet: nó xác định trùng lặp bằng compareTo, không
phải equals. Comparator chỉ so một trường sẽ khiến các phần tử khác nhau bị gộp — luôn
thêm tiêu chí phụ để bộ so sánh nhất quán với equals.
ConcurrentModificationException xảy ra khi nào? Cách tránh?
Xem đáp án
Xảy ra khi bạn sửa cấu trúc collection (thêm/xoá phần tử) trong lúc đang duyệt nó bằng iterator hoặc for-each.
Cơ chế: collection giữ bộ đếm modCount, tăng mỗi lần cấu trúc thay
đổi. Iterator chụp giá trị này lúc tạo và so sánh ở mỗi next(). Khác nhau nghĩa là
collection đã bị sửa sau lưng nó → ném ngoại lệ ngay (hành vi fail-fast: báo lỗi còn hơn
duyệt sai âm thầm).
Bốn cách tránh:
ds.removeIf(x -> dieuKien)— ngắn gọn nhất (Java 8+).Iterator.remove()— cách chuẩn, nó cập nhật cả hai bộ đếm.- Duyệt ngược bằng chỉ số — xoá phần tử i không ảnh hưởng các chỉ số nhỏ hơn.
- Gom vào list mới rồi
removeAll()một lượt.
Lưu ý: tên gọi gây hiểu lầm — ngoại lệ này xảy ra cả trong chương trình một luồng. Với đa
luồng thật thì dùng CopyOnWriteArrayList hoặc ConcurrentHashMap
(chương 12).
Comparable và Comparator khác nhau thế nào?
Xem đáp án
Comparable<T> định nghĩa thứ tự tự nhiên của chính lớp
đó. Cài đặt bên trong lớp, phương thức int compareTo(T o). Mỗi lớp chỉ có
một thứ tự tự nhiên. Ví dụ: String theo bảng chữ cái,
Integer theo giá trị.
Comparator<T> định nghĩa thứ tự bên ngoài, tuỳ ngữ
cảnh. Viết thành lambda hoặc lớp riêng, phương thức int compare(T a, T b). Viết
bao nhiêu cũng được, và dùng được cả với lớp bạn không sửa được mã nguồn.
ds.sort(null); // dùng Comparable
ds.sort(Comparator.comparing(SinhVien::ten)); // dùng Comparator
ds.sort(Comparator.comparingDouble(SinhVien::diem)
.reversed().thenComparing(SinhVien::ma)); // nhiều tiêu chí
Cả hai đều cấm viết bằng phép trừ: (a, b) -> a - b tràn số với
Integer.MIN_VALUE, và (int)(a - b) với double thì làm mất
phần thập phân — 8.5 - 8.2 ép về int thành 0, tức "bằng
nhau". Dùng Integer.compare / Double.compare /
Comparator.comparingDouble.
Vì sao list.remove(1) và list.remove(Integer.valueOf(1)) cho kết quả khác nhau?
Xem đáp án
Vì List có hai phương thức remove nạp chồng:
remove(int index) — xoá theo vị trí, và
remove(Object o) — xoá theo giá trị.
Với List<Integer>, gọi remove(1) thì trình biên dịch chọn
remove(int) — vì khớp chính xác được ưu tiên hơn autoboxing (quy tắc
chọn phương thức nạp chồng ở chương 4). Nên nó xoá phần tử ở vị trí 1, không phải
giá trị 1.
List<Integer> ds = new ArrayList<>(List.of(10, 20, 30));
ds.remove(1); // xoá vị trí 1 → [10, 30]
ds.remove(Integer.valueOf(10)); // xoá giá trị 10 → [30]
Với List<String> thì không có vấn đề này, vì String không thể là
int. Bẫy chỉ xuất hiện với List<Integer> — và nó là một trong những
lỗi âm thầm khó chịu nhất, vì code vẫn chạy và chỉ cho kết quả sai.
List.of(), Arrays.asList(), new ArrayList<>() khác nhau ra sao?
Xem đáp án
List.of() | Arrays.asList() | new ArrayList<>() | |
|---|---|---|---|
| Thêm/xoá | ❌ UOE | ❌ UOE | ✅ |
set(i, v) | ❌ UOE | ✅ được | ✅ |
Cho null | ❌ NPE | ✅ | ✅ |
| Liên kết mảng gốc | Không | ✅ là khung nhìn | Không |
| Từ Java | 9 | 1.2 | 1.2 |
Arrays.asList() là thứ dễ gây bất ngờ nhất: nó tạo một khung nhìn của mảng —
kích thước cố định nhưng set() được, và sửa nó là sửa luôn mảng gốc.
Nên dùng: List.of() cho hằng số và giá trị trả về bất biến;
new ArrayList<>(List.of(...)) khi cần list sửa được;
List.copyOf(x) để tạo bản sao bất biến từ collection có sẵn. Tránh
Arrays.asList() trừ khi bạn cố ý muốn khung nhìn của mảng.
Vì sao không nên dùng java.util.Stack?
Xem đáp án
Ba lý do, và chính Javadoc của JDK cũng khuyến nghị thay thế:
- Đồng bộ hoá không cần thiết —
Stackkế thừaVector, nên mọi phương thức đềusynchronized. Bạn trả giá về tốc độ cho tính năng gần như chẳng bao giờ dùng tới. - Rò rỉ trừu tượng — vì là
List, nó lộ luônget(i),add(i, e),remove(i). Người dùng chọc được vào giữa ngăn xếp, phá vỡ chính khái niệm LIFO. - Thứ tự duyệt ngược trực giác —
for (x : stack)duyệt từ đáy lên đỉnh, ngược với thứ tựpop(). Nguồn bug rất khó nhận ra.
Thay bằng ArrayDeque:
Deque<String> st = new ArrayDeque<>();
st.push("a"); st.pop(); st.peek();
// Nhanh hơn, API sạch hơn, duyệt đúng thứ tự đỉnh → đáy
Cùng lý do đó, tránh Vector (dùng ArrayList) và Hashtable
(dùng HashMap) — cả ba đều là di sản từ Java 1.0, giữ lại chỉ vì tương thích ngược.
Tổng kết
Sau chương 7, bạn đã nắm:
- Cây phân cấp:
Iterable→Collection→List/Set/Queue;Mapđứng riêng. - Bốn nhóm và tiêu chí phân biệt: thứ tự, trùng lặp, cách truy cập.
ArrayListlà mặc định —LinkedListgần như không bao giờ cần, vì cache CPU.- Ba loại
Setvà bẫyTreeSetdùngcompareTothayequals. ArrayDequelàm cả ngăn xếp lẫn hàng đợi;PriorityQueuecho ưu tiên và mẫu top-K.Iteratorvà cơ chế fail-fast quamodCount; bốn cách xoá an toàn.ComparablevsComparator; chuỗithenComparing; cấm viếtcomparebằng phép trừ.- Bảng độ phức tạp — và bài học "sai cấu trúc = chậm hàng nghìn lần mà không có lỗi nào".
- Collection bất biến:
List.ofvsArrays.asListvsunmodifiableList. - Class di sản cần tránh:
Vector,Stack,Hashtable.
Kết nối
- Chương 8 (Map) —
HashMaphoạt động bên trong, và vì sao hợp đồngequals/hashCodequyết định tất cả. - Chương 9 (Generics) —
List<E>, wildcard? extends/? super, và vì saoMyArrayListphải dùngObject[]. - Chương 10 (Stream) —
collection.stream(),filter/map/collect,Collectors.groupingBy. - Chương 12 (Đa luồng) —
ConcurrentHashMap,CopyOnWriteArrayList,BlockingQueue. - Chương 4-5 (OOP) —
equals/hashCodelà điều kiện tiên quyết để dùngSetvàMapđúng. - Trụ cột DSA —
ArrayList= mảng động,LinkedList= danh sách liên kết,TreeSet= cây đỏ đen,PriorityQueue= heap,HashSet= bảng băm. - Python Chương 3 — đối chiếu: Python gộp tất cả vào bốn kiểu dựng sẵn (
list,tuple,dict,set) vàcollections; không tách interface khỏi cài đặt.