在 Java 8 Lambda 表示式中,很容易讓原本寫習慣 Java 匿名函式的開發者搞混,如下面程式碼所示,建立一個簡單的 Runnable。
Runnable runnable = new Runnable(){
public void run(){
System.out.print("hello");
}
};
new Thread(runnable);
這樣代表我們把一段任務塞入到 Thread,透過 Thread 去執行這一段 Task,一般來說很常見這樣的寫法, 普遍也很容易接受,但是改成Lambda表示式就會變成這樣。
Runnable runnable = () -> System.out.print("hello");
new Thread(runnable);
這樣還稍微可以接受,但是應該可以更精簡。
new Thread(()->System.out.print("hello"));
這樣就是一個標準的 Lambda 表示式,由於我們在宣告 Thread 這個類別的時候,其實已經將型態宣告出來了。
public class Thread implements Runnable {
public Thread(Runnable target){
//...
}
//...
}
因此再傳入的時候,不需要再宣告一次,編譯器自動會幫我判別,而 Runnable 只是一個 interface。
public interface Runnable {
public abstract void run();
}
使用Lambda表示先決條件,interface 必須只有一個 method,才能夠使用,Lambda 表示會變成下面的宣告。
唯一的方法傳入的參數 -> 回傳值
所以 Runnable 會變成下面的樣子。
()->System.out.print("hello");
你會發現沒有傳入的參數就會變成 ()
左右括弧來表示,而由於沒有回傳值, 因此我們用標準輸出一行呈現,這時候你就會問,那如果有多行輸出怎麼辦?
其實也不難,只要用左右大括弧{
}
包起來就可以了。
()->{
System.out.print("hello");
System.out.print("World");
}
那如果有參數怎麼寫呢?
在Android按鈕要給定事件,通常都會這樣寫。
button.setOnClickListener(new OnClickListener(){
public void onClick(Event e){
Toast.makeText(context, "hello", Toast.LENGTH_SHORT).show();
}
});
轉換成我們剛剛所推導的流程,這個事件可以變成這樣。
button.setOnClickListener(e->
Toast.makeText(context, "hello", Toast.LENGTH_SHORT).show());
如果是多行就會變成下面程式碼所示。
button.setOnClickListener(e-> {
Toast.makeText(context, "hello", Toast.LENGTH_SHORT).show();
Toast.makeText(context, "world", Toast.LENGTH_SHORT).show();
});
此時你會發現單一行呈現,不需要用;
分號做結尾,但是,多行則必須每一行都以;
做結尾。
那如果是有回傳參數的方法呢?
我們利用 Callable 來進行解說,Callable 跟 Runnable 一樣都是一個interface,唯一不同的是它有一個泛型的回傳值。
public interface Callable<V> {
V call() throws Exception;
}
所以再傳入的時候,我們必須指定泛型的型態,從以下例子來看,透過一個 ThreadPool 傳入一個 Task,並且透過 Callable 回傳結果。
Executors.newSingleThreadExecutor().submit(new Callable<String>(){
@Override
public String call() throws Exception {
String s = "hello";
return s;
}
});
這樣就可以很清楚的看到,當任務執行完畢,會將hello
這個字串回傳回去,那如果換成 Lambda 就可以這樣寫。
Executors.newSingleThreadExecutor().submit(()->"hello");
當只有一行敘述的時候,該行敘述會被視為回傳值,因此,如果你直接寫成下面的程式碼。
Executors.newSingleThreadExecutor().submit(()->1);
編譯器會判定你的T型態為整數,也就是說會變成以下的類型。
public interface Callable<Integer> {
Integer call() throws Exception;
}
假設你是以標準輸出呈現,如下面程式碼所示。
Executors.newSingleThreadExecutor().submit(()->System.out.print("hello"));
則編譯器會認為你 Callable<Void>
以 Void 的型態呈現,因此,泛型的好處是各種彈性回傳都會讓編譯器通過編譯,那假設有一個interface 是固定型態呢?
舉個例子來說,我們假定有一個介面用來檢查是否合身?
如下面程式碼所示。
private class Wear{
void wearClothes(Fit fit) {
}
}
public interface Fit{
boolean onFit();
}
那麼我們實作這個介面就可以照下面程式碼這樣寫。
new Wear().wearClothes(new Fit() {
@Override
public boolean onFit() {
return false;
}
});
轉換成 Lambda 表示式就會變成下面程式碼這樣。
new Wear().wearClothes(()->false);
此時,如果你將回傳值改成數值,就會變成下面程式碼這樣。
new Wear().wearClothes(()->1);
那麼編譯器就不會讓你過了,這樣就可以清楚看出泛型跟非泛型的差異了,泛型彈性也比較足夠,好處是同樣一個 interface 可以讓多種形態適用。
回到剛剛的例子,我們一值都是傳入 0 個參數或者 1 個參數,那如果是兩個參數怎麼辦?
我們把上面的例子改成以下狀況。
private class Wear{
void wearClothes(Fit fit) {
}
}
public interface Fit{
boolean onFit(int height, int waist);
}
覆寫完的結果就會變成下面的狀況。
new Wear().wearClothes(new Fit() {
@Override
public boolean onFit(int height, int waist) {
return false;
}
});
跟剛剛好像其實只是多了一個參數,那我們來看看 Lambda 怎麼表示?
new Wear().wearClothes((h, w) ->false);
變得非常的簡短,而且 h 跟 w 並不知道是甚麼樣的型態,這樣有可能會造成隊友短時間的眼睛痛,所以你可以補上一些資訊。
new Wear().wearClothes((int h, int w) ->false);
這邊要注意的是,如果你一個參數有補上型態,另外一個參數也要跟著補上,一種要補大家一起補的概念。
new Wear().wearClothes((int h, w) ->false);
這樣編譯器不會讓你過。
那一個參數可以補足型態嗎?當然可以。
假設 interface 改成下面這樣子。
private static class Wear{
void wearClothes(Fit fit) {
}
}
public interface Fit{
boolean onFit(int height);
}
Lambda 就可以表示成下面程式碼這樣。
new Wear().wearClothes((int h) ->false);
一定要左右括弧(``)
嗎? 沒錯! 一定要。
否則編譯器仍然不會讓你過。
大致上在 Lambda 語法簡化處理的部分都講完了,Java 8 提供了一些函式讓 Lambda 在操作上更容易辨認,只需要認定某些方法,就可以清楚知道傳入的參數跟回傳的參數是那些,以下列出常見的寫法。
函式介面 | 回傳類型 | 方法名稱 | 傳入參數 |
Predicate<T> | boolean | test | T |
Supplier<T> | T | get | void |
Comsumer<T> | void | accept | T |
Function <T, R> | R | apply | T |
這樣就是一個簡單的 Lambda 範例了。