問題描述
Variable used in lambda expression should be final or effectively final
我們在使用Java8 lambda表達式的時候時不時會遇到這樣的編譯報錯
這句話的意思是,lambda 表達式中使用的變量應該是 final 或者有效的 final
,為什么會有這種規定?
匿名類中的局部變量
其實在 Java 8 之前,匿名類中如果要訪問局部變量的話,那個局部變量必須顯式的聲明為 final
,如下代碼在 Java 7 中是編譯不過的:
@Test
public void demo() {
String version = "1.8";
foo(new Supplier() {
@Override
public String get() {
return version; // 編譯報錯 Variable 'version' is accessed from within inner class, needs to be declared final
}
});
}
private void foo(Supplier supplier) {
System.out.println(supplier.get());
}
Java 7 要求 version 這個局部變量必須是 final 類型的,否則在匿名類中不可引用。
我們知道,lambda 表達式是由匿名內部類演變過來的,它們的作用都是實現接口方法,於是類比匿名內部類,lambda 表達式中使用的變量也需要是 final 類型。也就是說我們一開始圖片中,i 這個變量需要聲明為 final 類型,但是又發現個現象,如下代碼
for (int i = 0; i < 5; i++) {
int finalI = i;
list.forEach(item -> {
System.out.println(finalI);
});
}
i 這個變量賦值給了 finalI 變量,但是 finalI 並沒有聲明為 final 類型,代碼卻能夠編譯通過,這是因為 Java 8 之后,在匿名類或 Lambda 表達式中訪問的局部變量,如果不是 final 類型的話,編譯器自動加上 final 修飾符,即 Java8 新特性:effectively final
思考
前面一直說 Lambda 表達式或者匿名內部類不能訪問非 final 的局部變量,這是為什么呢?
- 首先思考
外部的局部變量 finalI 和匿名內部類里面的 finalI 是否是同一個變量?
我們知道,每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接,方法出口等信息,每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程
就是說在執行方法的時候,局部變量會保存在棧中,方法結束局部變量也會出棧,隨后會被垃圾回收掉,而此時,內部類對象可能還存在,如果內部類對象這時直接去訪問局部變量的話就會出問題,因為外部局部變量已經被回收了,解決辦法就是把匿名內部類要訪問的局部變量復制一份作為內部類對象的成員變量,查閱資料或者通過反編譯工具對代碼進行反編譯會發現,底層確實定義了一個新的變量,通過內部類構造函數將外部變量復制給內部類變量。
- 為何
還需要用final修飾?
其實復制變量的方式會造成一個數據不一致的問題,在執行方法的時候局部變量的值改變了卻無法通知匿名內部類的變量
,隨着程序的運行,就會導致程序運行的結果與預期不同,於是使用final修飾這個變量,使它成為一個常量,這樣就保證了數據的一致性
- lambda中可以使用原子類
因為原子類變量是線程安全的,保證每個線程操作的都是最新值,說到底還是滿足數據一致性