1. 程式人生 > 其它 >lambda 表示式為什麼不能修改區域性變數

lambda 表示式為什麼不能修改區域性變數

程式碼示例

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

原因分析

首先,我們明確以下幾點內容:

  1. value 是一個區域性變數。
  2. forEach(i -> value++)
    中的 value++ 屬於在 lambda 表示式中修改區域性變數。
  3. 我們可以把 lambda 表示式看作是一個匿名內部類例項化出來的物件。

一、
在Java的執行緒模型中,棧幀中的區域性變數是執行緒私有的,永遠不需要進行同步。但是如果允許通過匿名內部類把棧幀中的變數地址洩漏出去(逃逸),那麼就會引發非常可怕的後果:一份“本來被 Java 執行緒模型規定為永遠是執行緒私有的資料”將可能被併發訪問!哪怕它不被併發訪問,棧中變數的記憶體地址洩漏到棧幀之外這件事本身已經足夠危險了,這是Java這種記憶體安全的語言絕對無法容忍的。

二、
其實這段程式碼中的區域性變數 valueforEach(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 是不允許這種修改的,所以產生了報錯。