根據名稱的長度進行排序,可以如下撰寫程式:
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
方法的簽署與 Comparator
的 compare
方法相同。我們知道,Lambda 運算式是匿名方法(函式),而 Lambda 運算式的本體部份就是函式介面(Functional interface)的方法實作。因為我們只是把參數 s1
與 s2
傳給 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
方法的簽署與 Comparator
的 compare
方法相同,此時,我們可以直接參考 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 開發者的函數式程式設計〉中來討論。