欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

对JAVA中内部类(匿名内部类)访问的局部变量为何要用final修饰的讨论

程序员文章站 2022-07-10 18:46:55
前言:因为使用内部类而出现需要使用 final 修饰符主要的有两个地方:在内部类的方法使用到方法中定义的局部变量,则该局部变量需要添加 final 修饰符在内部类的方法形参使用到外部传过来的变量,则形参需要添加 final 修饰符其实这两种情况本质是一样的,即内部类方法若想使用外部方法中的变量,此变量必须被final修饰,如下图所示错误信息对于此种现象...

前言:

使用内部类而出现需要使用 final 修饰符主要的有两个地方:

  1. 在内部类的方法使用到方法中定义的局部变量,则该局部变量需要添加 final 修饰符
  2. 在内部类的方法形参使用到外部传过来的变量,则形参需要添加 final 修饰符

其实这两种情况本质是一样的,即内部类方法若想使用外部方法中的变量,此变量必须被final修饰,如下图所示

对JAVA中内部类(匿名内部类)访问的局部变量为何要用final修饰的讨论
错误信息


对于此种现象,网上的主流解释:

当我们创建匿名内部类的那个方法调用运行完毕之后,因为局部变量的生命周期和方法的生命周期是一样的,当方法弹栈,这个局部变量就会消亡了,但内部类对象可能还存在。 此时就会出现一种情况,就是我们调用这个内部类对象去访问一个不存在的局部变量,就可能会出现空指针异常。如果使用 final 修饰会在类加载的时候进入常量池,即使方法弹栈,常量池的常量还在,也可以继续使用,JVM 会持续维护这个引用在回调方法中的生命周期。

简单易懂,但在此我尝试推翻这个说法。

对JAVA中内部类(匿名内部类)访问的局部变量为何要用final修饰的讨论

我的结论为:

final存在的意义并不是为了使内部类所引用的局部变量存在(即使所引用的变量被释放了,变量的值内部类依旧可以用),而是为了防止因内外值的不一致而出现问题。

我在两个方面对这个结论进行论证:

  • 内部类并非引用的外部方法中的变量,而是对变量的值进行了备份
  • JDK8新特性增加的 effectively final 解决办法

论证一:

观察以下源代码:

public class Outer {

    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.test1();
        outer.test2("165");
    }

    public static class Inner {
        public void print() {
        }
    }

    //1. 在内部类的方法使用到方法中定义的局部变量,则该局部变量需要添加 final 修饰符(JDK8新特性不写默认有final)
    public void test1() {
        int temp = 1;
//        temp = 2; //报错,说明默认传入参数被final修饰
        new Inner() {
            @Override
            public void print() {
                System.out.println(temp);
            }
        }.print();
    }

    //2. 在内部类的方法形参使用到外部传过来的变量,则形参需要添加 final 修饰符(JDK8新特性不写默认有final)
    public void test2(String s) {
//        s = "123";   //报错,说明默认传入参数被final修饰
        new Inner() {
            @Override
            public void print() {
                System.out.println(s);
            }
        }.print();
    }
}

一共编译出四个class文件,查看Outer$1.class和Outer$2.class的代码:

//Outer$1.class
import Outer.Inner;

class Outer$1 extends Inner {
    Outer$1(Outer var1, int var2) {
        this.this$0 = var1;  
        this.val$temp = var2;
    }

    public void print() {
        System.out.println(this.val$temp);//注意看此处
    }
}


//Outer$2.class
import Outer.Inner;

class Outer$2 extends Inner {
    Outer$2(Outer var1, String var2) {
        this.this$0 = var1;
        this.val$s = var2;
    }

    public void print() {
        System.out.println(this.val$s);//注意看此处
    }
} 

第一个论据:

内部类并非引用的外部方法中的变量,而是对变量的值进行了备份,即并不存在变量释放后,内部类用不到的情况。

论证二:

观察下图:

对JAVA中内部类(匿名内部类)访问的局部变量为何要用final修饰的讨论
出现错误信息

根据Idea的错误提示点击后:

对JAVA中内部类(匿名内部类)访问的局部变量为何要用final修饰的讨论
将s变为effectively final 变量

对JAVA中内部类(匿名内部类)访问的局部变量为何要用final修饰的讨论

第一个论据:

如果确定内部类使用的外部变量的一个值,且只需要固定值时可以将这个值传入即可,人家(匿名内部类)只想要这个值嘛!大可不必直接把变量被final修饰。


综上所述:

final存在的意义并不是为了使内部类所引用的局部变量存在(即使所引用的变量被释放了,变量的值内部类依旧可以用),而是为了防止因内外值的不一致而出现问题。即内部类率先使用了这个值(A状态值),但在内部类方法没结束时,外面变量被其他语句稍迟变更了数据值(B状态值),此时内部类引用的值还是原来那个值(A),外面那个却变了(B),这在当程序出现错误时很难去排查到,为避免这种情况发生,Java规定要么被final修饰成为一个常量,要么用JDK8提供的effectively final办法确定内部类方法只使用最先的那个值。

参考文章:

https://www.jianshu.com/p/e310b56fd105