问题再现
首先我们将该问题演示一下。
Java8之前,在匿名内部类中访问外部方法的局部变量,该局部变量必须显式声明为final类型。
// JDK 1.7
public class TestInnerClass {
@SuppressWarnings("unused")
private void function() {
final int localvar = 1;
new Runnable() {
@Override
public void run() {
System.out.println(localvar);
}
};
}
}
到了Java8,在匿名内部类中访问的外部方法的局部变量不需要显式声明为final类型。这其实是Java8的一个语法糖——如果在匿名内部类中访问外部方法的局部变量,那么该局部变量会被隐式声明为final类型。一旦你尝试在外部方法中改变局部变量的值,就会出现报错信息:Local variable localvar defined in an enclosing scope must be final or effectively final。
//JDK 1.8
public class TestInnerClass {
@SuppressWarnings("unused")
private void function() {
int localvar = 1;
new Runnable() {
@Override
public void run() {
// localvar++; // Error
System.out.println(localvar);
}
};
// localvar++; // Error
}
}
问题阐述完毕,下面进入揭秘环节。
字节码文件与局部变量
首先我们要明确一点:匿名内部类也是一个类。作为一个类,它就会有对应的字节码文件(.class文件)。字节码文件是怎么产生的呢?我们知道Java文件通过编译之后,就会生成对应的字节码文件。也就是说,字节码文件是编译阶段的产物。
下图是TestInnerClass类的Java文件和编译之后生成的字节码文件。可以看到TestInnerClass.java文件编译之后生成了两个字节码文件:TestInnerClass.class和TestInnerClass$1.class,TestInnerClass$1.class就是匿名内部类的字节码文件。
那么局部变量是怎么产生的呢?局部变量是在程序在运行期动态生成的。如此一来问题就出现了,编译期生成的匿名内部类的字节码文件如何访问运行期动态生成的局部变量呢?
下面我们就一探究竟匿名内部类的字节码文件是如何访问到局部变量的。
查看反编译后的TestInnerClass$1.class文件:
class TestInnerClass$1 implements Runnable {
TestInnerClass$1(TestInnerClass var1) {
this.this$0 = var1;
}
public void run() {
System.out.println(1);
}
}
可以看到编译器直接将该局部变量的值对应的字节码嵌入到了匿名内部类的字节码文件中。那么这就是匿名内部类字节码文件访问局部变量的方式吗?
这样下结论未免太早,例子中匿名内部类访问的局部变量是一个编译期就可以确定下来的值,如果该局部变量的值在编译期无法确定下来呢?譬如这样:
//JDK 1.7
public class TestInnerClass {
@SuppressWarnings("unused")
private void function() {
final double localvar = Math.random(); // 运行期才会生成的数据
new Runnable() {
@Override
public void run() {
System.out.println(localvar);
}
};
}
}
我们再次查看反编译后的TestInnerClass$1.class文件:
class TestInnerClass$1 implements Runnable {
TestInnerClass$1(TestInnerClass var1, double var2) {
this.this$0 = var1;
this.val$localvar = var2;
}
public void run() {
System.out.println(this.val$localvar);
}
}
我们可以看到编译器将匿名内部类访问的局部变量作为该类构造方法的参数(var2)。
这就是说,如果匿名内部类访问的外部方法的局部变量的值在编译期无法确定下来,那么编译器会将该局部变量作为匿名内部类构造方法的参数传入。等到匿名内部类创建实例对象时,再将该局部变量的值作为匿名内部类构造方法的参数传入。
为什么是final
在了解上面的知识之后我们就能发现,匿名内部类访问的局部变量并不是真正的局部变量,而是局部变量的值拷贝。也就是说在匿名内部类中修改局部变量的值,无法影响到外部方法中的局部变量。
因此为了保证匿名内部类和外部方法中访问的局部变量的一致性,局部变量必须使用final修饰。