因為在 Lambda 表達式內部訪問的外部局部變量必須是?final
?或?effectively final
(事實最終變量),而?i++
?操作試圖改變這個變量的值,違反了這一規定。
下面我們來詳細拆解這個問題,讓你徹底明白。
1. 一個具體的例子
我們先看一段會報錯的代碼:
java
List<String> list = Arrays.asList("A", "B", "C");
int i = 0;
list.forEach(item -> {System.out.println(item);i++; // 編譯錯誤:Variable used in lambda expression should be final or effectively final
});
編譯器會直接在?i++
?這一行報錯。
2. 深入原理:為什么要有這個限制?
這背后有兩個關鍵原因:變量捕獲和并發安全。
原因一:變量捕獲與值拷貝
在傳統?
for
?循環中,變量?i
?是堆棧上的一個局部變量,它的生命周期和作用域非常清晰。但在 Lambda 表達式中,情況不同了。Lambda 表達式可能不會立即執行(比如它被傳遞到一個方法中,在未來的某個時間點才被調用)。為了確保 Lambda 在執行時還能“看到”這個外部變量?
i
,Java 采用了一種叫做?“變量捕獲”?的機制。捕獲發生時,Java 并不是把變量?
i
?本身傳遞進 Lambda,而是將變量?i
?的值做一個拷貝,傳遞給 Lambda 表達式。現在想象一下,如果允許你在 Lambda 內部修改?
i
(比如?i++
),你修改的只是 Lambda 內部的那個拷貝,而外部的原始變量?i
?的值并沒有改變。這會造成極大的困惑和歧義:你看的是同一個變量,但值卻不一樣。
為了保證數據的一致性,Java 語言設計者干脆規定:被捕獲的變量必須是不可變的(final or effectively final)。這樣就不存在“修改拷貝還是修改原值”的困惑了,因為大家看到的都是一個永遠不會改變的值。
原因二:并發安全
Lambda 表達式,尤其是在與 Stream API 結合使用時,很容易在多線程環境下并行執行。
假設允許在 Lambda 中修改外部變量,那么多個線程將會同時競爭修改同一個變量?
i
。i++
?這個操作本身(讀取、增加、寫入)就不是原子性的,這必然會導致嚴重的競態條件,得到不可預知的結果。強制使用?
final
?或?effectively final
?變量,就從根源上杜絕了這種線程不安全的數據修改,鼓勵開發者使用更安全的方式(如 reduction 操作?reduce()
,?collect()
)來匯總結果,而不是依賴易變的外部狀態。
3. 什么是 Effectively Final?
這是 Java 8 引入的一個概念。你不需要顯式地用?final
?關鍵字聲明一個變量,只要這個變量在初始化后再也沒有被修改過,編譯器就認為它是“事實最終變量”。
在你的例子中,i++
?試圖修改?i
,破壞了?i
?的 “effectively final” 狀態,所以編譯器報錯。
4. 如果我確實需要在 forEach 中計數,該怎么辦?
使用原子類(Atomic Classes)
創建一個可變的容器,但這個容器的原子操作是線程安全的。AtomicInteger
?就是一個不錯的選擇。
java
List<String> list = Arrays.asList("A", "B", "C");
AtomicInteger atomicCount = new AtomicInteger(0); // 創建一個原子整數list.forEach(item -> {System.out.println(item);atomicCount.getAndIncrement(); // 原子性自增,相當于 i++
});System.out.println("Count: " + atomicCount.get()); // 輸出:Count: 3
注意:這解決了編譯問題,但如果?forEach
?是并行流(parallelStream().forEach(...)
),雖然?getAndIncrement
?是原子的,整個計數邏輯在并發下仍然可能是亂序的。對于單純計數,更好的并行做法是?list.stream().count()
。