實作Comparator接口
Comparator介面的身影在JDK庫中隨處可見,從查找到排序,再到反轉操作,等等。 Java 8裡它變成了一個函數式接口,這樣的好處就是我們可以使用流式語法來實現比較器了。
我們用幾種不同的方式來實作一下Comparator,看看新式語法的價值所在。你的手指頭會感謝你的,不用實現匿名內部類別少敲了多少鍵盤啊。
使用Comparator進行排序
下面這個範例將使用不同的比較方法,來對一組人進行排序。我們先來建立一個Person的JavaBean。
複製代碼代碼如下:
public class Person {
private final String name;
private final int age;
public Person(final String theName, final int theAge) {
name = theName;
age = theAge;
}
public String getName() { return name; }
public int getAge() { return age; }
public int ageDifference(final Person other) {
return age - other.age;
}
public String toString() {
return String.format("%s - %d", name, age);
}
}
我們可以透過Person類別來實作Comparator接口,不過這樣我們只能使用一種比較方式。我們希望能比較不同的屬性――例如名字,年齡,或這些的組合。為了可以靈活的進行比較,我們可以使用Comparator,當我們需要進行比較的時候,再去產生相關的程式碼。
我們先來建立一個Person的列表,每個人都有不同的名字和年齡。
複製代碼代碼如下:
final List<Person> people = Arrays.asList(
new Person("John", 20),
new Person("Sara", 21),
new Person("Jane", 21),
new Person("Greg", 35));
我們可以透過人的名字或年齡來升序或降序的排序。一般的方法就是使用匿名內部類別來實作Comparator介面。這樣寫的話只有比較相關的程式碼是有意義的,其它的都只是走走形式而已。而使用lambda表達式則可以聚焦到比較的本質。
我們先將他們按年齡排序。
既然我們已經有了一個List對象,我們可以用它的sort()方法來進行排序。不過這個方法也有它的問題。這是一個void方法,也就是說當我們呼叫這個方法的時候,這個清單會改變。要保留原始清單的話,我們得先拷貝出一份來,然後再呼叫sort()方法。這簡直太費勁了。這時候我們得求助下Stream類別了。
我們可以從List那裡取得一個Stream對象,然後呼叫它的sorted()方法。它會傳回一個排好序的集合,而不是在原來的集合上做修改。使用這個方法的話可以很方便的配置Comparator的參數。
複製代碼代碼如下:
List<Person> ascendingAge =
people.stream()
.sorted((person1, person2) -> person1.ageDifference(person2))
.collect(toList());
printPeople("Sorted in ascending order by age: ", ascendingAge);
我們先透過stream()方法將列表轉換成一個Stream物件。然後呼叫它的sorted()方法。這個方法接受一個Comparator參數。由於Comparator是一個函數式接口,我們可以傳入一個lambda表達式。最後我們呼叫collect方法,讓它把結果儲存到一個列表裡。 collect方法是一個歸約器,它能把迭代過程中的物件輸出成某種特定的格式或型別。 toList()方法是Collectors類別的一個靜態方法。
Comparator的抽象方法compareTo()接收兩個參數,也就是要比較的對象,並傳回一個int型別的結果。為了相容於這個,我們的lambda表達式也接收兩個參數,兩個Person對象,它們的型別是由編譯器自動推導出來的。我們傳回一個int類型,表示比較的物件是否相等。
因為要按年齡排序,所以我們會比較兩個物件的年齡,然後再傳回比較的結果。如果他們一樣大,則返回0。否則如果第一個人更年輕的話就回傳一個負數,更年長的話就回傳正數。
sorted()方法會遍歷目標集合的每個元素並呼叫指定的Comparator,來決定元素的排序順序。 sorted()方法的執行方式有點類似前面說到的reduce()方法。 reduce()方法把列表逐步歸約出一個結果。而sorted()方法則是透過比較的結果來進行排序。
一旦我們排好序後我們想要把結果印出來,因此我們呼叫了一個printPeople()方法;下面來實作下這個方法。
複製代碼代碼如下:
public static void printPeople(
final String message, final List<Person> people) {
System.out.println(message);
people.forEach(System.out::println);
}
這個方法中,我們先列印了一個訊息,然後遍歷列表,列印出裡面的每個元素。
我們來呼叫下sorted()方法看看,它會將列表中的人按年齡從小到大進行排列。
複製代碼代碼如下:
Sorted in ascending order by age:
John - 20
Sara - 21
Jane - 21
Greg - 35
我們再看一下sorted()方法,來做一個改進。
複製代碼代碼如下:
.sorted((person1, person2) -> person1.ageDifference(person2))
在傳入的這個lambda表達式裡,我們只是簡單的路由了下這兩個參數――第一個參數作為ageDifference()方法的呼叫目標,而第二個作為它的參數。但是我們可以不這麼寫,而是用一個office-space模式――也就是使用方法引用,讓Java編譯器去做路由。
這裡用到的參數路由和前面看到的有點不同。我們之前看到的,要嘛參數是作為呼叫目標,要嘛是作為呼叫參數。而現在,我們有兩個參數,我們希望能分成兩個部分,一個是作為方法呼叫的目標,第二個則是作為參數。別擔心,Java編譯器會告訴你,「這個我來搞定」。
我們可以把前面的sorted()方法裡面的lambda表達式替換成一個短小精悍的ageDifference方法。
複製代碼代碼如下:
people.stream()
.sorted(Person::ageDifference)
這段程式碼非常簡潔,這多虧了Java編譯器提供的方法參考。編譯器接收到兩個person實例的參數,把第一個當作ageDifference()方法的呼叫目標,而第二個作為方法參數。我們讓編譯器去做這份工作,而不是自己直接去寫程式碼。當使用這種方式的時候,我們必須確定第一個參數就是引用的方法的呼叫目標,而剩下那個就是方法的入參。
重複使用Comparator
我們很容易就將清單中的人按年齡從小到大排好序了,當然從大到小進行排序也很容易。我們來試試看。
複製代碼代碼如下:
printPeople("Sorted in descending order by age: ",
people.stream()
.sorted((person1, person2) -> person2.ageDifference(person1))
.collect(toList()));
我們呼叫了sorted()方法並傳入一個lambda表達式,它剛好能適配成Comparator接口,就像前面的範例一樣。唯一不同的就是這個lambda表達式的實作――我們把要比較的人調了下順序。結果應該是依照他們的年齡從大到小排列的。我們來看一下。
複製代碼代碼如下:
Sorted in descending order by age:
Greg - 35
Sara - 21
Jane - 21
John - 20
只是改一下比較的邏輯費不了太多勁。但我們沒辦法把這個版本重構成方法引用的,因為參數的順序不符合方法引用的參數路由的規則;第一個參數並不是用來當作方法的呼叫目標,而是作為方法參數。有一個方法能解決這個問題,同時它也能減少重複的工作。我們來看下如何實現。
前面我們已經建立了兩個lambda表達式:一個是按年齡從小到大排序,一個是從大到小排序。這麼做的話,會出現程式碼冗餘和重複,並破壞了DRY原則。如果我們只是想要調整下排序順序的話,JDK提供了一個reverse方法,它有一個特殊的方法修飾符,default。我們會在77頁中的default方法來討論它,這裡我們先用下這個reversed()方法來去除冗餘性。
複製代碼代碼如下:
Comparator<Person> compareAscending =
(person1, person2) -> person1.ageDifference(person2);
Comparator<Person> compareDescending = compareAscending.reversed();
我們先建立了一個Comparator,compareAscending,來將人們依年齡從小到大排序。為了反轉比較順序,而不是再寫一次這個程式碼,我們只需要呼叫一下這個第一個Comparator的reversed()方法就可以取得第二個Comparator物件。在reversed()方法底層,它建立了一個比較器,來交換了比較的參數的順序。這說明reversed也是高階方法――它建立並傳回了一個無副作用的函數。我們把這個兩個比較器用到程式碼裡。
複製代碼代碼如下:
printPeople("Sorted in ascending order by age: ",
people.stream()
.sorted(compareAscending)
.collect(toList())
);
printPeople("Sorted in descending order by age: ",
people.stream()
.sorted(compareDescending)
.collect(toList())
);
從程式碼中明顯可以看到,Java8的這些新特性極大的減少了程式碼的冗餘及複雜度,不過好處遠不止這些,JDK裡還有無限可能等著你去探索。
我們已經可以按年齡排序了,想按名字來排序的話也很簡單。我們來按名字進行字典序排列,同樣的,只需要改下lambda表達式裡的邏輯就好了。
複製代碼代碼如下:
printPeople("Sorted in ascending order by name: ",
people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()))
.collect(toList()));
輸出的結果裡會依照名字的字典序來排列。
複製代碼代碼如下:
Sorted in ascending order by name:
Greg - 35
Jane - 21
John - 20
Sara - 21
現在為止,我們要嘛就按年齡排序,要嘛就按名字排序。我們可以讓lambda表達式的邏輯更聰明一些。例如我們可以同時按年齡和名字排序。
我們來選出名單中最年輕的人來。我們可以先按年齡從小到大排序然後選取結果中的第一個。不過其實用不著那樣,Stream有個min()方法可以實作這個。這個方法同樣也接受一個Comparator,不過回傳的是集合中最小的物件。我們來用下它。
複製代碼代碼如下:
people.stream()
.min(Person::ageDifference)
.ifPresent(youngest -> System.out.println("Youngest: " + youngest));
呼叫min()方法的時候我們用了ageDifference這個方法引用。 min()方法回傳的是一個Optinal對象,因為列表可能為空且裡面可能不只一個年紀最小的人。接著我們透過Optinal的ifPrsend()方法取得到年紀最小的那個人,並印出他的詳細資料。來看下輸出結果。
複製代碼代碼如下:
Youngest: John - 20
輸出年紀最大的同樣也很簡單。只要把這個方法引用傳給一個max()方法就好了。
複製代碼代碼如下:
people.stream()
.max(Person::ageDifference)
.ifPresent(eldest -> System.out.println("Eldest: " + eldest));
我們來看下最年長那位的名字和年齡。
複製代碼代碼如下:
Eldest: Greg - 35
有了lambda表達式和方法引用之後,比較器的實作變得更簡潔也更方便了。 JDK也為Compararor類別引入了不少便利的方法,使得我們可以更流暢的進行比較,下面我們將會看到。
多重比較和流式比較
我們來看下Comparator介面提供了哪些方便的新方法,並用它們來進行多個屬性的比較。
我們還是繼續使用上節的這個例子。依照名字排序的話,我們上面是這麼寫的:
複製代碼代碼如下:
people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()));
和上個世紀的內部類別的寫法比起來,這種寫法簡直太簡潔了。不過如果用了Comparator類別裡面的一些函數能讓它變得更簡單,使用這些函數能讓我們更流暢的表達自己的目的。比如說,要按名字排序的話,我們可以這麼寫:
複製代碼代碼如下:
final Function<Person, String> byName = person -> person.getName();
people.stream()
.sorted(comparing(byName));
這段程式碼中我們匯入了Comparator類別的靜態方法comparing()。 comparing()方法使用傳入的lambda表達式來產生一個Comparator物件。也就是說,它也是一個高階函數,接受一個函數入參並傳回另一個函數。除了能讓文法變得更簡潔外,這樣的程式碼讀起來也能更好的表達我們想要解決的實際問題。
有了它,進行多重比較的時候也能變得更流暢。例如,下面這段以名字和年齡比較的程式碼就能說明一切:
複製代碼代碼如下:
final Function<Person, Integer> byAge = person -> person.getAge();
final Function<Person, String> byTheirName = person -> person.getName();
printPeople("Sorted in ascending order by age and name: ",
people.stream()
.sorted(comparing(byAge).thenComparing(byTheirName))
.collect(toList()));
我們先是創建了兩個lambda表達式,一個回傳指定人的年齡,一個回傳的是他的名字。在呼叫sorted()方法的時候我們把這兩個表達式組合到了一起,這樣就能進行多個屬性的比較了。 comparing()方法創建並返回了一個按年齡比較的Comparator ,我們再調用這個返回的Comparator上面的thenComparing()方法來創建一個組合的比較器,它會對年齡和名字兩項進行比較。下面的輸出是先按年齡再按名字進行排序後的結果。
複製代碼代碼如下:
Sorted in ascending order by age and name:
John - 20
Jane - 21
Sara - 21
Greg - 35
可以看到,使用lambda表達式和JDK提供的新的工具類,可以輕鬆的將Comparator的實作進行組合。下面我們來介紹下Collectors。