初試 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();
    }
});

匿名類別

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 時的匿名類別時依舊冗長,有太多重複的資訊。

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();

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

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

因為編譯器可以從 names 推斷,sort 方法的第二個參數型態實際上就是 Comparator<String>,因而 name1name2 還是不用宣告型態;跟一開始的匿名類別寫法相比較,這邊的程式碼確實是簡潔許多,那麼,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方法的實作不是更好嗎?方法參考(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);

嗯!?仔細看看,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 運算式,也可以讓程式碼更為清楚。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter