在Java 8之前一个常识就是如果要在方法中定义一个匿名内部类并使用该方法内的局部变量(包括参数),需要使用final关键字修饰。网上也有很多对这种机制的解释和说明,但是大部分都是一种抽象的认识。如果能够分析一下字节码,这个问题其实很清楚。
显然的一个事实是局部变量(称为变量a)是保存在栈帧的局部变量表中的(引用或基本类型),这里把定义匿名内部类的方法称为方法A,匿名内部类的中使用这个局部变量的方法(称为方法B)经过编译后保存在class文件的方法表中,方法体保存在Code属性中。运行时经过类加载,验证存储到方法区中。在执行匿名内部类中,JVM创建的是方法B的栈帧。它显然和方法A的栈帧没有直接的关系,甚至方法B执行时,方法A的栈帧已经被回收了,那JVM如何保证方法B能够正确的使用变量a指向的值呢?
下面就通过查看字节码来看看Java编译器和JVM是如何做到这一点的,基于Oracle JDK8编译;
1. 字节码分析:
外围类:FinalParameter
定义一个派生自A的匿名内部类,其中使用方法testFinal的局部变量a和参数bytes;
public final class FinalParameter {
interface A {
void test();
}
private void testFinal(final byte[] bytes) {
final int a = 10;
new A() {
@Override
public void test() {
System.out.println(a + " " + bytes.length);
}
};
}
}
根据内部类命名规则我们知道这个匿名内部类的类名是FinalParameter$1;
使用javap -verbose FinalParameter$1,查看该类的字节码,Unix环境下注意不要忘了转义"$"。我截取了字段表部分:
final int val$a;
descriptor: I
flags: ACC_FINAL, ACC_SYNTHETIC
final byte[] val$bytes;
descriptor: [B
flags: ACC_FINAL, ACC_SYNTHETIC
final com.jvm.showByteCode.FinalParameter this$0;
descriptor: Lcom/jvm/showByteCode/FinalParameter;
flags: ACC_FINAL, ACC_SYNTHETIC
可以看到编译器为我们添加了3个字段,分别是:
val$a,val$bytes,this$0;
其中this$0是指向外围类对象实例的引用,而前两个正是从外围类方法局部变量bytes和a生成而来的,并且它们是final的。
再来看看FinalParameter$1类的<init>()方法:
描述符:
descriptor: (Lcom/jvm/showByteCode/FinalParameter;I[B)V
Code属性(方法实现):
Code:
stack=2, locals=4, args_size=4
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/jvm/showByteCode/FinalParameter;
5: aload_0
6: iload_2
7: putfield #2 // Field val$a:I
10: aload_0
11: aload_3
12: putfield #3 // Field val$bytes:[B
15: aload_0
16: invokespecial #4 // Method java/lang/Object."<init>":()V
19: return
外围类中testFinal方法中调用匿名内部类的部分:
10: invokespecial #3 // Method com/jvm/showByteCode/FinalParameter$1."<init>":(Lcom/jvm/showByteCode/FinalParameter;I[B)V
这里我们就可以清楚看到以下事实:
(1)<init>方法有4个参数,其中第一个是this,另外三个分别类型分别FinalParameter,int,byte数组;
(2)它们参数被赋值给了对应的final字段(this$0,val$a,val$bytes);
(3)外围类将当前实例对象引用,a数值,bytes数组引用传递给了FinalParameter$1的<init>()方法;
因此,我们可以知道,在匿名内部类中使用外围类方法的局部变量,实际上是使用对应值(引用)的final成员变量。
内部类中使用的是该局部变量的副本,因此如果要在语义上保证局部变量和副本的一致性,就应当使用final来保证该局部变量不变。
2. Java 8中“改进”:
private void testFinal(byte[] bytes) {
int a = 10;
new A() {
@Override
public void test() {
System.out.println(a + " " + bytes.length);
}
};
// a = 11;
// bytes = null;
}