CodeGym /Các khóa học /JAVA 25 SELF /Giao diện Comparator: tạo và sử dụng

Giao diện Comparator: tạo và sử dụng

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

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
int compareTo(T o)
int compare(T o1, T o2)
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
Collections.sort(list)
Collections.sort(list, comp)
Dùng được cho lớp của người khác không? Không

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)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)false, các collection như TreeSetTreeMap 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.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION