问题描述
Effectively final
- Java 1.8 新特性,对于一个局部变量或方法参数,如果他的值在初始化后就从未更改,那么该变量就是 effectively final(事实 final)。 这种情况下,可以不用加 final 关键字修饰。
内部类会持有外部类对象的引用
- 非静态内部类,持有外部类的引用;
- 以编译器自动生成的成员变量的形式持有;
- 通过编译器自动生成的构造方法传入。
- 静态内部类,不持有外部类的引用。
内部类
非静态内部类会通过自动生成的构造函数持有一个外部类对象的引用:
即便给内部类增加一个非默认的构造函数,编译器依然会自动为构造函数添加一个外部类对象的参数:
静态内部类
静态内部类自动生成的构造方法不会持有外部类的引用:
匿名内部类
匿名内部类也会通过构造函数持有一个外部类对象的引用:
匿名内部类会将捕获的局部变量在其构造函数中传入:
匿名内部类捕获外部变量添加 final 的作用是:保证匿名内部类捕获的副本引用和外部的局部变量始终都指向同一个对象,也就是没有人可以修改它们的指向。
假如外部方法的局部变量不加final
,有可能外部的局部变量的指向改变了,但是内部类却不知道。这样就可能导致内部类操作的对象和外部的局部变量指向的不是同一个对象,会出现 bug。
注意:匿名内部类捕获的局部变量加 final
是指的处于同一方法内的局部变量,如果捕获的是所处的外部类中的变量,则不需要加 final
,这是因为在这种情况下,可以直接通过外部类对象的引用来获取到其成员变量。
总结
- 匿名内部类,持有外部类的引用;
- 以编译器自动生成的成员变量的形式持有;
- 通过编译器自动生成的构造方法传入。
- 匿名内部类,通过这个引用访问外部类的成员变量和方法。
- 匿名内部类,访问外部局部变量时,其实是访问自身的一个成员变量;
- 这个成员变量,是编译器自动生成的;
- 这个成员变量,由编译器自动生成的构造方法初始化;
- 为了保证这个成员变量和外部局部变量时刻保持一致性,二者必须都是
final
的。
根本原因就是为了保证内部和外部对这个局部变量的对象的操作保持一致性。因此要求两个副本均不可变。
Kotlin 的匿名内部类
kotlin 中的匿名内部类不会外部变量时,即便不使用类似final
的关键字修饰也能保证内部和外部访问的同一局部对象的一致性。
通过以上字节码分析可以得出结论:
- Kotlin 的匿名内部类不会在构造函数中传入整个外部类对象的引用
- 但是对于捕获的局部变量,会自动生成一个不可变(
val
)的保证类对象(ObjectRef
)传入构造函数中。
对应的伪代码结构2如下:
ObjectRef 是什么:
我们看到它就是一个泛型类,内部持有一个 element
的泛型成员变量,可以认为是 Kotlin 为了解决捕获局部变量问题生成的一个装箱类。
除了 ObjectRef
,Kotlin 中还有 ByteRef
,ShortRef
、IntRef
、LongRef
、FloatRef
、 DoubleRef
、CharRef
、BooleanRef
。
所以虽然与 Java 的解决方式不同,但本质上看思想是一致的,都要保持内部和外部对捕获变量的操作一致性,即保证这两个副本的不可变性。
虽然包装类对象的指向不可变,但是包装类对象里面包的东西是可以改变指向的,这一点比 Java 要优秀:
这一切都是编译器为我们自动实现的,但对于开发者而言,体验上就会跟 Java 有明显的不同:“Java 需要 final
捕获,但 Kotlin 不需要 val
捕获”,但这是一个错觉,实际上 Kotlin 也需要,只不过你看不到而已。
内存泄漏问题
内存泄漏的根本原因就是一个长生命周期的对象被一个短生命周期的对象所引用。
- 一旦内部类对象被长生命周期对象引用,或自身生命周期过长,就会导致外部类无法被 GC 回收,因为它们在同一条引用链上,根据 GC Root 可达性分析算法判断为可达。
- 比如在
Activity
中通过匿名内部类的方式创建的Handler
、AsyncTask
、ResultReceiver
等。
解决方式:
- 不使用(匿名)内部类,将类的定义放在外部,显示的构造传参,并在外部类销毁时(如
Activity.onDestroy()
)主动断开对外部类的引用(例如很多框架会提供解绑、反注册的API以便用户在页面销毁时进行调用)。 - 必须要使用内部类的场景,采用静态内部类 + 弱引用指向外部类对象的方式。(由于弱引用一旦被GC扫描发现就会回收,所以不存在内存泄漏问题)