内部类引用外部类成员的情况
Java的内部类持有外部类的的引用,在字节码层面是怎么设计的呢?我们写一段代码来看看,假设有这么一个内部类Inner,访问了外部类Out,示例代码如下:
public class Outer {
int outerField = 5;
class Inner{
public int InnerMethod(){
//调用外部类的成员变量
return outerField+1;
}
}
public Inner getInnter(){
return new Inner();
}
public static void main(String[] args) {
Outer outer = new Outer();
Inner innter = outer.getInnter();
System.out.println(innter.InnerMethod());
}
}
接下来我们使用:
javac Outer.java
java Outer
这段代码的意思是,内部类访问了外部类的outerField,然后+1返回。最后再打印,我们贴一下打印结果:
6
接下来我们看一下我们Javac之后,反编译的产物:
Outer.class
public class Outer {
int outerField = 5;
public Outer() {
}
public Outer.Inner getInnter() {
return new Outer.Inner();
}
public static void main(String[] var0) {
Outer var1 = new Outer();
Outer.Inner var2 = var1.getInnter();
System.out.println(var2.InnerMethod());
}
class Inner {
Inner() {
}
public int InnerMethod() {
return Outer.this.outerField + 1;
}
}
}
Outer$Inner.class
class Outer$Inner {
Outer$Inner(Outer var1) {
this.this$0 = var1;
}
public int InnerMethod() {
return this.this$0.outerField + 1;
}
}
可以看到,Out$Innter.class反编译后的构造函数默认带了Outer参数。
接下来我们使用javap -c Out
来看看字节码
Compiled from "Outer.java"
public class com.java.oom.Outer {
int outerField;
//out class
public com.java.oom.Outer();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_5
6: putfield #7 // Field outerField:I
9: return
public com.java.oom.Outer$Inner getInnter();
Code:
0: new #13 // class com/java/oom/Outer$Inner
3: dup
4: aload_0
//这行, invokespecial需要传入 (Lcom/java/oom/Outer;)V来执行init函数。
5: invokespecial #15 // Method com/java/oom/Outer$Inner."<init>":(Lcom/java/oom/Outer;)V
8: areturn
public static void main(java.lang.String[]);
Code:
0: new #8 // class com/java/oom/Outer
3: dup
4: invokespecial #18 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #19 // Method getInnter:()Lcom/java/oom/Outer$Inner;
12: astore_2
13: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_2
17: invokevirtual #29 // Method com/java/oom/Outer$Inner.InnerMethod:()I
20: invokevirtual #33 // Method java/io/PrintStream.println:(I)V
23: return
}
综合反编译的结果和字节码,所以我们说,Java内部类会在init阶段,把外部类的对象传进去,然后在构造函数里面赋值为内部类的成员变量。
内部类不使用外部类的成员,是否会持有外部类的对象
那我们看一下,假设内部类不引用外部类的成员变量,会是什么样呢?
我们对刚才的Outer进行小范围的修改:
package com.java.oom;
import java.util.concurrent.atomic.AtomicInteger;
public class Outer {
class Inner{
public int InnerMethod(){
//调用外部类的成员变量
return 10;
}
}
public Inner getInnter(){
return new Inner();
}
public static void main(String[] args) {
Outer outer = new Outer();
Inner innter = outer.getInnter();
System.out.println(innter.InnerMethod());
}
}
接下来我们编译一下
javac Outer.java
java Outer
执行结果:
10
看一下生成的Outer.class:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.java.oom;
public class Outer {
public Outer() {
}
public Outer.Inner getInnter() {
return new Outer.Inner();
}
public static void main(String[] var0) {
Outer var1 = new Outer();
Outer.Inner var2 = var1.getInnter();
System.out.println(var2.InnerMethod());
}
class Inner {
Inner() {
}
public int InnerMethod() {
return 10;
}
}
}
然后看一下Inner.class:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.java.oom;
class Outer$Inner {
Outer$Inner(Outer var1) {
this.this$0 = var1;
}
public int InnerMethod() {
return 10;
}
}
这里看到还是有引用的,但是反编译出来的不一定是最终的,我们来看看字节码:
警告: 二进制文件Outer包含com.java.oom.Outer
Compiled from "Outer.java"
public class com.java.oom.Outer {
public com.java.oom.Outer();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public com.java.oom.Outer$Inner getInnter();
Code:
0: new #2 // class com/java/oom/Outer$Inner
3: dup
4: aload_0
5: invokespecial #3 // Method com/java/oom/Outer$Inner."<init>":(Lcom/java/oom/Outer;)V
8: areturn
public static void main(java.lang.String[]);
Code:
0: new #4 // class com/java/oom/Outer
3: dup
4: invokespecial #5 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #6 // Method getInnter:()Lcom/java/oom/Outer$Inner;
12: astore_2
13: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_2
17: invokevirtual #8 // Method com/java/oom/Outer$Inner.InnerMethod:()I
20: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
23: return
}
我们看看getInner的第五航,这里调用了Inner方法的init方法,这里你可以认为是调用了构造函数,传入的参数是
com/java/oom/Outer;返回值是void。到这里就真的确定在new Inner的时候,传入了Outer对象。那证明内部类无论是使用还是不适用外部类的对象,均会持有外部类的引用。所以开发时需要谨慎的使用匿名内部类,控制好使用的生命周期。还有就是使用静态内部类。都说到这了,我们看一下静态内部类的字节码表现是怎么样的.
静态内部类
我们对之前的类进行稍微的修改,把inner变成static:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.java.oom;
public class Outer {
public Outer() {
}
public Outer.Inner getInnter() {
return new Outer.Inner();
}
public static void main(String[] var0) {
Outer var1 = new Outer();
Outer.Inner var2 = var1.getInnter();
System.out.println(var2.InnerMethod());
}
static class Inner {
Inner() {
}
public int InnerMethod() {
return 10;
}
}
}
package com.java.oom;
class Outer$Inner {
Outer$Inner() {
}
public int InnerMethod() {
return 10;
}
}
我们再来看一下字节码:
警告: 二进制文件Outer包含com.java.oom.Outer
Compiled from "Outer.java"
public class com.java.oom.Outer {
public com.java.oom.Outer();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public com.java.oom.Outer$Inner getInnter();
Code:
0: new #2 // class com/java/oom/Outer$Inner
3: dup
4: invokespecial #3 // Method com/java/oom/Outer$Inner."<init>":()V
7: areturn
public static void main(java.lang.String[]);
Code:
0: new #4 // class com/java/oom/Outer
3: dup
4: invokespecial #5 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #6 // Method getInnter:()Lcom/java/oom/Outer$Inner;
12: astore_2
13: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_2
17: invokevirtual #8 // Method com/java/oom/Outer$Inner.InnerMethod:()I
20: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
23: return
}
从字节码可以看出,调用getInner方法的invokespecial函数,执行Init的参数和返回值是
"<init>":()V //()表示没有参数
这里非常需要注意的一个点是,静态内部类的生命周期是很长的,如果持有了短生命周期的东西,会造成内存泄漏,通常的解法是使用弱引用来持有短生命周期的对象。以AndroidHandler为例子:
package com.jhzl.memoryleak;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.lang.ref.WeakReference;
public class MainActivity extends AppCompatActivity {
static class MainHandler extends Handler{
WeakReference<Activity> activityWeakReference = null;
MainHandler(Activity activity){
activityWeakReference = new WeakReference(activity);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (activityWeakReference == null){
return;
}
Activity activity = activityWeakReference.get();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MainHandler mainHandler = new MainHandler(this);
}
}
这里就使用到了Activity对象,有可能生命周期就比Handler短,所以需要用弱引用包裹一下,防止泄露。