Lambda 運算式與函式介面

June 24, 2022

在〈初試 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 運算式的目標型態,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);
}

使用匿名類別來實作這類介面不是不好,只不過有其應用的場合,只不過在許多時候,特別是介面只有一個方法要實作時,你會只想關心參數及實作本體,不想理會類別與方法名稱,像是〈初試 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);

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

@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

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