
Chào các em, "dev nhí" Gen Z tương lai! Anh Creyt đây, và hôm nay chúng ta sẽ "bung lụa" một khái niệm mà nói thật, nếu không hiểu nó thì code của các em sẽ như một cái chợ mà ai cũng có thể vào, muốn làm gì thì làm, và tệ hơn là... có hàng tá đồ giống nhau. Đó chính là HashSet!
HashSet là gì mà "hot" vậy anh Creyt?
Tưởng tượng thế này nhé: Các em có một cái album sticker, nhưng không phải album nào cũng được đâu, đây là album "chất lừ" mà mỗi sticker chỉ được dán một lần duy nhất. Nếu có sticker y chang, thì xin lỗi, "next page" nhé, không có chỗ cho hai đứa giống nhau đâu!
Đó chính là HashSet trong Java. Nó là một loại "tập hợp" (hay còn gọi là Set trong thế giới Collections Framework của Java) có mấy đặc điểm "độc quyền" sau:
- Độc nhất vô nhị (No Duplicates): Đây là "luật bất thành văn" của HashSet. Nó chỉ chứa các phần tử duy nhất. Nếu em cố gắng thêm một phần tử đã có, nó sẽ "lắc đầu" và không thêm vào đâu, nhưng cũng không báo lỗi gì đâu nhé. Nó cứ im lặng thôi, như "người yêu cũ" vậy.
- Không thứ tự (No Order): Đừng hòng mà nghĩ đến chuyện các phần tử sẽ được sắp xếp theo thứ tự khi em thêm vào hay lấy ra. HashSet là một "đứa trẻ tự do", nó thích sắp xếp theo cách của nó (thực ra là theo thuật toán băm - hashing), nên đừng bao giờ dựa vào thứ tự khi làm việc với nó.
- Tốc độ "thần sầu" (Fast Operations): Việc thêm (add), xóa (remove), hay kiểm tra sự tồn tại (contains) của một phần tử trong HashSet diễn ra cực kỳ nhanh, gần như là tức thì (constant time complexity - O(1) trung bình). Nó giống như em có một "siêu năng lực" có thể tìm thấy bất cứ thứ gì trong chớp mắt vậy.
Vậy tóm lại, HashSet dùng để làm gì? Đơn giản là để lưu trữ một bộ sưu tập các phần tử mà em chắc chắn rằng mỗi phần tử chỉ xuất hiện một lần duy nhất, và em cần các thao tác kiểm tra/thêm/xóa phải siêu nhanh.
Code Ví Dụ Minh Họa: "Thực chiến" cùng HashSet
Giờ thì, lý thuyết suông mãi chán lắm. Chúng ta cùng "nhúng tay" vào code để xem bạn HashSet này hoạt động ra sao nhé!
import java.util.HashSet;
import java.util.Set;
import java.util.Iterator; // Để duyệt qua các phần tử
public class HashSetCreytDemo {
public static void main(String[] args) {
// 1. Khởi tạo một HashSet
// Tưởng tượng đây là cái album sticker của chúng ta, chỉ dán sticker tên trái cây
Set<String> fruitStickers = new HashSet<>();
System.out.println("Album sticker ban đầu: " + fruitStickers); // Rỗng toác
// 2. Thêm các phần tử vào HashSet
System.out.println("\n--- Thêm sticker ---");
fruitStickers.add("Táo");
fruitStickers.add("Chuối");
fruitStickers.add("Xoài");
System.out.println("Album sau khi dán 3 sticker: " + fruitStickers);
// 3. Thử thêm một phần tử đã có (sticker trùng)
System.out.println("Thử dán lại sticker 'Táo': " + fruitStickers.add("Táo")); // Sẽ trả về false
System.out.println("Album sau khi dán trùng 'Táo': " + fruitStickers); // Vẫn chỉ có 3 sticker thôi
// 4. Thêm một phần tử mới
System.out.println("Thêm sticker 'Dứa': " + fruitStickers.add("Dứa")); // Sẽ trả về true
System.out.println("Album sau khi dán 'Dứa': " + fruitStickers);
// 5. Kiểm tra sự tồn tại của một phần tử
System.out.println("\n--- Kiểm tra sticker ---");
System.out.println("Có sticker 'Chuối' không? " + fruitStickers.contains("Chuối"));
System.out.println("Có sticker 'Ổi' không? " + fruitStickers.contains("Ổi"));
// 6. Xóa một phần tử
System.out.println("\n--- Gỡ sticker ---");
System.out.println("Gỡ sticker 'Xoài': " + fruitStickers.remove("Xoài")); // Sẽ trả về true
System.out.println("Album sau khi gỡ 'Xoài': " + fruitStickers);
System.out.println("Thử gỡ sticker 'Ổi' không có: " + fruitStickers.remove("Ổi")); // Sẽ trả về false
// 7. Duyệt qua các phần tử (nhớ là không có thứ tự nhé!)
System.out.println("\n--- Các sticker còn lại trong album ---");
for (String fruit : fruitStickers) {
System.out.println("- " + fruit);
}
// Hoặc dùng Iterator (cách cổ điển hơn)
System.out.println("\n--- Duyệt bằng Iterator ---");
Iterator<String> iterator = fruitStickers.iterator();
while (iterator.hasNext()) {
System.out.println("* " + iterator.next());
}
// 8. Lấy số lượng phần tử
System.out.println("\nTổng số sticker còn lại: " + fruitStickers.size());
// 9. Xóa tất cả các phần tử
fruitStickers.clear();
System.out.println("Album sau khi gỡ hết sticker: " + fruitStickers);
System.out.println("Album có rỗng không? " + fruitStickers.isEmpty());
}
}
Giải thích code:
new HashSet<>();: Tạo một HashSet rỗng.add("Táo"): Thêm "Táo". Nếu "Táo" đã có, nó sẽ không thêm và trả vềfalse. Nếu chưa có, nó thêm và trả vềtrue.contains("Chuối"): Kiểm tra xem "Chuối" có trong Set không.remove("Xoài"): Xóa "Xoài".for (String fruit : fruitStickers): Duyệt qua các phần tử. Nhớ là thứ tự xuất hiện có thể khác mỗi lần chạy nhé!

Mẹo "Hack não" từ anh Creyt (Best Practices)
Để dùng HashSet "ngon lành cành đào" và không bị "bug" lặt vặt, các em cần nhớ vài điều sau:
-
Khi nào thì dùng HashSet?
- Khi em cần một danh sách các phần tử mà không được phép trùng lặp.
- Khi em cần kiểm tra sự tồn tại của một phần tử cực nhanh.
- Khi em không quan tâm đến thứ tự của các phần tử.
- Ví dụ: Lưu trữ các ID người dùng đang online, các từ khóa duy nhất của một bài viết, các số điện thoại đã đăng ký.
-
Cẩn thận với object tùy chỉnh (Custom Objects)! Nếu em lưu trữ các đối tượng do mình tự định nghĩa (ví dụ:
Set<Student>), thì việc "độc nhất vô nhị" không còn đơn giản nữa. HashSet sẽ dùng phương thứchashCode()vàequals()của đối tượng để xác định xem hai đối tượng có giống nhau hay không.- Mẹo: Luôn luôn
override(ghi đè) cảhashCode()vàequals()cho các lớp tùy chỉnh mà em định đưa vàoHashSet(hoặc các Collection khác dùng hashing nhưHashMap). Nếu không, hai đối tượng có cùng giá trị nhưng khác địa chỉ bộ nhớ sẽ bị coi là khác nhau, và HashSet sẽ cho phép cả hai cùng tồn tại, "phá vỡ" nguyên tắc "độc nhất" của nó. Các IDE hiện đại như IntelliJ IDEA hay Eclipse có thể tự động sinh ra hai phương thức này cho em đó!
- Mẹo: Luôn luôn
-
Đừng bao giờ tin vào thứ tự! Anh nói rồi, HashSet không giữ thứ tự. Nếu em cần một tập hợp các phần tử duy nhất và phải có thứ tự (ví dụ: theo thứ tự thêm vào hoặc theo thứ tự tự nhiên), thì em nên dùng
LinkedHashSet(giữ thứ tự thêm vào) hoặcTreeSet(sắp xếp tự nhiên hoặc theoComparator).
-
Hiệu suất (Performance):
- HashSet hoạt động dựa trên bảng băm (hash table). Kích thước ban đầu (initial capacity) và yếu tố tải (load factor) có thể ảnh hưởng đến hiệu suất. Mặc định là
16và0.75. Hiểu nôm na là khi số phần tử đạt 75% dung lượng, nó sẽ tự động tăng kích thước bảng băm lên gấp đôi. Nếu em biết trước số lượng phần tử lớn, việc cung cấp mộtinitial capacityphù hợp ngay từ đầu có thể giúp tránh việc thay đổi kích thước liên tục, giúp code "mượt" hơn.
- HashSet hoạt động dựa trên bảng băm (hash table). Kích thước ban đầu (initial capacity) và yếu tố tải (load factor) có thể ảnh hưởng đến hiệu suất. Mặc định là
Ứng dụng "thực chiến" trên các app/web mà các em đang dùng
HashSet không phải là một thứ "trên trời" đâu, nó được dùng rất nhiều trong các hệ thống mà các em tương tác hàng ngày:
- Shopee/Lazada: Khi em xem danh sách các sản phẩm đã xem gần đây, hệ thống cần đảm bảo mỗi sản phẩm chỉ xuất hiện một lần thôi, dù em có click vào xem bao nhiêu lần đi nữa.
- Facebook/Zalo: Danh sách bạn bè của em, danh sách người theo dõi. Hệ thống cần đảm bảo mỗi người chỉ là bạn/người theo dõi một lần.
- Các trang tin tức (Kênh 14, VnExpress): Khi hiển thị các "tags" (thẻ) liên quan đến bài viết, mỗi tag chỉ nên xuất hiện một lần. Hoặc thống kê số lượng người dùng duy nhất truy cập bài viết.
- Hệ thống chat (Discord, Slack): Danh sách người dùng đang online trong một kênh chat. Mỗi người dùng chỉ xuất hiện một lần.
- Trò chơi điện tử: Quản lý các vật phẩm độc nhất trong kho đồ của người chơi, hoặc các kỹ năng đã học.
Nên dùng cho case nào và đã từng "thử nghiệm" ra sao?
Anh Creyt đã từng "vật lộn" với các bài toán cần xử lý dữ liệu duy nhất và tốc độ cao.
Case 1: Lọc trùng dữ liệu từ file lớn.
Anh có một file log chứa hàng triệu dòng IP truy cập website, và nhiệm vụ là tìm ra tất cả các địa chỉ IP duy nhất. Nếu dùng ArrayList rồi duyệt từng cái để kiểm tra trùng thì "khóc thét" vì chậm như "rùa bò". Nhưng khi chuyển sang dùng HashSet, chỉ cần đọc từng IP và add vào HashSet, mọi thứ diễn ra "nhanh như một cơn gió". Tốc độ là sự khác biệt giữa việc chờ đợi hàng giờ và chỉ vài phút.
Case 2: Kiểm tra quyền truy cập.
Trong một hệ thống phân quyền, mỗi người dùng có thể có nhiều quyền (ví dụ: VIEW_PRODUCT, EDIT_PRODUCT, DELETE_PRODUCT). Khi người dùng đăng nhập, hệ thống cần nhanh chóng kiểm tra xem họ có quyền nào đó hay không. Lưu trữ các quyền của người dùng vào một HashSet<Permission> là lựa chọn hoàn hảo. Việc kiểm tra userPermissions.contains(Permission.EDIT_PRODUCT) sẽ cực kỳ nhanh.
Hướng dẫn nên dùng cho case nào:
- Loại bỏ các phần tử trùng lặp khỏi một danh sách hiện có.
- Kiểm tra nhanh chóng sự tồn tại của một phần tử.
- Lưu trữ các "đối tượng" mà sự "độc nhất" là quan trọng, ví dụ: ID, tên duy nhất, mã sản phẩm.
- Khi thứ tự của các phần tử không quan trọng.
Tóm lại, HashSet là một công cụ "đắc lực" trong bộ sưu tập Collections Framework của Java. Nắm vững nó, các em sẽ có thêm một "siêu năng lực" để xử lý dữ liệu hiệu quả hơn, đặc biệt là trong những tình huống cần tốc độ và sự "độc nhất vô nhị".
Ok, bài học hôm nay đến đây là kết thúc. "Dev nhí" nào còn thắc mắc, cứ "gào" lên nhé! Anh Creyt luôn sẵn sàng giải đáp!
Thuộc Series: Java – OOP
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!