Lambda 表示式與函式介面


匿名類別與 Lambda 中,你已經看過Lambda的幾個應用範例,接下來得瞭解一些細節了。首先,你得知道以下的程式碼:

Comparator<String> byLength = (String name1, String name2) -> name1.length() - name2.length();

可以拆開為兩部份,等號右邊是Lambda表示式(Expression),等號左邊是作為Lambda表示式的目標型態(Target type)。先來看看Lambda表示式:

(String name1, String name2) -> name1.length() - name2.length()

這個Lambda表示式表示接受兩個參數name1name2,參數都是String型態,目前->右邊定義了會傳回結果的單一運算式,如果運算比較複雜,必須使用多行陳述,可以加入{}定義陳述區塊,如果有傳回值,必須加上return,例如:

(String name1, String name2) -> {
    String n1 = name1.trim();
    String n2 = name2.trim();
    ...
    return n1.length() - n2.length();   
}

區塊可以由數個陳述句組成,不過基本上不建議如此使用。在運用Lambda時,儘量使用簡單的運算式會是比較好的,如果你的實作比較複雜,可以考慮方法參數等其他方式。如果不接受任何參數,也必須寫下括號。例如:

() -> "Justin" // 不接受參數,傳回字串
() -> System.out.println() // 不接受參數,沒有傳回值

單只有Lambda表示式的情況下,參數的型態必須寫出來,如果有目標型態的話,在編譯器可推斷出類型的情況下,就可以不寫出Lambda表示式的參數型態。例如以下範例可以從Comparator<String>中推斷出name1name2的型態,實際上是String,因而就不用寫出參數型態:

Comparator<String> byLength = (name1, name2) -> name1.length() - name2.length();

Lambda表示式本身是中性的,不代表任何一種物件,同樣的Lambda表示式,可用來表示不同目標型態的物件實作,舉例而言,(name1, name2) -> name1.length() - name2.length()在上面的範例中,用來表示Comparator<String>實例,如果你定義了一個介面:

public interface Func<P, R> {
    R apply(P p1, P p2);
}

那麼同樣是(name1, name2) -> name1.length() - name2.length(),在以下的範例中:

Func<String, Integer> func = (name1, name2) -> name1.length() - name2.length();

就用來表示目標型態為Func<String, Integer>的物件實作,這個例子也示範了如何定義 Lambda 表示式的目標型態,JDK8的Lambda並沒有導入新的型態來作為Lambda的實際型態,而是就現有的interface語法來定義函式介面(Functional interface),作為Lambda表示式的目標型態。函式介面就是介面,但要求僅具單一抽象方法,許多現存的介面都是這種介面,像是標準API中的RunnableCallableComparator等。

public interface Runnable {
    void run();
}
 
public interface Callable<V> {
    V call() throws Exception;
}
 
public interface Comparator<T> {
    int compare(T o1, T o2);
}

在JDK8之前,你可以使用匿名類別來實作這類介面,匿名類別不是不好,只不過有其應用的場合,只不過在許多時候,特別是介面只有一個方法要實作時,你會只想關心參數及實作本體,不想理會類別與方法名稱,像是 匿名類別與 Lambda 中的以匿名類別實作的例子:

Arrays.sort(names, new Comparator<String>() {
    public int compare(String name1, String name2) {
        return name1.length() - name2.length();
    }
});

實際上,你關心的只是怎麼比較兩個元素,這類情況下,使用Lambda會讓你能專心一點:

Arrays.sort(names, (name1, name2) -> name1.length() - name2.length());

所以,Lambda表示式只關心方法簽署上的參數與回傳定義,但忽略方法名稱。如果函式介面上定義的方法只接受一個參數,例如:

public interface Func {
    public void apply(String s);
}

你在撰寫Lambda運算式時,若編譯器可推斷出型態,本來可以寫為:

Func f = (s) -> System.out.println(s);

這時括號就是多餘的了,可以省略寫為:

Func f = s -> System.out.println(s);

函式介面是僅具單一抽象方法的介面,不過在JDK8中有時會難以直接分辨介面是否為函式介面,因為JDK8對interface語法做了演進,允許有預設方法(Default method)實作(之前會再介紹),而介面可能繼承其他介面、重新定義了某些方法等,這些都會使得確認介面是否為函式介面更為困難。有個新標註@FunctionalInterface在JDK8中被引入,它可以這麼使用:

@FunctionalInterface
public interface Func<P, R> {
    R apply(P p);
}

如果介面使用了@FunctinalInterface來標註,而本身並非函式介面的話,就會引發編譯錯誤。例如以下這個範例:

@FunctionalInterface
public interface Function<P, R> {
    R call(P p);
    R call(P p1, P p2);
}

編譯器會對此介面產生以下編譯錯誤:

@FunctionalInterface
^
  Function is not a functional interface
    multiple non-overriding abstract methods found in interface Function
1 error

Lambda語法不過就是匿名類別的編譯器語法蜜糖嘛!?接下來會探討更多細節,你就知道不是了!