Lambda
在介紹 Lambda 表達式之前乒融,我們先來看只有單個方法的 Interface(通常我們稱之為回調接口):
public interface OnClickListener {
void onClick(View v);
}
我們是這樣使用它的:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
v.setText("lalala");
}
});
這種回調模式在 Android 中非常流行,但是像上面這樣的匿名內部類并不是一個好的選擇赞季,因為:
- 語法冗余
- 匿名內部類中的 this 指針和變量容易產生誤解
- 無法捕獲非 final 局部變量
- 非靜態(tài)內部類默認持有外部類的引用奢驯,部分情況下會導致外部類無法被 GC 回收,導致內存泄露
而 Java 8 中引入的 Lambda 表達式就解決了以上的問題撒遣,先來看一個簡單的 Lambda 用法:
//多行注釋中為不使用Lambda的寫法
/**
Runnable run = new Runnable() {
@Override
public void run() {
System.out.println("Test")
}
};
*/
Runnable run = () -> System.out.println("Test");
我們來看一下Runnable接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
函數(shù)式接口
Java 8 引入了 FunctionalInterface
注解來表明一個接口打算成為一個函數(shù)式接口。
函數(shù)式接口: 只定義了一個抽象方法的接口稱之為函數(shù)式接口
在實際使用中不管 FunctionalInterface
注解是否存在禾进,Java 編譯器都會將所有滿足該定義的接口看作是函數(shù)式接口廉涕。
下面我們自己寫一個接口看一下能否如Runnable一般:
public interface TestLambdaInterface {
void testInterface();
}
我們如下使用該接口狐蜕,程序能正常編譯并輸出了結果,說明 Java 編譯器都會將所有滿足該定義的接口看作是函數(shù)式接口:
TestLambdaInterface test = ()->System.out.println("Test");
test.testInterface();
當我們將方法的返回值改成String時婆瓜,程序也能正常編譯并輸出了結果:
TestLambdaInterface test = ()->"test";
test.testInterface();
說明函數(shù)式接口與接口中定義方法的返回值無關
下面我們在接口中增加一個方法
public interface TestLambdaInterface {
void testInterface();
void testInterface2();
}
程序不能正常通過編譯
D:\Work\LearnJavaFX\src>javac -encoding utf-8 LambdaTest.java
LambdaTest.java:20: 錯誤: 不兼容的類型: TestLambdaInterface 不是函數(shù)接口
TestLambdaInterface test = () -> System.out.println("Test");
^
在 接口 TestLambdaInterface 中找到多個非覆蓋抽象方法
1 個錯誤
說明函數(shù)式接口只能有一個抽象方法
我們再來看看Java中提供的BinaryOperator接口:
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
其實現(xiàn)的接口 BiFunction 的代碼如下:
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t, U u) -> after.apply(apply(t, u));
}
}
該接口可以使用如下方法生成實例:
BinaryOperator<Long> add = (x, y) -> x + y;
說明函數(shù)式接口與接口中是否存在 default 與 static 方法無關
以上充分說明能成為函數(shù)式接口【可以使用 Lambda 來簡化接口操作】的條件為:只定義了一個抽象方法的接口勃救。
Lambda 書寫方式
/**
* 對于只有一個抽象方法且返回值為void可以使用下面的方法快捷的生成接口的實例
*/
Runnable voidFunc = () -> System.out.println("沒有參數(shù)的函數(shù)式接口");
/**
* 上面的聲明其實完整寫法應該為:
* Runnable voidFunc = () -> {System.out.println("沒有參數(shù)的函數(shù)式接口")};
* 對于只有一行語句時治力,可以省略{}宵统,
* 下面是多行語句的情況,{}不能省略
*/
Runnable muitiStatement = () -> {
System.out.println("第一行");
System.out.println("第二行");
//多行語句....
};
/**
* 對于有接口中有參數(shù)哦的方法我們可以使用
* ActionListener oneParam = (event) -> System.out.println("一個參數(shù)");
* 當只有一個參數(shù)時瓢省,可以省略()如下:
*/
ActionListener oneParam = event -> System.out.println("一個參數(shù)");
/**
* 多參數(shù)就必須將參數(shù)放入到()中調用痊班,按照參數(shù)順序寫入
*/
BinaryOperator<Long> add = (x, y) -> x + y;
/**
* 上面幾種情況中,接口的參數(shù)在編譯時進行類型的推斷馒胆,我們亦可以顯式聲明一下【這樣可以避免很多錯誤】
*/
BinaryOperator<Long> add2 = (Long x, Long y) -> x + y;
Lambda 表達式語法由參數(shù)列表
凝果、->
和函數(shù)體
組成。函數(shù)體既可以是一個表達式也可以是一個代碼塊型雳。
-
表達式:表達式會被執(zhí)行然后返回結果。它簡化掉了
return
關鍵字 - 代碼塊:顧名思義就是一坨代碼沿量,和普通方法中的語句一樣
目標類型
通過前面的例子我們可以看到冤荆,lambda 表達式?jīng)]有名字,那我們怎么知道它的類型呢佛掖?答案是通過上下文推導而來的涌庭。例如,下面的表達式的類型是 OnClickListener
ClickListener listener = (View v) -> {v.setText("lalala");};
這就意味著同樣的lambda表達式在不同的上下文里有不同的類型
Runnable runnable = () -> doSomething(); //這個表達式是 Runnable 類型的
Callback callback = () -> doSomething(); //這個表達式是 Callback 類型的
編譯器利用 lambda 表達式所在的上下文所期待的類型來推導表達式的類型拴魄,這個被期待的類型被稱為目標類型席镀。lambda 表達式只能出現(xiàn)在目標類型為函數(shù)式接口的上下文中。
Lambda 表達式的類型和目標類型的方法簽名必須一致顶捷,編譯器會對此做檢查屎篱,一個 lambda 表達式要想賦值給目標類型 T
則必須滿足下面所有的條件:
-
T
是一個函數(shù)式接口 - lambda 表達式的參數(shù)必須和
T
的方法參數(shù)在數(shù)量交播、類型和順序上一致(一一對應) - lambda 表達式的返回值必須和
T
的方法的返回值一致或者是它的子類 - lambda 表達式拋出的異常和
T
的方法的異常一致或者是它的子類
由于目標類型是知道 lambda 表達式的參數(shù)類型,所以我們沒必要把已知的類型重復一遍秦士。也就是說 lambda 表達式的參數(shù)類型可以從目標類型獲人硗痢:
//編譯器可以推導出s1和s2是String類型
Comparator<String> c = (s1, s2) -> s1.compareTo(s2);
button.setOnClickListener(v -> v.setText("lalala"));
作用域
在內部類中使用變量名和this非常容易出錯。內部類通過繼承得到的成員變量(包括來說 object 的)可能會把外部類的成員變量覆蓋掉关贵,未做限制的 this 引用會指向內部類自己而非外部類卖毁。
而 lambda 表達式的語義就十分簡單:它不會從父類中繼承任何變量,也不用引入新的作用域炭剪。lambda 表達式的參數(shù)及函數(shù)體里面的變量和它外部環(huán)境的變量具有相同的語義(this 關鍵字也是一樣)翔脱。
public class HelloLambda {
Runnable r1 = () -> System.out.println(this);
Runnable r2 = () -> System.out.println(toString());
@Override
public String toString() {
return "Hello, lambda!";
}
public static void main(String[] args) {
new HelloLambda().r1.run();
new HelloLambda().r2.run();
}
}
上面的代碼最終會打印兩個 Hello, lambda!
,與之相類似的內部類則會打印出類似 HelloLambda$1@32a890
和 HelloLambda$1@6b32098
這種出乎意料的字符串错妖。
基于詞法作用域的理念疚沐,lambda表達式不可以掩蓋任何其所在上下文的局部變量。
變量
在 JDK8 對匿名內部類中調用類之外變量必須為final的限制進行了一定的放寬:
String name = "123";
Runnable run = new Runnable() {
@Override
public void run() {
//在JDK8之前下面的語句是會報錯的痴施,只有name是final時才可以使用究流,但是JDK8中,這樣的語句就不會報錯
System.out.println(name);
}
};
雖然 JDK8 中上面的語句時可以的神得,但這只是減少了我們代碼中的編寫偷仿,編譯時還是會將 name 當做是 final 所以當有以下幾種情況時炎疆,編譯還是會報錯:
//1.對name進行了第二次賦值,這樣編譯器就會認為name不是final類型
String name = "123";
name="";
Runnable run = () -> {
System.out.println(name);
};
//2.在匿名類方法中對變量進行賦值也是不允許的
String name = "123";
Runnable run = () -> {
name="";
System.out.println(name);
};
如果真的需要在Lambda中或者匿名中對變量賦值全跨,那么應該將其放入數(shù)組亿遂,或者當做一個類的屬性來傳遞對象final時可以設置其屬性,不能設置其引用