先来看一下下面一段代码
public class InnerClassTest { public static void main(String[] args) { int a = 10; new Service() { @Override public void method() { System.out.println("a=" + a); } }.method(); a = 11; } } interface Service { public void method(); }
这段代码并不能通过编译,因为他会抱有如下异常:
这里发现,我的匿名内部类调用外部的局部变量的时候发生了报错,那么这个报错的原因是什么?
我们先来看解决这个报错的方法:
1:删掉下面的对a=11的修改,这意味着a这个值并没有被修改,是只读的
2:将a变量设定为final类型
两种方法都能解决上面的问题,但是为什么我们使用外部的局部变量的时候我们需要它是未被修改的或者说为什么必须是final的?
答:
匿名内部类无法直接访问外部类方法中的局部变量,除非该变量被声明为final类型,是因为匿名内部类在实例化时会隐式地持有对外部类方法中的局部变量的引用。为了确保引用的可用性和一致性,Java编译器要求局部变量必须是final类型的。
当一个局部变量被声明为final时,Java编译器会在内存中创建一个拷贝,而不是直接引用原始变量。这样做的目的是为了避免匿名内部类中对外部局部变量的修改导致不一致的情况发生。
通过将局部变量声明为final,Java编译器确保了匿名内部类在获取局部变量的值时,能够获取到该变量的固定值。这样,即使外部方法调用已经结束,局部变量仍然可以正确地被匿名内部类所访问和使用。
需要注意的是,在Java 8之后,如果局部变量被显式声明为final,即使没有使用final关键字,同样可以在局部类或匿名内部类中访问。这是因为Java 8引入了"effectively final"的概念,即在变量被赋值后,没有再发生修改。在这种情况下,编译器会将其视为final类型的变量,从而允许在局部类或匿名内部类中访问该变量。这也就是为什么上面我们只要把a=11这一行代码删掉也可以通过运行的原因。
当然,如果你的JDK版本是7或者更早,那么就依旧会报错,如下:
面试官:为什么匿名内部类只能访问外部类的final类型局部变量?
我对这个问题的完整解释是这样子回答的
我:其实对于为什么需要使用final类型的外部局部变量,我的解释应该会倾向于生命周期的概念。
我们知道,匿名内部类的调用发生在方法中,方法创建时会创建一个栈帧,栈帧中保存的是我们的局部变量等信息,这个时候如果我们使用了匿名内部类,还会再堆中创建一个类,然后如果我们的这个匿名内部类使用了外部变量,而外部变量的创建是跟随方法的,如果方法结束,那么外部变量就要被回收消失,此时会出现生命周期的问题,也就是我们的匿名内部类还指向这个方法,并且内部类还没有被回收,因为堆内对象的回收需要的是垃圾回收器的工作而不是跟随方法,即使这个对象是通过这个方法才创建的。
因此此时就会出现外部变量消失的情况,而匿名内部类依旧存在于堆内存中并且对外部变量存有引用,为了解决这种生命周期不一致的问题,可以使用final关键字修改局部变量的生命周期,我们知道如果对局部变量使用final修饰,他就会在内存中留有一份数据。
当局部变量被声明为final时,它们在内存中会保留其值。在使用final修饰的局部变量时,其值在声明时被确定,并且不能再被修改。这样做的目的是为了确保在匿名内部类或其他类的方法中使用这些final变量时,它们的值保持不变。
在编译过程中,如果一个局部变量被匿名内部类或其他闭包引用,编译器会创建一个新的内部类,并将这些被引用的final局部变量的值传递给内部类的构造函数。因此,这些final局部变量的值将在内存中一直存在,直到内部类对象不再被引用,并由垃圾回收器回收。
值得注意的是,如果局部变量没有被匿名内部类或其他闭包引用,即使将其声明为final,它们在方法执行完毕后仍然会被销毁,不会一直保存在内存中。只有在有需要的情况下才将局部变量声明为final。