方法與建構式參考


當我們臨時想要為函式介面定義實作時,Lambda表示式確實是很方便,然而有時候,你會發現某些靜態方法的本體實作流程,與你自行定義的Lambda表示式根本就是相同,JDK8考慮到這種狀況,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) {
        ...
    }
}

如果你使用JDK8,並如下撰寫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);


另一個更簡單的例子可以在 Iterable 與 Iterator 中看到,JDK8在Iterable上新增了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);

方法參考用來重用現有API的方法流程,而JDK8還提供了建構式參考(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);

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