認識 Lambda/Closure(8)方法參考與建構式參考 by caterpillar | CodeData
top

認識 Lambda/Closure(8)方法參考與建構式參考

分享:

認識 Lambda/Closure(7)JDK8 Lambda 語法 << 前情

English

根據名稱的長度進行排序,可以如下撰寫程式:

List<String> names = Arrays.asList("Justin", "Monica", "Irene", "caterpillar");
Collections.sort(names, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

如果你單只是看 compare 的方法本體實作,並不容易看出程式碼要做些什麼。你也許還會有其他的排序策略,因此,你在 StringOrder 類別中,定義了幾個 static 方法:

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);
    }
    ...
}

現在,你可以將先前的程式碼改寫為以下:

Collections.sort(names, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return StringOrder.byLength(s1, s2);
    }
});

程式打算做些什麼,現在看來是清楚多了。使用 JDK8 Lambda 的話,可以讓這個程式碼變得更清楚些。

Collections.sort(names, (s1, s2) -> NameOrder.byLength(s1, s2));

也許有個聰明的傢伙發現了,除了方法名稱之外,byLength 方法的簽署與 Comparatorcompare 方法相同。我們知道,Lambda 運算式是匿名方法(函式),而 Lambda 運算式的本體部份就是函式介面(Functional interface)的方法實作。因為我們只是把參數 s1s2 傳給 byLength 方法,那麼可以直接重用 byLength 方法的實作不是更好嗎?是的,JDK8 提供了方法可參考的特性,可以達到這個目的:

Collections.sort(names, NameOrder::byLength);

在 Java 中引人 Lambda 的同時,與現有 API 維持相容性是主要考量之一。除了採用函式介面之外,方法參數(Method reference)在重用現有 API 上也扮演了重要的角色。重用現有的方法實作,可避免到處寫下 Lambda 運算式。上面的例子是運用了方法參考中的一種形式 – 參考了 static 方法。你也可以參考至特定型態的任意物件之實例方法。例如,按照字典順序對名稱清單進行排序,原本可以如下撰寫:

Collections.sort(names, NameOrder::byLexicography);

從先前的段落說明中,我們知道 NameOrder::byLexicography 會參考到 byLexicography 方法實作,而以下的程式碼也有相同的排序效果:

Collections.sort(names, (s1, s2) -> s1.compareTo(s2));

我們可以發現到,在 Lambda 運算式的本體部份,第一個參數 s1 會是 compareTo 的接受者,而第二個參數 s2 則是 compareTo 方法的引數,在這種情況下,其實我們可以直接參考 String 類別的 compareTo 方法,像是:

Collections.sort(names, String::compareTo);

類似地,想對名稱清單按照字典順序排序,但忽略大小寫差異,本來可以如下參考 static 方法來達到:

Collections.sort(names, NameOrder::byLexicographyIgnoreCase);

再次地,在 byLexicographyIgnoreCase 的方法實作中,第一個參數是 compareToIgnoreCase 方法的接受者,而第二個參數是 compareToIgnoreCase 方法的引數,此時,我們可以直接參考 String 類別的 compareToIgnoreCase 方法。

Collections.sort(names, String::compareToIgnoreCase);

可輕易觀察到,方法參考不僅避免了重複撰寫 Lambda 運算式,也可以讓程式碼更為清楚。除了以下兩種方法參考形式外,我們還可以參考特定物件的實例方法。例如,假設你正在設計一個可以過濾職缺應徵者的軟體,而你有以下兩個類別:

public class JobVacancy {
    ...
    public int bySeniority(JobApplicant ja1, JobApplicant ja2) {
        ...
    }

    public int byEducation(JobApplicant ja1, JobApplicant ja2) {
        ...
    }
    ...
}
public class JobApplicant {
    ...
}

如果你使用 JDK8,並如下撰寫 Lambda 演算式來進行應徵者的排序:

List<JobApplicant> applicants = ...;
JobVacancy vacancy = ...;
Collections.sort(applicants, (ja1, ja2) -> vacancy.bySeniority(ja1, ja2));

Lambda 運算式捕捉了 vacancy 參考的物件。bySeniority 方法的簽署與 Comparatorcompare 方法相同,此時,我們可以直接參考 vacancy 物件的 bySeniority 方法。

Collections.sort(applicants, vacancy::bySeniority);

除了方法參考之外,JDK8 還提供了建構式參考(Constructor references)。你也許會發出疑問:「建構式?他們有傳回值型態嗎?」有的!其實每個建構式都會有傳回值型態 – 也就是定義他們的類別本身。例如,若你有以下的介面:

public interface Part {
    ...
}
public interface Material {
    ...
}
public interface PartFactory {
    Part createPart(Material material);
}

你為這些介面撰寫了一些實作:

public class PartImpl implements Part {
    public PartImpl(Material material) {
        ...
    }
}
public class MaterialImpl implements Material {
    ...
}
public PartFactoryImpl implements PartFactory {
    public Part createPart(Material material) {
        return new PartImpl(material);
    }
}

接著,你可能使用以下的程式碼來建立 Part 實例:

PartFactory factory = new PartFactoryImpl();
Part part = factory.createPart(new MaterialImpl());

createPart 方法的實作中,只是使用建構式來建立了 Part 的實例。使用 JDK8 的話,你就不用特別花時間定義 PartFactoryImpl 類別,你可以直接參考 PartImpl 的建構式。

PartFactory factory = PartImpl::new;
Part part = factory.createPart(new MaterialImpl());

如果某類別有多個建構式,就會使用函式介面的方法簽署來比對,找出對應的建構式進行呼叫。

終於,〈認識 Lambda/Closure〉要告一段落了。「等一下!怎麼沒討論預設方法(Default method)?那不是 Lambda 專案的一部份嗎?」

是的,預設方法確實是 Lambda 專案的一部份,不過他跟將現在的 API 演化有關。預設方法解除了介面上的一些限制,讓 Java 介面在進行防禦式(Defensive)的 API 演化時容易一些,並為流程的重用開啟了更多可能性,不過,也帶入多重繼承上的一些複雜度。在討論如何將現在的 API 演化的時候,我們也許會看到一些函數式程式設計(Functional programming)的影子。我想,用一個新的系列來討論這些有趣的主題,會是比較好的做法,所以這些會留到下一個系列〈Java 開發者的函數式程式設計〉中來討論。敬請期待!

分享:
按讚!加入 CodeData Facebook 粉絲群

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

熱門論壇文章

熱門技術文章