切換語言為:簡體

這次帶你徹底搞懂Java中的比較器設計

  • 爱糖宝
  • 2024-09-13
  • 2054
  • 0
  • 0

一、引言

開始講解本文的知識點之前,請看下面的一段程式碼,猜測方法的輸出

@Data
class Address {
    private String provinceCode;
    private String cityCode;
    private String areaCode;
    public Address(String provinceCode, String cityCode, String areaCode) {
        this.provinceCode = provinceCode;
        this.cityCode = cityCode;
        this.areaCode = areaCode;
    }
}

@Data
class Student {
    private String name;
    private Integer age;
    private Integer examScore;
    private Address address;

    public Student(String name, Integer age, Integer examScore, Address address) {
        this.name = name;
        this.age = age;
        this.examScore = examScore;
        this.address = address;
    }
}

private static List<Student> mockStudentList(){
    List<Student> res = new ArrayList<>();
    res.add(new Student("笨笨", 16, 90, new Address("Anhui", "Hefei", "001")));
    res.add(new Student("李四", 17, 95, new Address("Anhui", "Hefei", "001")));
    res.add(new Student("王五", 20, 95, new Address("ShangHai", "PuDong", "100010")));
    res.add(new Student("張三", 21, 80, new Address("ShangHai", "PuDong", "100010")));
    res.add(new Student("康康", 21, 72, new Address("ShangHai", "PuDong", "100010")));
    return res;
}
public static void main(String[] args){
    List<Student> students = mockStudentList();
    // 方式1
    List<Student> collect = students.stream().sorted().collect(Collectors.toList());
    // 方式2
    // Arrays.sort(students.toArray(new Student[0]));
    // 請你判斷這裏的輸出?
    System.out.println(collect);
}

正確答案是排序會異常,無論是使用上面程式碼裡面的哪種方式去排序都會出現異常,其實這個問題的答案本身很好猜因為既然要對物件列表進行排序,最起碼程式碼得知道具體的排序規則;

對於上面的程式碼如果要改正有下面兩種辦法,例如我們就想簡單的按照分數排序:

方法1:去修改一下Student類的實現,去實現 Comparable 介面

@Data
public class Student implements Comparable<Student> {
    private String name;
    private Integer age;
    private Integer examScore;
    private Address address;

    public Student(String name, Integer age, Integer examScore, Address address) {
        this.name = name;
        this.age = age;
        this.examScore = examScore;
        this.address = address;
    }

    @Override
    public int compareTo(Student o) {
        return Integer.compare(this.examScore, o.examScore);
    }
}

方法2:去修改主方法中的排序邏輯,明確指定排序規則:

List<Student> students = mockStudentList();
List<Student> collect = students.stream().sorted(Comparator.comparing(Student::getExamScore)).collect(Collectors.toList());

System.out.println(collect);

上面的兩種方法對應的則是本文重點分析的兩個介面 一個是Comparable 一個是Comparator

二、Comparable

原始碼定義:

public interface Comparable<T> {
    public int compareTo(T o);
}

這個介面如果實現了就表明當前的類應該是具備一種“可比較的能力”,例如Integer、Double、BigDecimal類的實現,翻看原始碼,發現都實現了這個介面。因為這些型別一般表達數值的含義,而數字一般是可以比較的。

此外我們發現String型別也是實現了這個介面的,所以說字串是可以直接排序的原因也是如此

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
        //……
}

關於Comparable介面的要點其實不多 ,在本文開始的例子中,仔細分析原始碼會發現,進入到Java最佳化過的排序演算法“TimSort ”的邏輯以後,排序的元素實際上是要轉為Comparable型別的,如果參與排序的物件本身沒實現這個介面,自然上轉型就會失敗。

所以我們如果想直接使用SteamAPI的sorted方法或者是Arrays.sort()方法的時候都需要確保物件確實是“可比較的”

但是很明視訊記憶體在物件需要進行比較 但是又不希望去實現Comparable介面的場合,所以Java提供了另一個介面Comparator

三、Comparator

對於Comparator介面則需要好好研究下,首先我們檢查下原始碼,我們就會發現這個比較介面並不像Comparable那麼簡單,尤其是在Java8引入Default方法以後這個介面提供了大量預設方法:

這次帶你徹底搞懂Java中的比較器設計

透過這些方法就可以實現大量複雜的排序邏輯

比如現在有個需求是要求對本文的Student物件先按照分數排序從高到底排序,相同分數的按照省份編碼排序,同時需要將沒有得分的學生排到最後,這時候程式碼就可以這樣寫:

List<Student> students = mockStudentList();
List<Student> collect = students.stream()
        .sorted(Comparator.comparing(Student::getExamScore, Comparator.nullsFirst(Comparator.naturalOrder()))
                .reversed()
                .thenComparing(Student::getAddress, Comparator.comparing(Address::getProvinceCode)))
        .collect(Collectors.toList());
System.out.println(collect);

首先sorted方法需要接收一個Comparator型別的引數,所以後麵的這一連串的鏈式呼叫就是爲了構造一種物件之間的比較規則;

3.1 Comparator.comparing 方法

Comparator.comparing在原始碼中存在兩種實現一種是兩個引數的一種是一個引數的

先分析一個引數的

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

keyExtractor用於提取出進行比較的屬性,然後由於泛型的型別限制提取出來的型別必須實現Comparable介面所以隨後直接使用compareTo方法構造了一個Compartor的Lambda表示式返回。

public static <T, U> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor,
        Comparator<? super U> keyComparator)
{
    Objects.requireNonNull(keyExtractor);
    Objects.requireNonNull(keyComparator);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                          keyExtractor.apply(c2));
}

兩個引數的方法則是直接用第二個Comparator引數構造了一個Comparator Lambda表示式返回。也就是說兩個引數的實現就是用第一個函式提取出要比較的部分,然後用第二個引數的比較器進行比較,就比如我們前面寫的程式碼片段中:

thenComparing(Student::getAddress, Comparator.comparing(Address::getProvinceCode))

第一個引數是用於提取出Student例項中Address屬性,但是這個屬性依然是個物件,對於程式碼而言還是不知道怎麼對這個物件比較大小,所以提供了第二個引數告訴程式怎麼比較。

3.2 Comparator.nullsFirst 方法

在日常程式碼開發中,如果我們要對物件列表按照某個欄位值進行排序但是這個欄位值有可能為空,這時候如果我們直接排序是否會有問題呢?

就比如我們本文開始的例子,假如有學生的得分是null 那麼按照下面的排序方法 你將收穫熟悉的NPE異常:

private static List<Student> mockStudentList() {
    List<Student> res = new ArrayList<>();
    res.add(new Student("笨笨", 16, 90, new Address("Anhui", "Hefei", "001")));
    res.add(new Student("李四", 17, 95, new Address("Anhui", "Hefei", "001")));
    res.add(new Student("王五", 20, 95, new Address("ShangHai", "PuDong", "002")));
    res.add(new Student("張三", 21, 80, new Address("ShangHai", "PuDong", "100010")));
    // 有一個學生 沒有分數 就會直接導致排序異常
    res.add(new Student("康康", 21, null, new Address("ShangHai", "PuDong", "100010")));
    return res;
}

private static void demo1() {
    List<Student> students = mockStudentList();
    List<Student> collect = students.stream().sorted(Comparator.comparing(Student::getExamScore)).collect(Collectors.toList());
    System.out.println(collect);
}

那麼有沒有辦法避免呢 ? 當然有 只要我們在程式碼中顯示宣告,排序的時候把null值排在前面(nullsFirst)或者後面(nullsLast)就可以:

List<Student> collect = students.stream()
        .sorted(Comparator.comparing(Student::getExamScore, 
                                     Comparator.nullsLast(Comparator.naturalOrder())))
        .collect(Collectors.toList());

PS:其實在Java8有這套解決方案以前 在Spring裡面也有一個org.springframework.util.comparator.Comparators 類似的nullsLow 和nullsHigh來解決這種問題

有的讀者可能會對 Comparator.nullsLast(Comparator.naturalOrder())) 這種寫法有點疑問 為什麼nulllast還需要再接收一個Comparator 引數呢?這時候就需要去看看這個Comparator.nullsLast實現了。

在Comparator介面的同包下有個Comparators類(這個類沒有用public修飾),這個類支撐了Comparator介面大量Default方法的功能實現

這次帶你徹底搞懂Java中的比較器設計

在Comparators下面定義了NullComparator,這個類就是實現null值排序的關鍵 我們關注下關鍵程式碼:

 final static class NullComparator<T> implements Comparator<T>, Serializable {
    private final boolean nullFirst;   
    private final Comparator<T> real;

    NullComparator(boolean nullFirst, Comparator<? super T> real) {
        this.nullFirst = nullFirst;
        this.real = (Comparator<T>) real;
    }

    @Override
    public int compare(T a, T b) {
        if (a == null) {
            return (b == null) ? 0 : (nullFirst ? -1 : 1);
        } else if (b == null) {
            return nullFirst ? 1: -1;
        } else {
            // 留意下這裏
            return (real == null) ? 0 : real.compare(a, b);
        }
    }
 }

程式碼寫的非常清晰 ,我們傳遞進去的Comparator.naturalOrder() 比較器就是解決比較都不是null值的情況下如何進行比較的問題。

3.2 Comparator.naturalOrder() 方法

這個所謂的自然排序方法的實現我們也可以深究一下怎麼實現

// Comparator.java
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
    return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}


// Comparators.java
enum NaturalOrderComparator implements Comparator<Comparable<Object>> {
    INSTANCE;

    @Override
    public int compare(Comparable<Object> c1, Comparable<Object> c2) {
        return c1.compareTo(c2);
    }

    @Override
    public Comparator<Comparable<Object>> reversed() {
        return Comparator.reverseOrder();
    }
}

從上面的程式碼可以看出所謂的自然排序就是呼叫實現了Comparable 例項的compareTo方法而已;(ps:這種用列舉來實現介面的寫法,是不是還挺少見的

四、總結

下面總結下Comparable、Comparator、以及Comparators;

首先前面兩個是介面最後一個則是為Comparator介面提供了一些額外能力的一個支撐類

如果我們定義的類具備比較能力,就可以實現Comparable介面,複寫compareTo方法,如果我們比較的物件沒有實現這個介面但是仍然需要排序就需要透過Comparator介面提供的一系列方法去定義排序規則。

此外"列舉實現介面" 和 “介面定義default方法搭配一個同包許可權的支撐類"的設計思路也可以在日常開發中進行學習和借鑑

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.