CodeGym /課程 /JAVA 25 SELF /Comparator 介面:建立與使用

Comparator 介面:建立與使用

JAVA 25 SELF
等級 29 , 課堂 3
開放

1. 簡介

在實務中,比較物件很少只需要一種方式。想像你有一個使用者清單:有時想依名字排序,有時依年齡,有時則依姓氏長度。或者你手上的類別並非你所撰寫,因此無法為其加入 compareTo。針對這些情境,Java 提供了 Comparator 介面。

何時 Comparable 不夠用

  • 類別無法修改(例如來自第三方函式庫)。
  • 需要多種排序方式(依不同欄位)。
  • 希望將比較邏輯與類別本身分離(例如在程式不同部分採用不同排序方式)。

類比
如果 Comparable 是物件內建的「自然順序」,那麼 Comparator 就是外部的「裁判」,可以依任何標準評分你的物件:今天依名字、明天依年齡、後天依名字長度。

2. Comparator 介面:語法與契約

介面宣告

public interface Comparator<T> {
    int compare(T o1, T o2);
}

方法 compare 應回傳:

  • 負數,如果第一個物件「小於」第二個。
  • 0,如果它們相等。
  • 正數,如果第一個「大於」第二個.

契約與 Comparable 相同,只是現在比較的是兩個物件,而不是透過 compareTo 比較「當前」與「另一個」。

範例:依姓氏排序的比較器

假設有一個類別 Person

public class Person {
    private String firstName;
    private String lastName;
    private int age;

    // 建構子與 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; }
}

建立一個依姓氏排序的比較器:

import java.util.Comparator;

public class LastNameComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, Person b) {
        return a.getLastName().compareTo(b.getLastName());
    }
}

備註:字串(String)的 compareTo 會以字典順序比較。

3. 使用 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));

        // 依姓氏排序
        Collections.sort(people, new LastNameComparator());

        for (Person p : people) {
            System.out.println(p.getLastName() + " " + p.getFirstName());
        }
    }
}

結果:

Novak Boris
Kostetskaya Anna
Bell Viktoriya

使用比較器依年齡排序

即使類別已依姓名實作 Comparable,仍可另外建立一個比較器——依年齡:

public class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, Person b) {
        return Integer.compare(a.getAge(), b.getAge());
    }
}

使用方式相同:

Collections.sort(people, new AgeComparator());

結果:

Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)

範例:即時選擇比較器

Collections.sort(people, new LastNameComparator()); // 依姓氏
Collections.sort(people, new AgeComparator());      // 依年齡

4. 匿名類別與 Lambda 運算式

比較器可以「即時」建立,無需宣告成獨立類別。

匿名類別

Collections.sort(people, new Comparator<Person>() {
    @Override
    public int compare(Person a, Person b) {
        return a.getFirstName().compareTo(b.getFirstName());
    }
});

Lambda 運算式

Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));

或更簡潔地使用清單的方法 List.sort

people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
  • 匿名類別是較舊的作法,顯得冗長。
  • Lambda 現代且精簡。

5. 範例:依不同條件排序

依姓氏長度排序

Comparator<Person> byLastNameLength = (a, b) ->
        Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);

先依年齡、再依名字(多層排序)

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);

在搜尋中使用比較器(範例)

比較器不僅適用於排序,也可用於在已排序的集合中搜尋:

// people 必須已依年齡排序!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
    System.out.println("找到年齡為 22 的人:" + people.get(idx));
}

6. 最佳實務與使用 Comparator 的要點

不要違反契約

  • compare(a, b) 回傳 0,則 compare(b, a) 也必須回傳 0
  • compare(a, b) > 0,則 compare(b, a) < 0
  • 記得考慮可能的 null 值(見下)。

別忘了 equals 與 hashCode

雖然比較器可以「自行」比較物件,但對於 TreeSet 這類結構,或在 TreeMap 中搜尋鍵時,重要的是比較器的邏輯需與 equals 一致。否則可能出現預期外的行為:兩個不同的物件依比較器被視為相等,但依 equals 則不相等。

考慮 null 的排序

若欄位可能為 null,請使用現成的輔助方法:

Comparator<Person> byLastNameNullSafe = Comparator.comparing(
    Person::getLastName,
    Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);

7. 實用細節

表格:Comparable 與 Comparator 的比較

Comparable Comparator
在哪裡實作? 在類別本身 在獨立類別/Lambda
方法
int compareTo(T o)
int compare(T o1, T o2)
可有多少種? 僅一種「自然」 不限,多種需求皆可
用法
Collections.sort(list)
Collections.sort(list, comp)
可用於外部類別?

範例:遞減排序

可以手動反轉順序:

Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);

或使用 reversed()

Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());

8. 使用 Comparator 的常見錯誤

錯誤 №1:違反比較契約。 若你忘了 compare(a, b)compare(b, a) 的符號應相反,或回傳任意值(例如直接做差:a.getAge() - b.getAge(),可能溢位),結果將不可預期。請使用 Integer.compare,不要用減法——這樣較安全。

錯誤 №2:忽略 null 值。 若用於比較的欄位可能是 null,務必處理(例如透過 Comparator.nullsFirst/Comparator.nullsLast),否則很容易在最意想不到的時刻得到 NullPointerException

錯誤 №3:不穩定的排序準則。 若比較器對相同物件回傳不同值(例如使用隨機數或高度可變的欄位),排序可能表現混亂。

錯誤 №4:與 equals 不一致。compare(a, b) == 0,但 a.equals(b)false,像是 TreeSetTreeMap 這類集合可能不如你預期。理想情況是:比較器的相等與 equals 的相等一致。

錯誤 №5:對外部類別在沒有比較器的情況下進行排序。 若你嘗試對「外部」類別的物件排序,卻沒有 Comparable 也未提供 Comparator,將得到編譯錯誤。請傳入明確的比較器。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION