99 Ngày Java — Ngày 33: Set — HashSet, LinkedHashSet, TreeSet
Ngày 31 đã hứa: Set là loài hứa duy nhất. Hôm nay ta sống trọn với lời hứa đó qua ba anh em HashSet – LinkedHashSet – TreeSet: giống nhau ở khử trùng lặp, khác nhau ở thứ tự và cái giá. Quan trọng hơn, ta soi hai hợp đồng đứng sau: equals/hashCode nuôi họ Hash (Ngày 24), và Comparable/Comparator nuôi họ Tree — cùng những cái bẫy mà ai dùng Set với object tự viết cũng sẽ gặp.

Set — lời hứa duy nhất trong thực chiến
// Set — cấu trúc sinh ra cho MỘT lời hứa: không có phần tử trùng
Set<String> daXem = new HashSet<>();
daXem.add("video-1"); // true — thêm mới
daXem.add("video-2"); // true
daXem.add("video-1"); // false — ĐÃ CÓ: Set từ chối, không ném lỗi
// add trả boolean = vừa thêm vừa kiểm tra trong MỘT thao tác:
if (!daXem.add(videoId)) {
// đã xem rồi — tăng view? bỏ qua? tùy nghiệp vụ
}
// contains là nghề chính — O(1) với HashSet (Ngày 31):
daXem.contains("video-2"); // true — không duyệt, tra thẳng
// Khử trùng lặp một phát:
List<String> emails = List.of("a@x.com", "b@x.com", "a@x.com");
Set<String> duyNhat = new HashSet<>(emails); // size = 2
// Đời thực Set ở khắp nơi:
// - tập user đã like / đã đọc / đã tham gia
// - tập tag của bài viết
// - tập quyền (permission) của một role
// Chung một câu hỏi: "X có trong tập không?" — đó là lúc gọi SetĐiểm ăn tiền của
add: trảbooleanthay vì ném lỗi — vừa thêm vừa kiểm tra trong một thao tác nguyên tử về logic, gọn hơn hẳn cặpcontains+addrời rạc.Nghề chính của Set là
containsO(1)— mọi bài toán "X có trong tập không" (đã xem, đã like, có quyền) đều là đất diễn; vànew HashSet<>(list)vẫn là câu thần chú khử trùng lặp một dòng của Ngày 31.Chọn
SethayListlà chọn lời hứa, không phải thói quen: dữ liệu có ngữ nghĩa "tập hợp" (không trùng, không quan tâm vị trí) mà lưu bằngListthì mọi chỗ dùng phải tự gác trùng lặp — trả nợ dần từng dòng code.
Ba anh em — khác nhau ở thứ tự
// Ba anh em một lời hứa — khác nhau ở THỨ TỰ
Set<String> hash = new HashSet<>();
Set<String> linked = new LinkedHashSet<>();
Set<String> tree = new TreeSet<>();
for (String s : List.of("chuối", "táo", "cam")) {
hash.add(s); linked.add(s); tree.add(s);
}
hash; // [cam, chuối, táo]? — KHÔNG hứa gì, thứ tự có thể đổi giữa các lần chạy
linked; // [chuối, táo, cam] — đúng THỨ TỰ CHÈN, ổn định
tree; // [cam, chuối, táo] — luôn SẮP XẾP theo thứ tự tự nhiên
// Giá niêm yết (Ngày 31):
// HashSet add/contains O(1) — nhanh nhất, mặc định
// LinkedHashSet O(1) + giữ thứ tự chèn — tốn thêm chút bộ nhớ (linked list ngầm)
// TreeSet O(log n) — đổi tốc độ lấy sắp xếp thường trực
// Chọn nhanh:
// - Chỉ cần khử trùng + hỏi contains? → HashSet
// - Cần in ra ổn định đúng thứ tự người dùng thêm? → LinkedHashSet
// - Cần "nhỏ nhất/lớn nhất/khoảng"? → TreeSet (phần dưới)HashSetkhông hứa thứ tự — thứ tự in ra có thể đổi giữa các lần chạy, nên test mà assert theo thứ tự của HashSet là test tự vỡ; cần ổn định thì đổi loài, đừng cầu may.LinkedHashSetgiữ thứ tự chèn bằng linked list ngầm — thêm chút bộ nhớ đổi lấy đầu ra ổn định: hợp làm dữ liệu hiển thị, log, hay bất cứ chỗ nào con người đọc.TreeSettrảO(log n)để luôn sắp xếp — đắt hơn Hash gấp nhiều lần chocontainsthuần, nên chỉ chọn khi thật sự cần thứ tự/khoảng; quy luật đặt tênHash–Linked–Treenày sẽ lặp lại nguyên xi ởMap(Ngày 34–35).
HashSet đứng trên equals và hashCode
// HashSet đứng trên vai equals + hashCode (Ngày 24) — quên là "trùng mà không trùng"
class DiemToaDo { // KHÔNG override equals/hashCode
int x, y;
DiemToaDo(int x, int y) { this.x = x; this.y = y; }
}
Set<DiemToaDo> tap = new HashSet<>();
tap.add(new DiemToaDo(1, 2));
tap.add(new DiemToaDo(1, 2)); // hai object khác nhau theo default equals!
tap.size(); // 2 — Set "thủng" lời hứa vì ta chưa dạy nó so sánh
// Cách chữa gọn nhất 2026 — record (Ngày 20): equals/hashCode chuẩn miễn phí
record Diem(int x, int y) { }
Set<Diem> tapDung = new HashSet<>();
tapDung.add(new Diem(1, 2));
tapDung.add(new Diem(1, 2)); // false — trùng thật sự bị chặn
tapDung.size(); // 1 ✅
// Bẫy chí mạng: SỬA phần tử sau khi đã add (Ngày 24 + 29)
Set<List<String>> nguyHiem = new HashSet<>();
List<String> ds = new ArrayList<>(List.of("a"));
nguyHiem.add(ds);
ds.add("b"); // hashCode của ds ĐỔI sau khi vào Set!
nguyHiem.contains(ds); // false — phần tử "lạc" ngay trong Set
// → phần tử của HashSet nên BẤT BIẾN: String, record, List.of — không phải ArrayListKhông override
equals/hashCodethì mọi object là "chính nó" — haiDiemToaDo(1,2)vẫn là hai phần tử: Set không thủng, nó chỉ so sánh theo đúng thứ ta (chưa) dạy. Thuộc bài Ngày 24 là miễn nhiễm.Cách chữa hiện đại: record (Ngày 20) —
equals/hashCodetheo giá trị được compiler viết hộ; class thường thìObjects.equals/Objects.hashtheo đúng công thức Ngày 24.Bẫy chí mạng nhất chương Collections: sửa phần tử sau khi add —
hashCodeđổi là phần tử "lạc" ngay trong Set của nó (containstrảfalsedù nó nằm đó). Kết luận nối ba bài 24 + 29 + hôm nay: phần tử của Set họ Hash nên bất biến.
TreeSet — Comparable, Comparator và cái bẫy compareTo
// TreeSet — Set biết sắp xếp: trả lời cả "nhỏ nhất? lớn nhất? trong khoảng?"
TreeSet<Integer> diem = new TreeSet<>(List.of(85, 42, 97, 61, 73));
diem.first(); // 42 — nhỏ nhất
diem.last(); // 97 — lớn nhất
diem.headSet(70); // [42, 61] — mọi phần tử < 70
diem.tailSet(70); // [73, 85, 97] — mọi phần tử >= 70
diem.ceiling(80); // 85 — phần tử nhỏ nhất >= 80
diem.floor(80); // 73 — phần tử lớn nhất <= 80
// Họ câu hỏi "khoảng/lân cận" này HashSet chịu chết — đây là lý do tồn tại của Tree
// Điều kiện: phần tử phải SO SÁNH ĐƯỢC
new TreeSet<DiemToaDo>().add(new DiemToaDo(1, 2));
// 💥 ClassCastException: DiemToaDo không Comparable — nổ lúc CHẠY, không phải compile
// Hai cách dạy TreeSet so sánh:
// 1. Phần tử tự biết: class X implements Comparable<X>
// 2. Đưa Comparator từ ngoài (Ngày 38 học sâu):
TreeSet<NhanVien> theoLuong =
new TreeSet<>(Comparator.comparing(NhanVien::luong));
// Bẫy tinh vi: TreeSet coi "bằng" theo compareTo, KHÔNG theo equals
TreeSet<BigDecimal> tien = new TreeSet<>();
tien.add(new BigDecimal("1.0"));
tien.add(new BigDecimal("1.00")); // compareTo bảo bằng nhau (Ngày 27!)
tien.size(); // 1 — HashSet sẽ trả 2 (equals khác nhau)Giá trị thật của
TreeSetkhông phải "in ra có sắp xếp" — mà là họ câu hỏi khoảng/lân cận:first/last/headSet/ceiling… — leaderboard, khung giá, lịch trống — những câuHashSetkhông có cửa trả lời.Phần tử phải so sánh được: tự
implements Comparablehoặc đưaComparatorvào constructor (Ngày 38 học sâu) — quên cả hai làClassCastExceptionlúc chạy, một trong những runtime error "bất ngờ" kinh điển.Bẫy tinh vi đáng giá của bài: TreeSet coi bằng nhau theo compareTo, không theo
equals—BigDecimal("1.0")và("1.00")(Ngày 27) là một phần tử trong TreeSet nhưng hai trong HashSet: cùng dữ liệu, hai kết quả — hiểu để không ngã ngửa.
Phép toán tập hợp và đồ nghề
// Phép toán tập hợp — toán lớp 6, code một dòng
Set<String> lopJava = new HashSet<>(List.of("An", "Bình", "Chi"));
Set<String> lopSpring = new HashSet<>(List.of("Bình", "Chi", "Dũng"));
// HỢP (union): học ít nhất một lớp
Set<String> hoc = new HashSet<>(lopJava);
hoc.addAll(lopSpring); // [An, Bình, Chi, Dũng]
// GIAO (intersection): học CẢ hai lớp
Set<String> caHai = new HashSet<>(lopJava);
caHai.retainAll(lopSpring); // [Bình, Chi]
// HIỆU (difference): chỉ học Java
Set<String> chiJava = new HashSet<>(lopJava);
chiJava.removeAll(lopSpring); // [An]
// Lưu ý: các phép này SỬA set — copy trước khi tính (đúng bài Ngày 29)
// Đồ nghề quen từ Ngày 31 áp dụng nguyên xi:
Set<String> banGoc = Set.of("a", "b"); // bất biến, từ chối null
Set<String> anToan = Set.copyOf(nguonBenNgoai); // defensive copy
// Enum? Có Set chuyên dụng nhanh hơn hẳn (Ngày 23):
EnumSet<TrangThai> dangXuLy = EnumSet.of(TrangThai.MOI, TrangThai.DANG_GIAO);
// bitmask bên trong — nhỏ và nhanh hơn HashSet nhiều lần cho enumUnion/intersection/difference =
addAll/retainAll/removeAll— nhưng cả ba sửa set tại chỗ: copy trước khi tính (Ngày 29), nếu không "tập gốc" biến mất không dấu vết.Đồ nghề Ngày 29 + 31 áp nguyên:
Set.ofcho hằng,Set.copyOfcho defensive copy — cùng tính cách ghétnullvà bất biến thật.Phần tử là enum?
EnumSet(Ngày 23) là bản chuyên dụng chạy bằng bitmask — nhỏ và nhanh hơnHashSetnhiều lần; cùng họ cóEnumMapsẽ gặp lại ở bài Map.
Bài tập nhỏ
Viết class
SinhVien(maSo, ten)không override gì, add 2 object cùng mã số vàoHashSet— giải thích size; chuyển sang record và chạy lại.Tái hiện phần tử "lạc": add một
ArrayListvào Set, sửa list, chứng minhcontainstrả false — rồi sửa thiết kế cho đúng.Cùng một list đầu vào, in ra qua
HashSet/LinkedHashSet/TreeSet— chạy 3 lần và ghi nhận loài nào ổn định.Dùng
TreeSetđiểm thi: tìm điểm cao nhất, thấp nhất, đếm số điểm trong khoảng 70–85 bằngsubSet— không vòng lặp nào.Chứng minh bẫy
compareTo: addBigDecimal 1.0và1.00vào cả TreeSet lẫn HashSet, so sánh size và giải thích.Hai danh sách email marketing: dùng phép giao tìm khách nhận cả hai chiến dịch, phép hiệu tìm khách chỉ nhận một — nhớ copy trước.
Kết luận
Bốn ý mang về: Set = lời hứa duy nhất với add trả boolean và contains O(1) làm nghề chính; ba anh em khác nhau ở thứ tự — Hash nhanh nhất, Linked giữ thứ tự chèn, Tree luôn sắp xếp với giá O(log n); họ Hash đứng trên equals/hashCode và cần phần tử bất biến, họ Tree đứng trên compareTo — hai định nghĩa "bằng nhau" có thể cho hai kết quả; và phép toán tập hợp ba dòng thay cả trang vòng lặp. Ngày 34 ta mở nắp capo cấu trúc quan trọng nhất java.util: HashMap hoạt động thế nào — bucket, hash, load factor, treeify — câu hỏi phỏng vấn quốc dân. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


