1. Giới thiệu
Trong thực tế, hiếm khi chỉ một cách so sánh đối tượng là đủ. Hãy tưởng tượng bạn có một danh sách người dùng: đôi khi bạn muốn sắp xếp theo tên, đôi khi — theo tuổi, và khi khác — theo độ dài họ. Hoặc bạn có một lớp không do bạn viết, nên bạn không thể thêm compareTo vào đó. Chính cho các trường hợp như vậy, trong Java có giao diện Comparator.
Khi Comparable là chưa đủ
- Không thể sửa lớp (ví dụ nó đến từ thư viện bên thứ ba).
- Cần nhiều cách sắp xếp khác nhau (theo các trường khác nhau).
- Muốn tách logic so sánh khỏi bản thân lớp (ví dụ sắp xếp khác nhau ở các phần khác nhau của chương trình).
Ẩn dụ
Nếu Comparable là “thứ tự tự nhiên” tích hợp của đối tượng, thì Comparator là vị trọng tài bên ngoài có thể đánh giá các đối tượng của bạn theo bất kỳ tiêu chí nào: hôm nay theo tên, ngày mai — theo tuổi, ngày kia — theo độ dài tên.
2. Giao diện Comparator: cú pháp và hợp đồng
Khai báo giao diện
public interface Comparator<T> {
int compare(T o1, T o2);
}
Phương thức compare phải trả về:
- Số âm nếu đối tượng thứ nhất “nhỏ hơn” đối tượng thứ hai.
- 0 nếu chúng bằng nhau.
- Số dương nếu đối tượng thứ nhất “lớn hơn” đối tượng thứ hai.
Hợp đồng giống như của Comparable, chỉ khác là bây giờ so sánh hai đối tượng, chứ không phải “đối tượng hiện tại” và “đối tượng khác” thông qua compareTo.
Ví dụ: comparator để sắp xếp theo họ
Giả sử chúng ta có lớp Person:
public class Person {
private String firstName;
private String lastName;
private int age;
// Constructor và getter
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}
Tạo comparator để sắp xếp theo họ:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
Ghi chú: phương thức compareTo của chuỗi (String) so sánh theo thứ tự bảng chữ cái.
3. Sử dụng Comparator: sắp xếp các collection
Sắp xếp bằng comparator
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", "Kostetskaya", 25));
people.add(new Person("Boris", "Novak", 20));
people.add(new Person("Viktoriya", "Bell", 22));
// Sắp xếp theo họ
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
Kết quả:
Novak Boris
Kostetskaya Anna
Bell Viktoriya
Sắp xếp theo tuổi bằng comparator
Ngay cả khi lớp đã triển khai Comparable theo tên, bạn vẫn có thể tạo một comparator riêng — theo tuổi:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
Và dùng tương tự:
Collections.sort(people, new AgeComparator());
Kết quả:
Boris Novak (20)
Bell Viktoriya (22)
Anna Kostetskaya (25)
Ví dụ: chọn comparator “ngay tại chỗ”
Collections.sort(people, new LastNameComparator()); // Theo họ
Collections.sort(people, new AgeComparator()); // Theo tuổi
4. Lớp ẩn danh và biểu thức lambda
Có thể tạo comparator “tức thời” mà không cần khai báo lớp riêng.
Lớp ẩn danh
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Biểu thức lambda
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
Hoặc ngắn gọn hơn bằng phương thức List.sort:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- Lớp ẩn danh — cách cũ, cồng kềnh.
- Lambda — hiện đại và gọn gàng.
5. Ví dụ: sắp xếp theo nhiều tiêu chí
Sắp xếp theo độ dài họ
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
Sắp xếp theo tuổi, rồi theo tên (nhiều cấp)
Comparator<Person> byAgeThenName = (a, b) -> {
int cmp = Integer.compare(a.getAge(), b.getAge());
if (cmp != 0) return cmp;
return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);
Dùng comparator cho tìm kiếm (ví dụ)
Comparator không chỉ áp dụng cho sắp xếp, mà còn cho tìm kiếm trong các collection đã sắp xếp:
// danh sách people phải được sắp xếp theo tuổi!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("Đã tìm thấy người có độ tuổi 22: " + people.get(idx));
}
6. Best practices và những điểm cần lưu ý khi làm việc với Comparator
Đừng vi phạm hợp đồng
- Nếu compare(a, b) trả về 0, thì compare(b, a) cũng phải trả về 0.
- Nếu compare(a, b) > 0, thì compare(b, a) < 0.
- Tính đến khả năng có giá trị null (xem bên dưới).
Đừng quên equals và hashCode
Mặc dù comparator so sánh đối tượng “theo cách riêng”, đối với các cấu trúc như TreeSet hoặc khi tìm khóa trong TreeMap, điều quan trọng là logic so sánh bằng comparator nên đồng thuận với equals. Nếu không, bạn có thể gặp kết quả bất ngờ: hai đối tượng khác nhau được coi là bằng nhau theo comparator, nhưng lại không bằng nhau theo equals.
Sắp xếp có tính đến null
Nếu các trường có thể là null, hãy dùng các helper “có sẵn”:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. Những điểm tinh tế hữu ích
Bảng: So sánh Comparable và Comparator
| Comparable | Comparator | |
|---|---|---|
| Được triển khai ở đâu? | Trong chính lớp | Trong lớp/lambda riêng |
| Phương thức | |
|
| Có bao nhiêu biến thể? | Chỉ một “tự nhiên” | Không giới hạn, tùy nhu cầu |
| Cách dùng | |
|
| Dùng được cho lớp của người khác không? | Không | Có |
Ví dụ: sắp xếp giảm dần
Có thể đảo thứ tự thủ công:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
Hoặc dùng reversed():
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. Những lỗi thường gặp khi làm việc với Comparator
Lỗi số 1: Vi phạm hợp đồng so sánh. Nếu bạn quên rằng compare(a, b) và compare(b, a) phải trái dấu, hoặc trả về các giá trị tùy tiện (ví dụ chỉ lấy hiệu — a.getAge() - b.getAge(), có thể tràn), thì kết quả sẽ khó lường. Hãy dùng Integer.compare thay vì phép trừ — an toàn hơn.
Lỗi số 2: Bỏ qua giá trị null. Nếu các trường bạn so sánh có thể là null, nhất định phải xử lý trường hợp này (ví dụ qua Comparator.nullsFirst/Comparator.nullsLast), nếu không bạn rất dễ nhận NullPointerException vào lúc không ngờ.
Lỗi số 3: Tiêu chí sắp xếp không ổn định. Nếu comparator trả về các giá trị khác nhau cho cùng một cặp đối tượng (ví dụ dùng số ngẫu nhiên hoặc một trường thay đổi nhiều), việc sắp xếp có thể trở nên hỗn loạn.
Lỗi số 4: Không nhất quán với equals. Nếu compare(a, b) == 0, nhưng a.equals(b) là false, các collection như TreeSet và TreeMap có thể hoạt động khác mong đợi. Tốt nhất là sự bằng nhau theo comparator và theo equals nên trùng khớp.
Lỗi số 5: Sắp xếp lớp của người khác mà không có comparator. Nếu bạn cố gắng sắp xếp các đối tượng của một lớp “bên ngoài” mà không có Comparable và cũng không truyền Comparator, bạn sẽ gặp lỗi biên dịch. Hãy truyền một comparator tường minh.
GO TO FULL VERSION