代碼示例
public static void main(String[] args) {
int value = 0;
IntStream.range(0, 10).forEach(i -> value++);
System.out.println(value);
}
這段代碼中第三行的 value 會出現以下的錯誤提示:
Variable used in lambda expression should be final or effectively final
原因分析
首先,我們明確以下幾點內容:
value是一個局部變量。forEach(i -> value++)中的value++屬於在 lambda 表達式中修改局部變量。- 我們可以把 lambda 表達式看作是一個匿名內部類實例化出來的對象。
一、
在Java的線程模型中,棧幀中的局部變量是線程私有的,永遠不需要進行同步。但是如果允許通過匿名內部類把棧幀中的變量地址泄漏出去(逃逸),那么就會引發非常可怕的后果:一份“本來被 Java 線程模型規定為永遠是線程私有的數據”將可能被並發訪問!哪怕它不被並發訪問,棧中變量的內存地址泄漏到棧幀之外這件事本身已經足夠危險了,這是Java這種內存安全的語言絕對無法容忍的。
二、
其實這段代碼中的局部變量 value 和 forEach(i -> value++) 中的 value 實際上是兩個名字和值相同的變量而已,只是在 lambda 表達式中隱式創建了一個名為 value 的變量,並且將局部變量 value 的值 copy 了過去,因此它們是兩個不同的"符號"。同時 Java 為了防止我們在 lambda 中修改其隱式創建出來的變量時,將內外的這兩個同名"符號"誤以為是同一個變量,就規定這種變量必須是 final 或 等效於 final 的,不能修改。
解決方案
如果我們就是想將這兩個變量作為同一個變量使用,可以借助於在堆中創建的對象來實現。
public static void main(String[] args) {
int[] value = new int[1];
IntStream.range(0, 10).forEach(i -> value[0]++);
System.out.println(value[0]);
}
其他疑問
既然可以通過上面的方法借助堆中的對象可以實現在 lambda 中修改外部的變量,那么為什么下面的代碼依然報錯?
public static void main(String[] args) {
Integer value = new Integer(0);
IntStream.range(0, 10).forEach(i -> value++);
System.out.println(value);
}
public static void main2(String[] args) {
int[] value = new int[1];
IntStream.range(0, 10).forEach(i -> value = new int[1]);
System.out.println(value[0]);
}
原因: 第一段代碼通過 Integer 傳值給 lambda 的思路是沒有問題的,但是 value++ 卻又將傳遞進來的 value 的值給替換了 (Java 中是值傳遞的,只不過對於對象參數,值的內容是對象的引用),第二段代碼中的 value = new int[1] 也是同理,它們都是將 value 的值的引用給修改了, 而 lambda 是不允許這種修改的,所以產生了報錯。
