【JDK8】排序策略的實作


在收集物件之後,對物件進行排序是常用的動作,JDK8 之前,基本上可使用 java.util.Arraysjava.util.Collectionssort 方法,而陣列或 java.util.List 收集的元素必須實作 java.lang.Comparable,或者呼叫 sort 方法時要指定 java.util.Comparator

搭配 Lambda 來排序

如果你使用 JDK8,因為 Comparator 介面需要實作的只有一個 compare 方法,因此若使用 sort 方法時要指定 Comparator,可以使用 Lambda 語法來讓它更簡潔一些:

List<String> words = Arrays.asList("B", "X", "A", "M", "F", "W", "O");
Collections.sort(words, (s1, s2) -> -s1.compareTo(s2));

List 的 sort 與 Stream 的 sorted

實際上,JDK8 在 List 上增加了 sort 方法,可接受 Comparator 實例來指定排序方式,因此你還可以寫成:

List<String> words = Arrays.asList("B", "X", "A", "M", "F", "W", "O");
words.sort((s1, s2) -> -s1.compareTo(s2));

如果只是想使用 StringcompareTo 方法,透過 JDK8 的方法參考特性,實際上還可以寫得更簡潔:

List<String> words = Arrays.asList("B", "X", "A", "M", "F", "W", "O");
words.sort(String::compareTo);

以上的排序,會直接在原有的 List 上進行,也就是改變了原本 List 上的元素順序,如果想要產生一個新的排序結果,不要影響原有的 Listjava.util.stream.Streamsorted 方法可以使用,你可以運用管線化操作風格來完成排序:

List<String> words = Arrays.asList("B", "X", "A", "M", "F", "W", "O")
                           .stream()
                           .sorted()
                           .collect(toList());

Streamsorted 也有接受 Comparator 的版本,範例中的 toList 方法,是 java.util.stream.Collectors 的靜態方法,有關 StreamCollectors 的使用,可以參考我撰寫的 使用 Stream 進行管線操作Stream 的 reduce 與 collect

在管線化操作時若使用 parallelStream,要注意使用 sorted 時可能會(或完全失去)失去平行化的一些優勢,可以參考 Stream 與平行化

附帶一提的是,Arrays 在 JDK8 中也新增了 parallelSort 方法,可以將指定的陣列分為子陣列並以平行化方式分別排序,然後再進行合併排序。

Comparator 的靜態方法

JDK8 中,介面上也可以定義靜態方法,Comparator 上就有一些不錯的方法。來考慮一個更複雜的情況,如果有個 List 中某些索引處包括 null,現在你打算讓那些 null 排在最前頭,之後依字串的長度由大到小排序,那會怎麼寫?這樣嗎?

public class StrLengthInverseNullFirstComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        if(s1 == s2) {
            return 0;
        }
        if(s1 == null) {
            return -1;
        }
        if(s2 == null) {
            return 1;
        }
        if(s1.length() == s2.length()) {
            return 0;
        }
        if(s1.length() > s2.length()) {
            return -1;
        } 
        return 1;
    }
}

不怎麼好讀,對吧!更別說為了表示這個比較器的目的,必須取個又臭又長的類別名稱!其實排序會有各式各樣的組合需求,JDK8 考量到這點,為排序加入了一些高階語義 API,例如 Comparator 上新增了一些靜態方法,結合這些方法,可以讓程式碼寫來具有較高的可讀性。

以方才的需求為例,在 JDK8 中要建立對應的 Comparator 實例,可以如下撰寫:

package cc.openhome;

import java.util.*;
import static java.util.Comparator.*;
import static java.lang.System.out;

public class Sort6 {
    public static void main(String[] args) {
        List words = Arrays.asList(
                "B", "X", "A", "M", null ,"F", "W", "O", null);
        words.sort(nullsFirst(reverseOrder()));
        out.println(words); // [null, null, X, W, O, M, F, B, A]
    }
}

reverseOrder 傳回的 Comparator 會是 Comparable 物件上定義順序的反序,nullsFirst 接受 Comparator,在其定義的順序上加上讓 null 排在最前頭的規則後,傳回新的 Comparator

Comparator 的預設方法

JDK8 中,介面上也可以定義預設方法(Default method),實際上,Comparator 也定義了一些預設方法,像是 thenComparing 方法,這可以讓你用更具彈性的方式,從既有的 Comparator 實例組合出更複雜比較條件的 Comparator 實例。

例如,你可能想要排序時先依客戶的姓來排,如果姓相同再依名來排,如果姓名都相同,再依他們居住地的郵遞區號來排,那麼你可以如下建立 Comparator

package cc.openhome;

import static java.lang.System.out;
import java.util.*;
import static java.util.Comparator.comparing;

public class CustomerDemo {
    public static void main(String[] args) {
        List<Customer> customers = Arrays.asList(
                new Customer("Justin", "Lin", 804),
                new Customer("Monica", "Huang", 804),
                new Customer("Irene", "Lin", 804)
        );

        Comparator<Customer> byLastName = comparing(Customer::getLastName);

        customers.sort(
                byLastName
                .thenComparing(Customer::getFirstName)
                .thenComparing(Customer::getZipCode)
        );

        customers.forEach(out::println);
    } 
}

class Customer {
    private String firstName;
    private String lastName;
    private Integer zipCode;

    public Customer(String firstName, String lastName, Integer zipCode) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.zipCode = zipCode;
    }

    public String toString() {
        return String.format("Customer(%s %s, %d)", firstName, lastName, zipCode);
    }

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public Integer getZipCode() { return zipCode; }
}

每次 Comparator 實例呼叫 thenComparing 方法,都會傳回新的 Comparator 實例,你就可以再呼叫 thenComparing 方法,組合出自己想要的排序方式。

如果你沒有使用 JDK8,基本上在 guava-libraries 上,也可以取得這類高階排序 API,這部份的內容可以參考 【Guava 教學】(3)高階排序概念的實現