方法與建構式參考

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 運算式,,你可以直接參考 outprintln 方法:

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

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

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