对JAVA中内部类(匿名内部类)访问的局部变量为何要用final修饰的讨论
前言:
使用内部类而出现需要使用 final 修饰符主要的有两个地方:
- 在内部类的方法使用到方法中定义的局部变量,则该局部变量需要添加 final 修饰符
- 在内部类的方法形参使用到外部传过来的变量,则形参需要添加 final 修饰符
其实这两种情况本质是一样的,即内部类方法若想使用外部方法中的变量,此变量必须被final修饰,如下图所示
对于此种现象,网上的主流解释:
当我们创建匿名内部类的那个方法调用运行完毕之后,因为局部变量的生命周期和方法的生命周期是一样的,当方法弹栈,这个局部变量就会消亡了,但内部类对象可能还存在。 此时就会出现一种情况,就是我们调用这个内部类对象去访问一个不存在的局部变量,就可能会出现空指针异常。如果使用 final 修饰会在类加载的时候进入常量池,即使方法弹栈,常量池的常量还在,也可以继续使用,JVM 会持续维护这个引用在回调方法中的生命周期。
简单易懂,但在此我尝试推翻这个说法。
我的结论为:
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);//注意看此处
}
}
第一个论据:
内部类并非引用的外部方法中的变量,而是对变量的值进行了备份,即并不存在变量释放后,内部类用不到的情况。
论证二:
观察下图:
根据Idea的错误提示点击后:
第一个论据:
如果确定内部类使用的外部变量的一个值,且只需要固定值时可以将这个值传入即可,人家(匿名内部类)只想要这个值嘛!大可不必直接把变量被final修饰。
综上所述:
final存在的意义并不是为了使内部类所引用的局部变量存在(即使所引用的变量被释放了,变量的值内部类依旧可以用),而是为了防止因内外值的不一致而出现问题。即内部类率先使用了这个值(A状态值),但在内部类方法没结束时,外面变量被其他语句稍迟变更了数据值(B状态值),此时内部类引用的值还是原来那个值(A),外面那个却变了(B),这在当程序出现错误时很难去排查到,为避免这种情况发生,Java规定要么被final修饰成为一个常量,要么用JDK8提供的effectively final办法确定内部类方法只使用最先的那个值。
参考文章: