局部变量需为“事实最终态”以确保内部类值拷贝的语义一致性:因栈上局部变量生命周期短于堆中内部类对象,编译器通过拷贝初始化隐式字段,仅当变量不再被修改时才能保证内外值同步。

因为局部变量存在栈上,而内部类实例活在堆中,二者生命周期不匹配——编译器只能靠“值拷贝”来桥接,而“事实最终态”(effectively final)是保证拷贝值不被外部篡改、从而维持语义一致的必要约束。
栈帧消亡后,局部变量就没了
方法执行时,局部变量分配在当前线程的虚拟机栈帧里;方法返回,栈帧弹出,变量空间立即释放。但匿名内部类或局部内部类创建的对象可能被传给其他线程、注册为监听器、或长期持有引用——它存活在堆内存中,远超原方法生命周期。JVM 不允许堆对象直接持有一个已失效栈地址,这会引发悬垂引用或读取垃圾数据。
编译器悄悄做了值拷贝
你写的这段代码:
void handle() {
String msg = "ok";
new Thread(() -> System.out.println(msg)).start();
}
编译后,等效于让内部类构造器把 msg 的当前值作为参数传入,并存为一个隐式私有字段(如 val$msg)。这个字段在内部类实例化时就固定了,后续不可变。
这不是语法糖,而是字节码层面的真实字段——它和原始局部变量完全独立,只是初始值相同。
为什么必须是“事实最终态”
如果允许非 final 局部变量被捕获,外部方法后续还能修改它,而内部类持有的仍是旧副本,就会出现两个“不同步”的值:
- 基本类型:外部改了 int count = 5; 后又赋值为 10,内部类看到的永远是 5
- 引用类型:外部把 list 指向新 ArrayList,内部类仍用老 list;更危险的是,若外部把 list = null,内部类调用 list.add() 就直接 NPE
Java 不禁止你改对象内部状态(比如 list.add()),只禁止你改“引用本身”或“基本值本身”——这正是 effectively final 所保障的边界:确保拷贝那一刻的值,在整个内部类生命周期内逻辑可信。
不是为了安全,而是为了确定性
final 或 effectively final 不是为防恶意修改,而是向编译器发出明确信号:“这个变量不会再变”。有了这个承诺,编译器才敢做值拷贝;运行时才敢信任那个隐式字段代表“当时那个值”的唯一权威副本。没有它,闭包行为就失去可预测性——而这恰恰是函数式编程和异步回调的基石。










