lambda 表达式中使用的变量如何一定要有final修饰?

一、场景描述

最近在使用和学习Java的多线程池,而在使用单线程线程池SingleThreadExecutor是出现了问题。

编译报错:Variable used in lambda expression should be final or effectively final

这句话的意思是,lambda 表达式中使用的变量应该是 final 或者有效的 final

二、解决方案

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// 创建一个单线程版的线程池
ExecutorService es = Executors.newSingleThreadExecutor();
// 使用
for(int i=0;i<10;i++){
int finalI = i;
es.execute(() -> System.out.println(Thread.currentThread().getName()+"打印的值是"+ finalI));
}
}

i 这个变量赋值给了 finalI 变量,但是 finalI 并没有声明为 final 类型,然而代码却能够编译通过,这是因为 Java 8 之后,在匿名类或 Lambda 表达式中访问的局部变量,如果不是 final 类型的话,编译器自动加上 final 修饰符,即 Java8 新特性:effectively final。

三、原因

前面说 Lambda 表达式或者匿名内部类不能访问非 final 的局部变量,这是为什么呢?为什么会有这种规定?

3.1 首先思考外部的局部变量 finalI 和匿名内部类里面的 finalI 是否是同一个变量?

我们知道,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接,方法出口等信息,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程(《深入理解Java虚拟机》第2.2.2节 Java虚拟机栈)。

就是说在执行方法的时候,局部变量会保存在栈中,方法结束局部变量也会出栈,随后会被垃圾回收掉,而此时,内部类对象可能还存在,如果内部类对象这时直接去访问局部变量的话就会出问题,因为外部局部变量已经被回收了,解决办法就是把匿名内部类要访问的局部变量复制一份作为内部类对象的成员变量,查阅资料或者通过反编译工具对代码进行反编译会发现,底层确实定义了一个新的变量,通过内部类构造函数将外部变量复制给内部类变量。


3.2 为何还需要用final修饰?

其实复制变量的方式会造成一个数据不一致的问题,在执行方法的时候局部变量的值改变了却无法通知匿名内部类的变量,随着程序的运行,就会导致程序运行的结果与预期不同,于是使用final修饰这个变量,使它成为一个常量,这样就保证了数据的一致性。