为什么匿名内部类引用的局部变量需要final修饰?
使用JDK1.8之前的版本, 编写以下代码
示例1
public class Outer {
public void test() {
Persion persion = new Persion("lic","18");
new Thread(){
@Override
public void run() {
System.out.println(persion);
}
}.start();
}
public static void main(String[] args) {
new Outer().test();
}
}
执行结果: 编译不通过!
添加final关键字
示例2:
public class Outer {
public void test() {
final Persion persion = new Persion("lic","18");
new Thread(){
@Override
public void run() {
System.out.println(persion);
}
}.start();
}
public static void main(String[] args) {
new Outer().test();
}
}
执行结果: 执行成功!
为什么匿名内部类引用的局部变量需要final修饰才可以?
JVM在编译内部类时也会编译出一个单独的类文件出来
Outer$1.class内容:
可以看到, 在内部类Outer$1中维护了两个自己的成员变量this$0和var$persion, 并且在构造方法中进行赋值; 那么, 也就是JVM在构造内部类初始化时, 将外部方法的局部变量进行了备份, 维护到自己的实例中, 在run方法中直接调用输出即可; 注意: 在内部类Outer$1也维护着外部类实例的引用this$0, 使得在内部类中可以直接调用外部类的变量, 方法; 此时局部变量persion和var$persion是互不干扰的两个变量, 但是在语义上却是同一个值。 如果persion没有被final修饰, 那么, 在run方法中对var$persion的引用地址进行修改, persion是无法感知, 更不用说同步了, 反之亦然, 这就违背了数据的一致性; 所以在匿名内部类引用局部变量时, 局部变量是需要final进行修饰的; 注意: 在jdk1.8之后, 在编码时不加final关键字也是可以的, 因为JVM在编译时会自动帮我们加上;
思考1: 如果persion变量没有被final修饰, 会出现什么问题?
数据不一致问题:
思考2: 为什么要将外部局部变量拷贝一份到内部类中呢?
(1) 如果内部对象是一个Thread对象, 在Java虚拟机的运行时数据区域中,局部变量persion是位于方法内部的,因此局部变量persion是在虚拟机栈上(虚拟机栈是线程的私有内存区),也就意味着这个变量无法进行共享,匿名内部类也就无法直接访问,因此只能通过值传递的方式,传递到匿名内部类中, 在内部类中进行备份, 使得在内部类中可以随意访问; 在persion为外部类的成员变量时, 对应的存储位置在虚拟机中的堆位置上,因此无论在这个类的哪个地方,我们只需要通过 this.this$0.persion,就可以获得这个变量。因此,在创建内部类时,无需进行拷贝,甚至都无需将这个persion传递给内部类, 而是通过外部类的实例引用获取
(2) 如果内部类的对象为一个普通对象, 且不考虑是Thread这种情况, 那么, 貌似不需要使用数据拷贝也可以, 但是如果内部类引用着外部局部变量, 当外部方法执行完成后, 该方法执行使用虚拟机栈会被立刻回收, 那么外部的局部变量也会被销毁, 由于内部类是一个独立的对象, 并不会随着外部方法执行完成而销毁, 而是通过jvm的可达性分析后, 判定该对象没有被GC根引用, 才会进行标记, 准备销毁; 那么在此过程中, 就出现了一个问题, 也就是内部类中还引用着一个已经被jvm销毁的变量; 如果在内部对象的finalize()中使用了该变量, 但是该变量已经不可用了... 所以需要将外部的局部变量备份到内部类对象中, 虽然在备份后, 局部变量与备份到内部类的成员变量时两个互不干扰的变量, 为了保证在语义上保持一致, 需要加上final关键字修饰;
为什么引用外部类的成员变量就不需要final修饰?
示例3:
public class Outer {
Persion persion = new Persion("lic","18");
public void test() {
new Thread(){
@Override
public void run() {
System.out.println(persion);
}
}.start();
}
public static void main(String[] args) {
new Outer().test();
}
}
执行结果: 运行成功!
Outer$1.class内容:
可以看到, 在run方法中使用外部类的成员变量persion时是通过外部类的引用this.this$0.persion来调用的,实际上调用的还是外部实例对象的变量, 在内部类中并没有进行备份维护, 所以不论是在外部类的方法中还是在run方法中修改变量persion时, 修改的都是同一个变量, 保证了数据一致性, 也就不需要final了;
如果调用的外部方法为静态方法
在上面的反编译类中可以看出, jvm在编译内部类时, 会将外部类的实例对象封装到内部类中, 以便在内部类中调用外部类的方法; 如果外部方法时静态方法, 则jvm不会将外部类实例封装到内部类中, 而是直接通过类名来调用外部静态方法或静态变量