在 匿名類別與 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 表示式的目標型態,JDK8的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);
}
在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);
}
編譯器會對此介面產生以下編譯錯誤:
^
Function is not a functional interface
multiple non-overriding abstract methods found in interface Function
1 error
Lambda語法不過就是匿名類別的編譯器語法蜜糖嘛!?接下來會探討更多細節,你就知道不是了!