天天看点

匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?

为什么匿名内部类引用的局部变量需要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修饰, 而引用外部类的成员变量就不需要final修饰?
添加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修饰, 而引用外部类的成员变量就不需要final修饰?
为什么匿名内部类引用的局部变量需要final修饰才可以?

JVM在编译内部类时也会编译出一个单独的类文件出来

匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?

 Outer$1.class内容:

匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?

可以看到,  在内部类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修饰, 会出现什么问题?

数据不一致问题:

匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要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();
    }
}
           

执行结果: 运行成功!

匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?

 Outer$1.class内容:

匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?

可以看到, 在run方法中使用外部类的成员变量persion时是通过外部类的引用this.this$0.persion来调用的,实际上调用的还是外部实例对象的变量, 在内部类中并没有进行备份维护,  所以不论是在外部类的方法中还是在run方法中修改变量persion时, 修改的都是同一个变量, 保证了数据一致性, 也就不需要final了;

如果调用的外部方法为静态方法

在上面的反编译类中可以看出, jvm在编译内部类时, 会将外部类的实例对象封装到内部类中, 以便在内部类中调用外部类的方法; 如果外部方法时静态方法, 则jvm不会将外部类实例封装到内部类中, 而是直接通过类名来调用外部静态方法或静态变量

匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?
匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?
匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?
匿名内部类引用的局部变量为什么需要final修饰, 而引用外部类的成员变量就不需要final修饰?

继续阅读