在收集物件之後,對物件進行排序是常用的動作,JDK8 之前,基本上可使用 java.util.Arrays
或 java.util.Collections
的 sort
方法,而陣列或 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));
如果只是想使用 String
的 compareTo
方法,透過 JDK8 的方法參考特性,實際上還可以寫得更簡潔:
List<String> words = Arrays.asList("B", "X", "A", "M", "F", "W", "O");
words.sort(String::compareTo);
以上的排序,會直接在原有的 List
上進行,也就是改變了原本 List
上的元素順序,如果想要產生一個新的排序結果,不要影響原有的 List
,java.util.stream.Stream
有 sorted
方法可以使用,你可以運用管線化操作風格來完成排序:
List<String> words = Arrays.asList("B", "X", "A", "M", "F", "W", "O")
.stream()
.sorted()
.collect(toList());
Stream
的 sorted
也有接受 Comparator
的版本,範例中的 toList
方法,是 java.util.stream.Collectors
的靜態方法,有關 Stream
與 Collectors
的使用,可以參考我撰寫的 使用 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)高階排序概念的實現。