【码农每日一题】Java 内部类(Part 2)相关面试题

473 阅读2分钟
关注一下嘛,又不让你背锅!

问:Java 中为什么成员内部类可以直接访问外部类的成员?

答:成员内部类可以无条件访问外部类的成员或者方法的原因解释我们可以通过下面例子来说明。

  1. public class OutClass {

  2.    public class InnerClass {

  3.    }

  4. }

我们执行命令 javac OutClass.java 编译会发现得到两个 class 文件,分别为 OutClass.class 和 OutClass$InnerClass.class,所以编译器在进行编译的时候会把成员内部类单独编译成一个字节码文件,我们接着通过 javap [-v] OutClass$InnerClass 看下编译后的成员内部类的字节码,如下:

  1. Compiled from "OutClass.java"

  2. public class OutClass$InnerClass {

  3.  final OutClass this$0;

  4.  public OutClass$InnerClass(OutClass);

  5. }

可以看到编译后的成员内部类中有一个指向外部类对象的引用,且成员内部类编译后构造方法也多了一个指向外部类对象的引用参数,所以说编译器会默认为成员内部类添加了一个指向外部类对象的引用并且在成员内部类构造方法中对其进行赋值操作,因此我们可以在成员内部类中随意访问外部类的成员,同时也说明成员内部类是依赖于外部类的,如果没有创建外部类的对象则也无法创建成员内部类的对象。

问:Java 1.8 之前为什么方法内部类和匿名内部类访问局部变量和形参时必须加 final?

答:在 Java 1.8 以下因为对于普通局部变量或者形参的作用域是方法内,当方法结束时局部变量或者形参就要随之消失,而其匿名内部类或者方法内部类的生命周期又没结束,匿名内部类或者方法内部类如果想继续使用方法的局部变量就需要一些手段,所以 Java 在编译匿名内部类或者方法内部类时就有一个规定来解决生命周期问题,即如果访问的外部类方法的局部变量值在编译期能确定则直接在匿名内部类或者方法内部类里面创建一个常量拷贝,如果访问的外部类方法的局部变量值无法在编译期确定则通过构造器传参的方式来对拷贝进行初始化赋值。由此说明在匿名内部类或者方法内部类中访问的外部类方法的局部变量或者形参是内部类自己的一份拷贝,和外部类方法的局部变量或者形参不是一份,所以如果在匿名内部类或者方法内部类对变量做修改操作就一定会导致数据不一致性(外部类方法的参数不会跟着被修改,引用类型仅是引用,值修改不存在问题),为了杜绝数据不一致性导致的问题 Java 就要求使用 final 来保证,所以必须是 final 的。在 Java 1.8 开始我们可以不加 final 修饰符了,系统会默认添加,Java 将这个功能称为 Effectively final。

上面这段话可以通过下面的例子说明(对于非 final 无法编译通过,所以不再举例),如下:

  1. public class OutClass {

  2.    private int out = 1;

  3.    public void func(final int param) {

  4.        final int in = 2;

  5.        new Thread() {

  6.            @Override

  7.            public void run() {

  8.                out = param;

  9.                out = in;

  10.            }

  11.        }.start();

  12.    }

  13. }

上面类文件在 java 1.8 以下通过 javac 编译后执行 javap -l -v OutClass$1.class 查看匿名内部类的字节码可以发现如下情况:

  1. ......

  2. class OutClass$1 extends java.lang.Thread

  3. ......

  4. {

  5.  //匿名内部类有了自己的 param 属性成员。

  6.  final int val$param;

  7.  ......

  8.  //匿名内部类持有了外部类的引用作为一个属性成员。

  9.  final OutClass this$0;

  10.  ......

  11.  //匿名内部类编译后构造方法自动多了两个参数,一个为外部类引用,一个为 param 参数。

  12.  OutClass$1(OutClass, int);

  13.    ......

  14.  public void run();

  15.    ......

  16.    Code:

  17.      stack=2, locals=1, args_size=1

  18.           //out = param;语句,将匿名内部类自己的 param 属性赋值给外部类的成员 out。

  19.         0: aload_0

  20.         1: getfield      #1    // Field this$0:LOutClass;

  21.         4: aload_0

  22.         5: getfield      #2    // Field val$param:I

  23.         8: invokestatic  #4    // Method OutClass.access$002:(LOutClass;I)I

  24.        11: pop

  25.        //out = in;语句,将匿名内部类常量 2 (in在编译时确定值)赋值给外部类的成员 out。

  26.        12: aload_0

  27.        13: getfield      #1    // Field this$0:LOutClass;

  28.        //将操作数2压栈,因为如果这个变量的值在编译期间可以确定则编译器默认会在

  29.        //匿名内部类或方法内部类的常量池中添加一个内容相等的字面量或直接将相应的

  30.        //字节码嵌入到执行字节码中。

  31.        16: iconst_2

  32.        17: invokestatic  #4    // Method OutClass.access$002:(LOutClass;I)I

  33.        20: pop

  34.        21: return

  35.      ......

  36. }

  37. ......

通过字节码指令我想不用再多解释了吧,上面字节码包含了访问局部变量编译时可确定值和不可确定值的两种情况,自己可以再琢磨下。

嘎然而止,这是续上篇 Part 1 部分的内部类 Part 2 部分,Part 3 部分尽请收看明日推送续集(内部类绝对算的上是面试高频题了,细节很多)~~

老铁们,别嫌短,长了你肯定不会看完的,所以这就是码农每日一题的宗旨(其他历史文章请查看公众号历史记录)~

看完分享一波嘛,和你的小伙伴一起讨论才更加有意思,右上角分享 666~

看个笑话放松一下

从前有个人钓鱼,钓到了只鱿鱼。鱿鱼求他:你放了我吧,别把我烤来吃啊。那个人说:好的,那么我来考问你几个问题吧。鱿鱼很开心说:你考吧你考吧!然后这人就把鱿鱼给烤了..

To read without reflecting is like eating without digesting. 

读书而不思考,等于吃饭而不消化。