【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。


相关文章
|
1月前
|
Java 开发工具
【Azure Storage Account】Java Code访问Storage Account File Share的上传和下载代码示例
本文介绍如何使用Java通过azure-storage-file-share SDK实现Azure文件共享的上传下载。包含依赖引入、客户端创建及完整示例代码,助你快速集成Azure File Share功能。
339 4
|
2月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
3月前
|
存储 安全 Java
java: 无法访问org.springframework.ldap.core.LdapTemplate
java: 无法访问org.springframework.ldap.core.LdapTemplate
133 9
|
4月前
|
Java 数据库连接 数据库
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
本文全面总结了Java核心知识点,涵盖基础语法、面向对象、集合框架、并发编程、网络编程及主流框架如Spring生态、MyBatis等,结合JVM原理与性能优化技巧,并通过一个学生信息管理系统的实战案例,帮助你快速掌握Java开发技能,适合Java学习与面试准备。
235 2
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
|
2月前
|
算法 Java
50道java基础面试题
50道java基础面试题
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
311 4