Java内部类的这件小事儿

316 阅读3分钟

面试连环炮

朋友小白近期面试遇到了一个爱打连环炮的面试官,跑来向我吐槽,以下是面试过程中的一部分对话(对话有点长请耐心看完)。

面试官: 请问你在开发过程中对内存这块儿是如何进行优化的?

小白: 首先排查应用是否存在内存泄露问题,然后 bla bla bla......

面试官: 你是怎么排查应用存在内存泄漏的?

小白: 借助LeakCanary以及As自带的Profiler工具进行排查的。

面试官: 那你给我说说LeakCanary是如何发现内存泄漏的?

小白: bla bla bla......

面试官: 哪些情况容易导致内存泄漏?

小白: 1、使用Handler以非静态内部类或匿名内部类的形式,这时候内部类会持有外部类的引用从而容易导致内存泄漏;2、持有静态的Context(Activity)引用;3、不正确的单例模式,比如单例持有Activity;4、bla bla bla......

面试官: (重点来了)Java规范明确规定了private修饰的成员只能本类才可以访问,但是你在写Java内部类的时候,内部类却是可以访问到外部类的private成员的,而且编译器也是会通过的,这是为什么呢?

小白: 因为内部类也是外部类的一个成员,所以成员之间可以访问。

面试官: 你确定吗?内部类可是会被编译成单独的一个类的哦,例如:Outter$Inner。

小白: 嗯...嗯...嗯...那我就不清楚了。


面试到这里基本这一块儿就告一段落了,后面小白下去了专门去对面试官最后的提问进行了求解。

答疑解惑

为何内部类能访问外部内的private属性,或是外部类也能访问内部类的private属性,Java规范中不是规定了private修饰的属性只能本类才可以访问的吗?例如下面的代码,编译器是通过的:

public class Outter {
    private int outterId;

    public void testOutter(){
    // innerId:外部类访问内部类private属性
        int id = new Inner().innerId;
        System.out.println(id);
    }

    public class Inner{
        private int innerId;
        public void testInner() {
        // outterId:内部类访问外部类private属性
            outterId = 3;
            System.out.println(outterId);
        }
    }
}

光看这个类显然看不出来个所以然,咋办?不管三七二十一还是先看看编译后的class文件再说吧,我们首先定位到Outter的类路径,然后在Terminal中执行javac命令,编译生成相应的class文件

//javac命令
javac Outter.java

生成了Outter.class

public class Outter {
    private int outterId;

    public Outter() {
    }

    public void testOutter() {
        int var1 = (new Outter.Inner()).innerId;
        System.out.println(var1);
    }

    public class Inner {
        private int innerId;

        public Inner() {
        }

        public void testInner() {
            Outter.this.outterId = 3;
            System.out.println(Outter.this.outterId);
        }
    }
}

也同时生成了Outter$Inner.class

public class Outter$Inner {
    private int innerId;
    //这里就是内部类持有外部类的引用的原因直接是在构造方法中作为参数传递进来,赋值给了this$0
    public Outter$Inner(Outter var1) {
        this.this$0 = var1;
    }

    public void testInner() {
       //Inner能访问Outter的private属性的关键所在
        Outter.access$102(this.this$0, 3);
        System.out.println(Outter.access$100(this.this$0));
    }
}

这貌似能看出来点东西了,内部类持有外部类的引用原来如此啊,直接是在构造方法中作为参数传递进来,赋值给了this$0。那Outter.access$102(this.this$0, 3)这又是什么鬼?难道这就是Inner能访问Outter的private属性的关键所在?我在Outter中没看到这个access$102方法啊?看来class文件也并不能看到所有问题的答案啊!只能更深一步了,那就反解析出Outter.class对应的汇编指令,看看到底是个什么鬼?
执行javap命令(javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息)

//javap命令
javap Outter.class

执行完javap命令后会得到以下结果:

Compiled from "Outter.java"
public class com.airland.kotlinchapter.Outter {
  public com.airland.kotlinchapter.Outter();
  public void testOutter();
  static int access$102(com.airland.kotlinchapter.Outter, int);
  static int access$100(com.airland.kotlinchapter.Outter);
}

终于看到了,原来真的会生成static的access102方法用来访问private的属性,那access102方法用来访问private的属性,那access100又是什么鬼?我们来具体看看,执行javap -c Outter命令分解方法代码:

Compiled from "Outter.java"
public class com.airland.kotlinchapter.Outter {
  public com.airland.kotlinchapter.Outter();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method java/lang/Object."<init>":()V
       4: return

  public void testOutter();
    Code:
       0: new           #3                  // class com/airland/kotlinchapter/Outter$Inner
       3: dup
       4: aload_0
       5: invokespecial #4                  // Method com/airland/kotlinchapter/Outter$Inner."<init>":(Lcom/airland/kotlinchapter/Outter;)V
       8: invokestatic  #5                  // Method com/airland/kotlinchapter/Outter$Inner.access$000:(Lcom/airland/kotlinchapter/Outter$Inner;)I
      11: istore_1
      12: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      15: iload_1
      16: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
      19: return

  static int access$102(com.airland.kotlinchapter.Outter, int);
    Code:
       0: aload_0
       1: iload_1
       2: dup_x1
       3: putfield      #1                  // Field outterId:I
       6: ireturn

  static int access$100(com.airland.kotlinchapter.Outter);
    Code:
       0: aload_0
       1: getfield      #1                  // Field outterId:I
       4: ireturn
}

终于真相大白了,static int access$102(com.airland.kotlinchapter.Outter, int)方法相当于setter,简单的从下面的putfield也能看出来,而static int access$100(com.airland.kotlinchapter.Outter)相当于getter,同样的从下面的getfield也可以看出,并且它们都返回该属性。同样的外部类访问内部类的private属性也是一样,编译后同样会生成access$xxx的static方法。你同样可以使用javap Outter.Innerjavap -c Outter.Inner去查看相应结果。

总结

虽然Java规范规定了private修饰的成员只能在本类中访问,但是Inner却能访问Outter的private属性,而在Outter中通过new Inner()也能直接访问Inner的private的属性。这看起来好像有点矛盾,事实上却是编译器编译后,给相应的private属性生成access$xxx的static方法,通过access$xxx方法就能实现private属性的访问。
看来遇到问题了还是要多思考,多刨根问底,知其然更要知其所以然。