1. Giới thiệu
Hãy bắt đầu với một ví dụ đời thường. Hãy tưởng tượng bạn tổ chức một bữa tiệc và lập danh sách khách mời. Bạn gửi lời mời, rồi hóa ra cùng một người xuất hiện trong danh sách hai lần (thậm chí ba lần — vì người đó rất thích tiệc tùng!). Nếu bạn dùng danh sách thông thường (List), các bản sao như vậy rất dễ xuất hiện. Nhưng nếu bạn có một collection không cho phép thêm cùng một vị khách hai lần — cuộc sống sẽ đơn giản hơn.
Đây là lúc collection Set — tập hợp — phát huy tác dụng.
Set là collection chỉ lưu các phần tử duy nhất. Nếu bạn cố thêm một phần tử đã tồn tại, nó đơn giản sẽ không được thêm (và chẳng ai phật ý cả).
Giao diện Set: các thuộc tính chính
Trong Java, Set là một interface xác định hành vi của collection không có bản sao. Nó kế thừa từ interface Collection, vì vậy hỗ trợ các thao tác như thêm (add), xóa (remove), kiểm tra sự tồn tại (contains) và duyệt.
Đặc điểm then chốt:
- Trong Set không thể có hai phần tử giống hệt nhau.
- Các phần tử có thể được lưu theo thứ tự tùy ý — phụ thuộc vào triển khai cụ thể.
- Không có chỉ mục: không thể truy cập phần tử theo số thứ tự như trong danh sách.
Cú pháp khai báo
Set<String> guests = new HashSet<>();
2. HashSet: nhanh, đơn giản, không có thứ tự
HashSet là triển khai phổ biến nhất của interface Set. Nó dựa trên bảng băm (giống như HashMap, nhưng không có cặp “khóa-giá trị”, chỉ có các giá trị duy nhất). Ưu điểm chính là tốc độ của các thao tác thêm, xóa và tìm kiếm.
HashSet hoạt động như thế nào?
Hãy hình dung một chiếc hộp nơi bạn để đồ. Để nhanh chóng hiểu trong đó đã có thứ gì tương tự chưa, mỗi món đồ nhận một “mã” — mã băm. Khi bạn thêm một phần tử vào HashSet, trước tiên mã băm này được tính toán. Nếu chưa từng gặp mã đó, phần tử sẽ được đưa vào collection một cách bình thường. Nếu mã đã tồn tại, thì sẽ kiểm tra thêm về tính bằng nhau thông qua equals(). Và chỉ khi các đối tượng thực sự trùng nhau, phần tử mới sẽ không được thêm.
Nói cách khác, HashSet tự động đảm bảo tính duy nhất: hai đối tượng giống hệt nhau sẽ không xuất hiện trong nó.
Điểm thú vị: nếu bạn làm việc với các lớp do bạn tự định nghĩa và muốn lưu chúng trong HashSet, bạn phải ghi đè các phương thức equals() và hashCode(). Nếu không, collection có thể hoạt động khó lường — các đối tượng tưởng như giống nhau sẽ bị coi là khác nhau.
Các phương thức chính của HashSet
Set<String> guests = new HashSet<>();
guests.add("Ivan");
guests.add("Mariya");
guests.add("Pyotr");
guests.add("Ivan"); // Bản sao! Sẽ không được thêm.
System.out.println(guests); // [Ivan, Mariya, Pyotr] — thứ tự có thể bất kỳ
guests.remove("Pyotr"); // Xóa phần tử
System.out.println(guests.contains("Mariya")); // true
System.out.println(guests.size()); // 2
Hãy thử điều này trong mã
Giả sử trong ứng dụng của chúng ta, ta muốn lưu tên nhiệm vụ duy nhất, để không có hai nhiệm vụ trùng tên:
import java.util.HashSet;
import java.util.Set;
public class UniqueTasksDemo {
public static void main(String[] args) {
Set<String> tasks = new HashSet<>();
tasks.add("Làm bài tập về Java");
tasks.add("Vuốt ve con mèo");
tasks.add("Làm bài tập về Java"); // Bản sao!
System.out.println("Danh sách nhiệm vụ:");
for (String task : tasks) {
System.out.println("- " + task);
}
// Trong danh sách sẽ chỉ có hai nhiệm vụ, bản sao sẽ không được thêm
}
}
3. TreeSet: khi thứ tự quan trọng!
Đôi khi chúng ta cần không chỉ tính duy nhất mà còn được sắp xếp các phần tử. Ví dụ, muốn xem tên khách theo thứ tự chữ cái thay vì ngẫu nhiên. Khi đó dùng TreeSet.
TreeSet là triển khai của interface Set lưu các phần tử theo thứ tự sắp xếp tăng dần. Nó dựa trên cấu trúc “cây đỏ-đen”.
Ví dụ sử dụng TreeSet
import java.util.Set;
import java.util.TreeSet;
public class SortedGuestsDemo {
public static void main(String[] args) {
Set<String> guests = new TreeSet<>();
guests.add("Vladimir");
guests.add("Aleksey");
guests.add("Ekaterina");
guests.add("Aleksey"); // Bản sao!
System.out.println("Khách (theo thứ tự chữ cái):");
for (String guest : guests) {
System.out.println("- " + guest);
}
// Kết quả:
// - Aleksey
// - Vladimir
// - Ekaterina
}
}
Lưu ý: Nếu bạn thêm bản sao, nó sẽ không xuất hiện trong tập hợp. Mọi thứ hoạt động đúng như mong đợi!
Khi nào dùng TreeSet?
- Khi cần một tập các phần tử duy nhất được sắp xếp.
- Khi cần tìm kiếm nhanh nhưng tốc độ thêm không quá quan trọng (chậm hơn một chút so với HashSet).
- Nếu phần tử là lớp do bạn tự định nghĩa, chúng phải “có thể so sánh” (cài đặt interface Comparable) hoặc bạn phải cung cấp Comparator của riêng mình.
4. Những điểm hữu ích
HashSet vs TreeSet: chọn cái nào?
| Tiêu chí | HashSet | TreeSet |
|---|---|---|
| Thứ tự lưu trữ | Không được đảm bảo | Được sắp xếp tăng dần |
| Tốc độ thao tác | Nhanh hơn (O(1)) | Chậm hơn (O(log n)) |
| Yêu cầu kiểu | Bất kỳ (chỉ cần equals()/hashCode()) | Comparable hoặc Comparator |
| Kịch bản điển hình | Khi cần truy cập nhanh tới các phần tử duy nhất | Khi cần xuất/duyệt có thứ tự |
Đặc điểm khi làm việc với Set
- Không có chỉ mục. Khác với List, Set không có phương thức get(int index). Nếu cần truy cập theo chỉ mục — hãy dùng List.
- Không có bản sao. Nếu bạn cố thêm một phần tử đã có sẵn, nó sẽ không được thêm. Phương thức add sẽ trả về false.
- Không đảm bảo thứ tự (trừ TreeSet). Trong HashSet, thứ tự các phần tử có thể khác nhau mỗi lần chạy. Nếu cần thứ tự chèn, hãy dùng LinkedHashSet.
- Giá trị null.
- HashSet cho phép lưu một phần tử null.
- TreeSet không cho phép thêm null nếu không có Comparator đặc biệt, nếu không sẽ ném NullPointerException.
5. Bài toán điển hình cho Set
Loại bỏ phần tử trùng lặp khỏi danh sách
Giả sử chúng ta có danh sách sinh viên, trong đó một số người bị ghi hai lần. Cần giữ lại các tên duy nhất:
import java.util.*;
public class RemoveDuplicatesDemo {
public static void main(String[] args) {
List<String> students = Arrays.asList("Anna", "Igor’", "Anna", "Mariya", "Igor’", "Pavel");
Set<String> uniqueStudents = new HashSet<>(students);
System.out.println("Sinh viên duy nhất: " + uniqueStudents);
// Thứ tự không được đảm bảo!
}
}
Nếu cần kết quả đã sắp xếp — hãy dùng TreeSet:
Set<String> sortedUniqueStudents = new TreeSet<>(students);
System.out.println("Sinh viên duy nhất (theo bảng chữ cái): " + sortedUniqueStudents);
Kiểm tra tính duy nhất (ví dụ, login người dùng)
Set<String> usedLogins = new HashSet<>();
usedLogins.add("student1");
usedLogins.add("java_lover");
String newLogin = "student1";
if (usedLogins.contains(newLogin)) {
System.out.println("Tên đăng nhập này đã được sử dụng!");
} else {
System.out.println("Tên đăng nhập còn trống!");
}
Duyệt các phần tử của tập hợp
Duyệt được thực hiện bằng vòng lặp for-each:
for (String name : uniqueStudents) {
System.out.println(name);
}
6. Lỗi thường gặp khi làm việc với Set
Lỗi số 1: Mong đợi một thứ tự xác định của phần tử trong HashSet. Nhiều người mới ngạc nhiên vì sao các phần tử của tập hợp được in ra theo “thứ tự kỳ lạ”. Điều đó là bình thường — HashSet không đảm bảo thứ tự. Nếu cần thứ tự chèn — hãy dùng LinkedHashSet, nếu cần sắp xếp — TreeSet.
Lỗi số 2: Cố gắng truy cập phần tử theo chỉ mục. Đôi khi người ta cố viết thứ gì đó như set.get(0). Không thể như vậy: Set không hỗ trợ chỉ mục. Cần truy cập theo chỉ mục — hãy dùng List.
Lỗi số 3: Lưu các đối tượng có thể thay đổi. Nếu bạn lưu các đối tượng có thể thay đổi các trường tham gia vào equals()/hashCode(), sau khi thay đổi các trường này, phần tử có thể “bị mất” khỏi tập hợp. Hãy làm cho phần tử bất biến hoặc đừng thay đổi các trường nhận diện.
Lỗi số 4: Kỳ vọng rằng bản sao sẽ được thêm vào. Thêm cùng một phần tử nhiều lần sẽ không làm tăng kích thước tập hợp — các bản sao bị bỏ qua, phương thức add sẽ trả về false.
Lỗi số 5: Sử dụng kiểu nguyên thủy. Cú pháp như Set<int> sẽ không biên dịch. Hãy dùng các lớp bao: Set<Integer>, Set<Double>, v.v.
GO TO FULL VERSION