初試 Lambda
June 24, 2022來看個匿名類別的應用場合,舉例而言,將名稱依長度進行排序:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, new Comparator<String>() {
public int compare(String name1, String name2) {
return name1.length() - name2.length();
}
});
匿名類別
Arrays
的 sort
方法可以用來排序,只不過,你得告訴他兩個元素比較時順序為何,sort
規定你得實作 java.util.Comparator
來說明這件事,然而那個匿名類別的語法有些冗長,如果想稍微改變一下 Arrays.sort
該行的可讀性,可以如下:
Comparator<String> byLength = new Comparator<String>() {
public int compare(String name1, String name2) {
return name1.length() - name2.length();
}
};
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, byLength);
透過變數 byLength
,確實是可以讓排序的意圖清楚許多,只是實作 java.util.Comparator
時的匿名類別時依舊冗長,有太多重複的資訊。
Lambda 運算式
例如,宣告 byLength
時已經寫了一次 Comparator<String>
,為什麼實作匿名類別時又得寫一次 Comparator<String>
?使用 Lambda 特性的話,可以寫為:
Comparator<String> byLength = (String name1, String name2) -> name1.length() - name2.length();
重複的 Comparator<String>
資訊從等號右邊去除了,而因為這個匿名類別只有一個方法要實作,因此實際上從等號左邊的 Comparator<String>
宣告就可以知道,實際上是要實作 Comparator<String>
的 compare
方法。
仔細看看,還有重複的資訊,既然宣告變數時使用了 Comparator<String>
,為什麼的參數上又得宣告一次 String
?確實不用,因為編譯器確實可以從變數的宣告型態得知這個資訊,因此可以再簡化為:
Comparator<String> byLength = (name1, name2) -> name1.length() - name2.length();
等號右邊的運算式是夠簡短了,將它直接放到 Arrays
的 sort
方法中:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, (name1, name2) -> name1.length() - name2.length());
因為編譯器可以從 names
推斷,sort
方法的第二個參數型態實際上就是 Comparator<String>
,因而 name1
與 name2
還是不用宣告型態;跟一開始的匿名類別寫法相比較,這邊的程式碼確實是簡潔許多,那麼,Lambda 只是匿名類別的語法蜜糖嗎?不!還有許多細節會在後續介紹,現在還是先集中重複性的去除與可讀性的改善。
方法參考
如果你在許多地方,都會有依字串長度排序的需求,那你會怎麼做?如果是同一個方法內,那麼就像之前,用個 byName
區域變數吧!如果是多個方法間要共用,那就用個 byName
的值域(Field)成員吧!因為 byName
要參考的實例沒有狀態問題,因而宣告為 static
比較適合,如果要在多個類別之間共用,那麼就設定為 public static
如何?例如:
package cc.openhome;
public class StringOrder {
public static int byLength(String s1, String s2) {
return s1.length() - s2.length();
}
public static int byLexicography(String s1, String s2) {
return s1.compareTo(s2);
}
public static int byLexicographyIgnoreCase(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
}
這次你聰明一些了,將一些字串排序時可能的方式都定義出來了,原本的依名稱長度排序就可以改寫為:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, (name1, name2) -> StringOrder.byLength(name1, name2));
也許你發現了,除了方法名稱之外,byLength
方法的簽署與 Comparator
的 compare
方法相同,我們只是在 Lambda 運算式中將參數 s1
與 s2
傳給 byLength
方法,這不是重複是什麼?可以直接重用byLength方法的實作不是更好嗎?方法參考(Method reference)的特性可以達到這個目的:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, StringOrder::byLength);
方法參考在重用現有 API 扮演了重要的角色。重用現有的方法實作,可避免到處寫下 Lambda 運算式。上面的例子是運用了方法參考中的一種形式,參考了 static
方法。
現在來看看另一個需求,如果想依字典順序排序名稱呢?因為你已經定義了 StringOrder
,也許你會這麼撰寫:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, StringOrder::byLexicography);
嗯!?仔細看看,StringOrder
的 byLexicography
,只不過是呼叫 String
的 compareTo
方法,也就是將第一個參數 s1
作為 compareTo
主詞,第二個參數 s2
作 compareTo
方法的受詞,在這種情況下,其實我們可以直接參考 String
類別的 compareTo
方法,例如:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, String::compareTo);
類似地,想對名稱按照字典順序排序,但忽略大小寫差異,也不用再透過 StringOrder
的 static
方法了,只要直接參考 String
的 compareToIgnoreCase
方法:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, String::compareToIgnoreCase);
可輕易觀察到,方法參考不僅避免了重複撰寫 Lambda 運算式,也可以讓程式碼更為清楚。