方法與建構式參考
June 24, 2022當我們臨時想要為函式介面定義實作時,Lambda 運算式確實是很方便,然而有時候,你會發現某些靜態方法的本體實作流程,與你自行定義的 Lambda 運算式根本就是相同。
參考靜態方法
Java 考慮到這種狀況,Lambda 運算式只是定義函式介面實作的一種方式,除此之外,只要靜態方法的方法簽署中參數與傳回值定義相同,也可以使用靜態方法來定義函式介面實作。
舉例來說,在〈初試 Lambda〉曾定義過以下程式碼:
package cc.openhome;
public class StringOrder {
public static int byLength(String s1, String s2) {
return s1.length() - s2.length();
}
}
如果想要定義 Comparator<String>
的實作,必須實作其定義的 int compare(String s1, String s2)
方法,你可以使用 Lambda 運算式定義:
Comparator<String> lengthComparator = (s1, s2) -> s1.length() - s2.length();
然而仔細觀察,除了方法名稱之外,StringOrder
的靜態方法 byLength
之參數、傳回值,與 Comparator<String>
的 int compare(String s1, String s2)
之參數、傳回值都相同,你可以讓函式介面的實作參考 StringOrder
的靜態方法 byLength
:
Comparator<String> lengthComparator = StringOrder::byLength;
這樣的特性在JDK8中稱為方法參考(Method references),這可以讓你避免到處寫下 Lambda 運算式,儘量運用現有的API實作,也可以改善可讀性,在〈初試 Lambda〉就探討過,與其寫下 …
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, (name1, name2) -> name1.length() - name2.length());
不如寫下以下來得清楚:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, StringOrder::byLength);
參考實例方法
除了參考靜態方法作為函式介面實作之外,還可以參考特定物件的實例方法。例如,假設你正在設計一個可以過濾職缺應徵者的軟體,而你有以下類別:
public class JobVacancy {
...
public int bySeniority(JobApplicant ja1, JobApplicant ja2) {
...
}
}
若如下撰寫Lambda演算式來進行應徵者的排序:
JobVacancy vacancy = createJobVacancy(...);
JobApplicant[] applicants = retrieveApplicants(...);
Arrays.sort(applicants, (ja1, ja2) -> vacancy.bySeniority(ja1, ja2));
Lambda 運算式捕捉了 vacancy
參考的物件,實際上,bySeniority
方法的簽署與 Comparator<JobApplicant>
的 compare
方法相同,此時,我們可以直接參考 vacancy
物件的 bySeniority
方法:
Arrays.sort(applicants, vacancy::bySeniority);
再來看個例子,有些物件具有 forEach
方法,可以迭代物件進行指定處理:
List<String> names = Arrays.asList("Justin", "Monica", "Irene");
names.forEach(name -> out.println(name));
new HashSet(names).forEach(name -> out.println(name));
new ArrayDeque(names).forEach(name -> out.println(name));
發現了嗎?寫了三個重複的 Lambda 運算式,,你可以直接參考 out
的 println
方法:
List<String> names = Arrays.asList("Justin", "Monica", "Irene");
names.forEach(out::println);
new HashSet(names).forEach(out::println);
new ArrayDeque(names).forEach(out::println);
函式介面實作也可以參考類別上定義的非靜態方法,函式介面會試圖用第一個參數方法接收者,而之後的參數依序作為被參考的非靜態方法之參數。舉例而言:
Comparator<String> naturalOrder = String::compareTo;
雖然 Comparator<String>
的 int compare(String s1, String s2)
方法必須有兩個參數,然而在以上的方法參考中,會試圖用第一個參數 s1
作為 compareTo
的方法接收者,而之後的參數只剩下 s2
,剛好作為 s1.compareTo(s2)
,實際的應用在〈初試 Lambda〉也看過:
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, String::compareTo);
...
Arrays.sort(names, String::compareToIgnoreCase);
參考建構式
Java 還提供了建構式參考(Constructor references),用來重用現有 API 的物件建構流程。你也許會發出疑問:「建構式?他們有傳回值型態嗎?」語法上不需要,但事實上有!其實每個建構式都會有傳回值型態,也就是定義他們的類別本身。例如,如果你有個介面如下定義:
package cc.openhome;
public interface Function {
R apply(P p);
}
如果你使用〈定義與使用泛型〉的 ArrayList
,定義了一個 map
方法,可以將一個 List
的實例轉換為另一個型態的實例:
static <X, Y> ArrayList<Y> map(ArrayList<X> list, Function<X, Y> mapper) {
ArrayList<Y> mappedList = new ArrayList<>();
for(int i = 0; i < list.size(); i++) {
mappedList.add(mapper.apply(list.get(i)));
}
return mappedList;
}
你也許會這麼使用這個 map
方法:
ArrayList<String> names = new ArrayList<>();
...
ArrayList<Person> persons = map(names, name -> new Person(name));
實際上,你不過是將 name
用來呼叫 Person
的建構式,那麼不如直接參考 Person
的建構式:
ArrayList<String> names = new ArrayList<>();
...
ArrayList<Person> persons = map(names, Person::new);
如果某類別有多個建構式,就會使用函式介面的方法簽署來比對,找出對應的建構式進行呼叫。