【Java面试】为什么匿名内部类只能访问外部类的final类型局部变量?

简介: 【Java面试】为什么匿名内部类只能访问外部类的final类型局部变量?

先来看一下下面一段代码

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。


相关文章
|
2月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2月前
|
安全 Java 容器
【Java集合类面试二十七】、谈谈CopyOnWriteArrayList的原理
CopyOnWriteArrayList是一种线程安全的ArrayList,通过在写操作时复制新数组来保证线程安全,适用于读多写少的场景,但可能因内存占用和无法保证实时性而有性能问题。
|
2月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
2月前
|
Java
【Java集合类面试二十八】、说一说TreeSet和HashSet的区别
HashSet基于哈希表实现,无序且可以有一个null元素;TreeSet基于红黑树实现,支持排序,不允许null元素。
|
2月前
|
Java
【Java集合类面试二十三】、List和Set有什么区别?
List和Set的主要区别在于List是一个有序且允许元素重复的集合,而Set是一个无序且元素不重复的集合。
|
2月前
|
Java
【Java集合类面试二十六】、介绍一下ArrayList的数据结构?
ArrayList是基于可动态扩展的数组实现的,支持快速随机访问,但在插入和删除操作时可能需要数组复制而性能较差。
|
2月前
|
存储 Java 索引
【Java集合类面试二十四】、ArrayList和LinkedList有什么区别?
ArrayList基于动态数组实现,支持快速随机访问;LinkedList基于双向链表实现,插入和删除操作更高效,但占用更多内存。
|
14天前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
2月前
|
Java
【Java集合类面试二十二】、Map和Set有什么区别?
该CSDN博客文章讨论了Map和Set的区别,但提供的内容摘要并未直接解释这两种集合类型的差异。通常,Map是一种键值对集合,提供通过键快速检索值的能力,而Set是一个不允许重复元素的集合。
|
2月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
下一篇
无影云桌面