寫在開頭
Lambda ,希臘字母 “λ” 的英文名稱。沒錯,就是你高中數學老師口中的那個“蘭布達”。在編程世界中,它是匿名函數的別名, Java 從 Java 8 開始引入 lambda 表達式。而 Android 開發者的世界里,直到 Android Studio 2.4 Preview 4 及其之后的版本里,lambda 表達式才得到完全的支持(在此之前需要使用 Jack 編譯器或 retrolambda 等插件,詳見鏈接)。新版本 Android Studio 使用向導詳見 《在 Android Studio 上使用 Java 8 新特性》。
Oracle 官方推出的 lambda 教程開篇第一句就表揚了其對匿名內部類笨拙繁瑣的代碼的簡化,然而,在各大 RxJava 教程下的評論中,最受吐槽的就是作者提供的示例代碼用了 lambda 表達式,給閱讀造成了很大的障礙。
所以,在這篇文章中,我會先講解 lambda 表達式的作用和三種形式,之后提供一個在 Android Studio 便捷使用 lambda 的小技巧,然后說一說 lambda 表達式中比較重要的變量捕獲概念,最后再講一些使用 lambda 表達式前后的差異。
作用
前面提到,lambda 是匿名函數的別名。簡單來說,lambda 表達式是對匿名內部類的進一步簡化。使用 lambda 表達式的前提是編譯器可以準確的判斷出你需要哪一個匿名內部類的哪一個方法。
我們最經常接觸使用匿名內部類的行為是為 view 設置 OnClickListener ,這時你的代碼是這樣的:
button.setOnClickListener(new View.OnClickListener(){
@Override public void onClick(View v){
doSomeWork();
}
});
使用匿名內部類,實現了對象名的隱匿;而匿名函數,則是對方法名的隱匿。所以當使用 lambda 表達式實現上述代碼時,是這樣的:
button.setOnClickListener(
(View v) -> {
doSomeWork();
}
);
看不懂?沒關系,在這兩個示例中,你只要理解,lambda 表達式不僅對對象名進行隱匿,更完成了方法名的隱匿,展示了一個接口抽象方法最有價值的兩點:參數列表和具體實現。下面我會對 lambda 的各種形式進行列舉。
形式
在 Java 中,lambda 表達式共有三種形式:函數式接口、方法引用和構造器引用。其中,函數式接口形式是最基本的 lambda 形式,其余兩種形式都是基于此形式進行拓展。
PS:為了更好的展示使用 lambda 表達式前后的代碼區別,本文將使用 lambda 表達式給引用賦值的形式作為實例展示,而不是常用的直接將 lambda 表達式傳入方法之中。同時,舉例也不一定具有實際意義。
函數式接口
函數式接口是指有且只有一個抽象方法的接口,比如各種 Listener 接口和 Runnable 接口。lambda 表達式就是對這類接口的匿名內部類進行簡化。基本形式如下:
( 參數列表... ) -> { 語句塊... }
下面以 Java 提供的 Comparator 接口來展示一個實例,該接口常用于排序比較:
interface Comparator<T> {int compare(T var1, T var2);}
Comparator<String> comparator = new Comparator<String> (){
@Override public int compare(String s1, String s2) {
doSomeWork();
return result;
}
};
Comparator<String> comparator = (String s1, String s2) -> {
doSomeWork();
return result;
};
當編譯器可以推導出具體的參數類型時,我們可以從參數列表中忽略參數類型,那么上面的代碼就變成了:
Comparator<String> comparator = ( s1 , s2 ) -> {
doSomeWork();
return result;
};
當參數只有一個時,參數列表兩側的圓括號也可省略,比如 OnClickListener 接口可寫成 :
interface OnClickListener { void onClick(View v); }
OnClickListener listener = v -> { 語句塊... } ;
然而,當方法沒有傳入參數的時候,則記得提供一對空括號假裝自己是參數列表(霧),比如 Runnable 接口:
interface Runnable { void run(); }
Runnable runnable = () -> { 語句塊... } ;
當語句塊內的處理邏輯只有一句表達式時,其兩側的花括號也可省略,特別注意這句處理邏輯表達式后面也不帶分號。比如這個關閉 activity 的點擊方法:
button.setOnClickListener( v -> activity.finish() );
同時,當只有一句去除花括號的表達式且接口方法需要返回值時,這個表達式不用(也不能)在表達式前加 return ,就可以當作返回語句。下面用 Java 的 Function 接口作為示例,這是一個用于轉換類型的接口,在這里我們獲取一個 User 對象的姓名字符串并返回:
interface Function <T, R> { R apply(T t); }
Function <User, String> function = new Function <User, String>(){
@Override public String apply(User user) {
return user.getName();
}
};
Function <User, String> function = user -> user.getName() ;
方法引用
在介紹第一種形式的之前,我曾寫道:函數式接口形式是最基本的 lambda 表達式形式,其余形式都是由其拓展而來。那么,現在來介紹第二種形式:方法引用形式。
當我們使用第一種 lambda 表達式的時候,進行邏輯實現的時候我們既可以自己實現一系列處理,也可以直接調用已經存在的方法,下面以 Java 的 Predicate 接口作為示例,此接口用來實現判斷功能,我們來對字符串進行全面的判空操作:
interface Predicate<T> { boolean test(T t); }
Predicate<String> predicate=
s -> {
//用基本代碼組合進行判斷
return s==null || s.length()==0 ;
};
我們知道,TextUtils 的 isEmpty() 方法實現了上述功能,所以我們可以寫作:
Predicate<String> predicate = s -> TextUtils.isEmpty(s) ;
這時我們調用了已存在的方法來進行邏輯判斷,我們就可以使用方法引用的形式繼續簡化這一段 lambda 表達式:
Predicate<String> predicate = TextUtils::isEmpty ;
驚不驚喜?意不意外?
方法引用形式就是當邏輯實現只有一句且調用了已存在的方法進行處理( this 和 super 的方法也可包括在內)時,對函數式接口形式的 lambda 表達式進行進一步的簡化。傳入引用方法的參數就是原接口方法的參數。
接下來總結一下方法引用形式的三種格式:
object :: instanceMethod
直接調用任意對象的實例方法,如 obj::equals 代表調用 obj 的 equals 方法與接口方法參數比較是否相等,效果等同obj.equals(t);
。
當前類的方法可用this::method
進行調用,父類方法同理。
ClassName :: staticMethod
直接調用某類的靜態方法,并將接口方法參數傳入,如上述TextUtils::isEmpty
,效果等同TextUtils.isEmpty(s);
ClassName :: instanceMethod
較為特殊,將接口方法參數列表的第一個參數作為方法調用者,其余參數作為方法參數。由于此類接口較少,故選擇 Java 提供的 BiFunction 接口作為示例,該接口方法接收一個 T1 類對象和一個 T2 類對象,通過處理后返回 R 類對象:
interface BiFunction<T1, T2, R> {
R apply(T1 t1, T2 t2);
}
BiFunction<String,String,Boolean> biFunction=
new BiFunction<String, String, Boolean>() {
@Override public Boolean apply(String s1, String s2){
return s1.equals(s2);
}
};
// ClassName 為接口方法的第一個參數的類名,同時利用接口方法的第一個參數作為方法調用者,其余參數作為方法參數,實現 s1.equals(s2);
BiFunction<String,String,Boolean> biFunction= String::equals;
構造器引用
Lambda 表達式的第三種形式,其實和方法引用十分相似,只不過方法名替換為 new 。其格式為 ClassName :: new
。這時編譯器會通過上下文判斷傳入的參數的類型、順序、數量等,來調用適合的構造器,返回對象。
使用技巧
Android Studio 會在可以轉化為 lambda 表達式的代碼上進行如圖的灰色標識,這時將光標移至灰色區域,按下 Alt + Enter ,選擇第一項(方法引用和構造器引用在第二項),IDE 就會自動進行轉換。
變量捕獲
在使用匿名內部類時,若要在內部類中使用外部變量,則需要將此變量定義為 final 變量。因為我們并不知道所實現的接口方法何時會被調用,所以通過設立 final 來確保安全。在 lambda 表達式中,仍然需要遵守這個標準。
不過在 Java 8 中,新增了一個 effective final 功能,只要一個變量沒有被修改過引用(基本變量則不能更改變量值),即為實質上的 final 變量,那么不用再在聲明變量時加上 final 修飾符。接下來還是通過一個示例解釋,示例中共有三句被注釋掉的賦值語句,去除任意一句的注釋,都會報錯:Variable used in lambda expression should be final or effectively final。
int effectiveFinalInt=666;//外部變量
//①effectiveFinalInt=233;
button.setOnClickListener(v -> {
Toast.makeText( effectiveFinalInt + "").show();
//②effectiveFinalInt=233;
});
//③effectiveFinalInt=233;
可以看到,我們可以不做任何聲明上的改變即可在 lambda 中使用外部變量,前提是我們以 final 的規則對待這個變量。
一點玄學
this 關鍵字
在匿名內部類中,this 關鍵字指向的是匿名類本身的對象,而在 lambda 中,this 指向的是 lambda 表達式的外部類。
方法數差異
當前 Android Studio 對 Java 8 新特性編譯時采用脫糖(desugar)處理,lambda 表達式經過編譯器編譯后,每一個 lambda 表達式都會增加 1~2 個方法數。而 Android 應用的方法數不能超過 65536 個。雖然一般應用較難觸發,但仍需注意。
參考資料
書籍:《 Java 核心技術 》
網絡文章:
在 Android Studio 上使用 Java 8 新特性(官方)
Oracle 官方 lambda 教程
匿名函數--維基百科(需科學上網)
深入淺出 Java 8 Lambda 表達式