匿名類別與 Lambda


在正式看Lambda之前,先來看個匿名類別的應用場合,舉例而言,將名稱依長度進行排序:

String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, new Comparator<String>() {
    public int compare(String name1, String name2) {
        return name1.length() - name2.length();
    }
});

Arrayssort()方法可以用來排序,只不過,你得告訴他兩個元素比較時順序為何,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時的匿名類別時依舊冗長,有太多重複的資訊,如果使用JDK8的話,你可以使用Lambda特性去除重複的資訊,我們一步一步來。

例如,宣告byLength時已經寫了一次Comparator<String>,為什麼實作匿名類別時又得寫一次Comparator<String>?使用JDK8的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();

等號右邊的運算式是夠簡短了,讓我們將它直接放到Arrayssort()方法中:

String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, (name1, name2) -> name1.length() - name2.length());

因為編譯器可以從names推斷,sort()方法的第二個參數型態實際上就是Comparator<String>,因而name1name2還是不用宣告型態;跟一開始的匿名類別寫法相比較,這邊的程式碼確實是簡潔許多,那麼,JDK8的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方法的簽署與Comparatorcompare方法相同,我們只是在Lambda運算式中將參數s1s2傳給byLength方法,這不是重複是什麼?可以直接重用byLength方法的實作不是更好嗎?是的,JDK8 提供了方法參考的特性,可以達到這個目的:

String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, StringOrder::byLength);

在Java中引入Lambda的同時,與現有API維持相容性是主要考量之一。方法參數(Method reference)在重用現有API上扮演了重要的角色。重用現有的方法實作,可避免到處寫下Lambda運算式。上面的例子是運用了方法參考中的一種形式,參考了static方法。

現在來看看另一個需求,如果想依字典順序排序名稱呢?因為你已經定義了StringOrder,也許你會這麼撰寫:

String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, StringOrder::byLexicography);

嗯!?仔細看看,StringOrderbyLexicography(),只不過是呼叫StringcompareTo()方法,也就是將第一個參數s1作為compareTo()主詞,第二個參數s2compareTo()方法的受詞,在這種情況下,其實我們可以直接參考String類別的compareTo方法,例如:

String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, String::compareTo);

類似地,想對名稱按照字典順序排序,但忽略大小寫差異,也不用再透過StringOrderstatic方法了,只需要直接參考StringcompareToIgnoreCase()方法:

String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, String::compareToIgnoreCase);

可輕易觀察到,方法參考不僅避免了重複撰寫Lambda運算式,也可以讓程式碼更為清楚。

這邊只是初嘗一下Lambda的甜頭,關於Lambda還有更多細節,後續我們再來一一探討。