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 運算式表示接受兩個參數 name1
、name2
,參數都是 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>
推斷出 name1
與 name2
的型態,實際上是 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 的 Runnable
、Callable
、Comparator
等。
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