因为在 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()
。