[Java] 内部类 (inner class) 为何可以访问宿主类的成员 (第一部分)

93 阅读6分钟

Java 的内部类 (inner class) 为何可以访问宿主类的成员(第一部分)

这个问题可以分为 2 部分 ⬇️

  1. 内部类如果可以访问宿主类的成员,那么照理说,应该要先获取宿主类的实例。否则巧妇难为无米之炊。
  2. 内部类访问宿主类的 private 成员时,为什么没有权限问题。

本文只讨论上面提到的第 1 部分。

要点

内部类可以分为以下 3 种类型

  1. Member Class (成员内部类)
  2. Local Class (局部内部类)
  3. Anonymous Class (匿名内部类)

对上述 3 种类型而言,当内部类的实例(简称为 innerinner)被创建时,宿主类的一个实例(简称为 outerouter)会被作为参数传递给内部类的构造函数。这样,在需要的时候,innerinner 就可以通过 outerouter 来访问宿主类的成员了。

正文

根据 Java Language Specification 中的 8.1.3. Inner Classes and Enclosing Instances 小节 的描述,一共有 3 种类型的内部类(inner class) ⬇️

image.png

3 种类型列举如下,我们分别来看。

  1. Member Class (成员内部类)
  2. Local Class (局部内部类)
  3. Anonymous Class (匿名内部类)

Case 1: Member Class (成员内部类)

我们用以下代码来进行探讨。 (请将代码保存为 MyOuter.java)

import java.lang.reflect.Field;

public class MyOuter {
    private int a;

    public class MyInner {
        private final int b = a;
    }

    public static void main(String[] args) {
        for (Field field : MyInner.class.getDeclaredFields()) {
            System.out.printf("field name is: [%s]%n", field.getName());
            System.out.printf("field type is: [%s]%n", field.getType().getName());
            System.out.printf("field is synthetic: [%s]%n", field.isSynthetic());
            System.out.println();
        }
    }

    public MyInner build() {
        return new MyInner();
    }
}

下面的命令可以编译 MyOuter.java ⬇️

javac -g -parameters MyOuter.java

编译之后,会生成如下两个 class 文件 ⬇️

MyOuter.class
MyOuter$MyInner.class

可以借助 Intellij IDEA 来查看 MyOuter$MyInner.class 的内容 ⬇️

image.png

由此可见,当 MyInner 的实例被创建时,一个 MyOuter 的实例会作为参数传递给 MyInner 的构造函数。

用如下命令,我们可以查看 MyOuter.class 文件的详细内容 ⬇️

javap -v -p 'MyOuter'

build() 方法相关的部分展示如下(其他部分已略去)⬇️

  public MyOuter$MyInner build();
    descriptor: ()LMyOuter$MyInner;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: new           #7                  // class MyOuter$MyInner
         3: dup
         4: aload_0
         5: invokespecial #57                 // Method MyOuter$MyInner."<init>":(LMyOuter;)V
         8: areturn
      LineNumberTable:
        line 20: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LMyOuter;

可以认为对应的 java 代码是这样的 ⬇️

// 下方的 java 代码是我手动转化的,不保证绝对准确,仅供参考。

public MyOuter$MyInner build() {
  return new MyOuter$MyInner(this);
}
一个特殊的例子

下面的例子来自 《Java 编程思想》一书的 10.7.2 从多层嵌套类中访问外部类的成员 一节 (代码链接)

//: innerclasses/MultiNestingAccess.java
// Nested classes can access all members of all
// levels of the classes they are nested within.

class MNA {
  private void f() {}
  class A {
    private void g() {}
    public class B {
      void h() {
        g();
        f();
      }
    }
  }
}	

public class MultiNestingAccess {
  public static void main(String[] args) {
    MNA mna = new MNA();
    MNA.A mnaa = mna.new A();
    MNA.A.B mnaab = mnaa.new B();
    mnaab.h();
  }
} ///:~

将上述代码保存为 MultiNestingAccess.java 并用下面的命令进行编译。

javac -g -parameters MultiNestingAccess.java

编译之后,会生成如下的 4 个 class 文件

MNA.class
MNA$A.class
MNA$A$B.class
MultiNestingAccess.class

我们用下面的命令来查看 B 类的结构。

javap -v -p 'MNA$A$B'

完整的内容有点长,我把部分结果附在下面。

  final MNA$A this$1;
    descriptor: LMNA$A;
    flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC

  public MNA$A$B(MNA$A);
    descriptor: (LMNA$A;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$1:LMNA$A;
         5: aload_0
         6: invokespecial #7                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LMNA$A$B;
            0      10     1 this$1   LMNA$A;
    MethodParameters:
      Name                           Flags
      this$1                         final mandated

  void h();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field this$1:LMNA$A;
         4: invokevirtual #13                 // Method MNA$A.g:()V
         7: aload_0
         8: getfield      #1                  // Field this$1:LMNA$A;
        11: getfield      #18                 // Field MNA$A.this$0:LMNA;
        14: invokevirtual #22                 // Method MNA.f:()V
        17: return
      LineNumberTable:
        line 11: 0
        line 12: 7
        line 13: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  this   LMNA$A$B;

根据这些结果,我们可以手动写出对应的 java 代码 ⬇️

// 以下代码是我自己手工转化的,不保证绝对准确,仅供参考

public class MNA$A$B {
  final MNA$A this$1;
  
  public MNA$A$B(final MNA$A this$1) {
    this.this$1 = this$1;
    super();
  }
  
  void h() {
    this.this$1.g();
    this.this$1.this$0.f();
  }
}

可见,在创建 B 类的实例时,一个 A 类的实例会作为参数提供给 B 类的构造函数。 类似地,在创建 A 类的实例时,一个 MNA 类的实例会作为参数提供给 MNA 类的构造函数 (对应的分析略去,读者朋友可以自行分析对应的 class 文件)。

所以在 B 类的 h() 方法中,

  1. B 的实例通过访问 A 类型的 this$1 字段,可以调用 A 类上定义的 g() 方法
  2. B 的实例通过访问 A 类型的 this$1 字段,然后再通过 this$1 访问 MNA 类型的 this$0 字段,这样就可以调用 MNA 上定义的 f() 方法
小总结

在调用 成员内部类 的构造函数时,其中一个参数会是宿主类的实例(简称为 outerouter)。于是, 成员内部类 的实例就可以通过 outerouter 来访问宿主类的成员了。

Case 2: Local Class (局部内部类)

请将以下代码保存为 MyOuter2.java ⬇️

public class MyOuter2 {
    private int a;

    void f(int b) {
        class LocalClass {
            LocalClass(int b) {
                System.out.println(a + b);
            }
        }
        
        LocalClass lc = new LocalClass(42);
    }
}

下方的命令可以编译 MyOuter2.java ⬇️

javac -g -parameters MyOuter2.java

编译之后,会生成如下 2class 文件 ⬇️

MyOuter2.class            
MyOuter2$1LocalClass.class

用以下命令可以查看 MyOuter2.class 文件的详细内容 ⬇️

javap -v -p 'MyOuter2'

部分结果展示如下 ⬇️

  private int a;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  public MyOuter2();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LMyOuter2;

  void f(int);
    descriptor: (I)V
    flags: (0x0000)
    Code:
      stack=4, locals=3, args_size=2
         0: new           #7                  // class MyOuter2$1LocalClass
         3: dup
         4: aload_0
         5: bipush        42
         7: invokespecial #9                  // Method MyOuter2$1LocalClass."<init>":(LMyOuter2;I)V
        10: astore_2
        11: return
      LineNumberTable:
        line 11: 0
        line 12: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   LMyOuter2;
            0      12     1     b   I
           11       1     2    lc   LMyOuter2$1LocalClass;
    MethodParameters:
      Name                           Flags
      b

可以认为对应的 java 代码是这样的 ⬇️

// 以下代码是我自己手工转化的,不保证绝对准确,仅供参考

public class MyOuter2 {
    private int a;
    
    public MyOuter2() {
        super();
    }
    
    void f(int b) {
        MyOuter2$1LocalClass lc = new MyOuter2$1LocalClass(this, 42);
    }
}

用以下命令可以查看 MyOuter2$1LocalClass.class 的具体内容 ⬇️

javap -v -p 'MyOuter2$1LocalClass'

部分结果展示如下 ⬇️

  MyOuter2$1LocalClass(MyOuter2, int);
    descriptor: (LMyOuter2;I)V
    flags: (0x0000)
    Code:
      stack=3, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: aload_1
         8: getfield      #13                 // Field MyOuter2.a:I
        11: iload_2
        12: iadd
        13: invokevirtual #19                 // Method java/io/PrintStream.println:(I)V
        16: return
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 8: 16
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0  this   LMyOuter2$1LocalClass;
            0      17     1 this$0   LMyOuter2;
            0      17     2     b   I
    MethodParameters:
      Name                           Flags
      this$0                         final mandated
      b

可以认为对应的 java 代码是这样的 ⬇️

// 以下代码是我自己手工转化的,不保证绝对准确,仅供参考

class MyOuter2$1LocalClass {
    MyOuter2$1LocalClass(final MyOuter2 this$0, int b) {
        super();
        System.out.println(this$0.a + b);
    }
}
小总结

在调用 局部内部类 的构造函数时,其中一个参数会是宿主类的实例(简称为 outerouter)。于是, 局部内部类 的实例就可以通过 outerouter 来访问宿主类的成员了。

Case 3: Anonymous Class (匿名内部类)

下方代码的 f(int) 方法里会生成一个匿名内部类,这个匿名内部类 implementRunnable 接口。在这个匿名内部类的 run() 方法中,会用到 ab

public class MyOuter3 {
    private int a;

    void f(int b) {
      Runnable r = new Runnable() {
        @Override
        public void run() {
          System.out.println(a + b);
        }
      };
    }
}

请将代码保存为 MyOuter3.java,用以下命令可以编译 MyOuter3.java ⬇️

javac -g -parameters MyOuter3.java

编译后,会生成如下 2class 文件 ⬇️

MyOuter3.class
MyOuter3$1.class

用以下命令,我们可以查看 MyOuter3$1.class 的详细内容 ⬇️

javap -v -p 'MyOuter3$1'

部分结果展示如下 ⬇️

  final int val$b;
    descriptor: I
    flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC

  final MyOuter3 this$0;
    descriptor: LMyOuter3;
    flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC

  MyOuter3$1();
    descriptor: (LMyOuter3;I)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: iload_2
         2: putfield      #1                  // Field val$b:I
         5: aload_0
         6: aload_1
         7: putfield      #7                  // Field this$0:LMyOuter3;
        10: aload_0
        11: invokespecial #11                 // Method java/lang/Object."<init>":()V
        14: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LMyOuter3$1;
            0      15     1 this$0   LMyOuter3;
    MethodParameters:
      Name                           Flags
      this$0                         final mandated
      val$b                          final synthetic
    Signature: #16                          // ()V

  public void run();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #7                  // Field this$0:LMyOuter3;
         7: getfield      #23                 // Field MyOuter3.a:I
        10: aload_0
        11: getfield      #1                  // Field val$b:I
        14: iadd
        15: invokevirtual #28                 // Method java/io/PrintStream.println:(I)V
        18: return
      LineNumberTable:
        line 8: 0
        line 9: 18
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  this   LMyOuter3$1;

可以认为对应的 java 代码是这样的 ⬇️

// 以下代码是我手工转化的,不保证绝对准确,仅供参考

class MyOuter3$1 implements java.lang.Runnable {
  final int val$b;
  final MyOuter3 this$0;
  
  MyOuter3$1(final MyOuter3 this$0, final int val$b) {
    this.val$b = val$b;
    this.this$0 = this$0;
    super();
  }
  
  public void run() {
    System.out.println(this.this$0.a + this.val$b);
  }
}

用如下命令,可以查看 MyOuter3.class 的详细内容 ⬇️

javap -v -p 'MyOuter3'

部分结果展示如下 ⬇️

  private int a;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  public MyOuter3();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LMyOuter3;

  void f(int);
    descriptor: (I)V
    flags: (0x0000)
    Code:
      stack=4, locals=3, args_size=2
         0: new           #7                  // class MyOuter3$1
         3: dup
         4: aload_0
         5: iload_1
         6: invokespecial #9                  // Method MyOuter3$1."<init>":(LMyOuter3;I)V
         9: astore_2
        10: return
      LineNumberTable:
        line 5: 0
        line 11: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LMyOuter3;
            0      11     1     b   I
           10       1     2     r   Ljava/lang/Runnable;
    MethodParameters:
      Name                           Flags
      b

可以认为对应的 java 代码是这样的 ⬇️

// 以下代码是我手工转化的,不保证绝对准确,仅供参考

public class MyOuter3 {
  private int a;
  
  public MyOuter3() {
    super();
  }
  
  void f(int b) {
    Runnable r = new MyOuter3$1(this, b);
  }
}
小总结

在调用 匿名内部类 的构造函数时,其中一个参数会是宿主类的实例(简称为 outerouter)。于是,匿名内部类 就可以通过 outerouter 来访问宿主类的成员了。

其他

如果本文对您有帮助,可以继续阅读 [Java] 内部类 (inner class) 为何可以访问宿主类的成员 (第二部分)