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

37 阅读3分钟

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

背景

[Java] 内部类 (inner class) 为何可以访问宿主类的成员 (第二部分) 中提到,宿主类的 class 文件中有 NestMembers 属性,内部类的 class 文件中有 NestHost 属性,虚拟机通过查看这两个属性,就可以知道宿主类和内部类的对应关系,从而允许它们访问对方的成员。

NestMembers 属性和 NestHost 属性都是 Java 11 才开始出现的属性(Java Virtual Machine Specification 中的 4.7. Attributes 有如下表格),那么在更早的版本(例如 Java 10)中,宿主类和内部类之间这种特殊关系要如何表示呢?

image.png

结论

在 Java 11 之前,javac 编译器会在宿主类/内部类中添加合成方法,使得宿主类和内部类可以间接地访问对方的 private 成员。

代码

我们用以下 java 代码来进行探索。请将代码保存为 MyOuter5.java

public class MyOuter5 {
  private int a;

  // a member class
  class MemberClass {
    private int b = a;
  }
  
  void f() {
    MemberClass mc = new MemberClass();
    int temp = mc.b;
  }
}

以下命令可以编译 MyOuter5.java。(为了查看 Java 11 之前的情况,这里用 --release 10 来指定对应的版本为 Java 10,读者朋友也可以使用早于 Java 11 的其他版本)

javac --release 10 -g -parameters MyOuter5.java

编译后,会得到如下两个 class 文件

MyOuter5.class
MyOuter5$MemberClass.class

用如下命令可以查看 MyOuter5.class 的内容。

javap -v -p 'MyOuter5'

不难验证,里面确实没有 NestMembers 属性了。 完整的结果有点长,我把与字段/方法相关部分粘贴如下(常量池等部分已略去)。

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

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

  void f();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=3, locals=3, args_size=1
         0: new           #13                 // class MyOuter5$MemberClass
         3: dup
         4: aload_0
         5: invokespecial #15                 // Method MyOuter5$MemberClass."<init>":(LMyOuter5;)V
         8: astore_1
         9: aload_1
        10: invokestatic  #18                 // Method MyOuter5$MemberClass.access$100:(LMyOuter5$MemberClass;)I
        13: istore_2
        14: return
      LineNumberTable:
        line 10: 0
        line 11: 9
        line 12: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LMyOuter5;
            9       6     1    mc   LMyOuter5$MemberClass;
           14       1     2  temp   I

  static int access$000(MyOuter5);
    descriptor: (LMyOuter5;)I
    flags: (0x1008) ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0    x0   LMyOuter5;
}

从上述结果可以反推出对应的 java 代码应该是这样的⬇️

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

public class MyOuter5 {
  private int a;

  public MyOuter5() {
    super();
  }
  
  void f() {
    MemberClass mc = new MyOuter5$MemberClass(this);
    int temp = MyOuter5$MemberClass.access$100(mc); // ⬅️ 这里调用内部类的静态方法(该静态方法不是 private 的,所以宿主类可以正常调用),从而间接访问内部类的 private 字段
  }
  
  // ⬇️ 这是编译器合成的静态方法,用于返回宿主类中 a 字段的值。内部类对象通过调用这个静态方法就可以访问宿主类的 a 字段了。
  static int access$000(MyOuter5 x0) {
    return x0.a;
  }
}

让我们用如下命令查看 MyOuter5$MemberClass.class 的内容。

javap -v -p 'MyOuter5$MemberClass'

不难验证,里面确实没有 NestHost 属性了。 以下是与字段/方法相关部分(常量池等部分已略去)。

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

  final MyOuter5 this$0;
    descriptor: LMyOuter5;
    flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC

  MyOuter5$MemberClass(MyOuter5);
    descriptor: (LMyOuter5;)V
    flags: (0x0000)
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #7                  // Field this$0:LMyOuter5;
         5: aload_0
         6: invokespecial #11                 // Method java/lang/Object."<init>":()V
         9: aload_0
        10: aload_0
        11: getfield      #7                  // Field this$0:LMyOuter5;
        14: invokestatic  #17                 // Method MyOuter5.access$000:(LMyOuter5;)I
        17: putfield      #1                  // Field b:I
        20: return
      LineNumberTable:
        line 5: 0
        line 6: 9
        line 5: 20
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      21     0  this   LMyOuter5$MemberClass;
            0      21     1 this$0   LMyOuter5;
    MethodParameters:
      Name                           Flags
      this$0                         final mandated

  static int access$100(MyOuter5$MemberClass);
    descriptor: (LMyOuter5$MemberClass;)I
    flags: (0x1008) ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field b:I
         4: ireturn
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0    x0   LMyOuter5$MemberClass;
}

从上述结果可以反推出对应的 java 代码应该是这样的⬇️

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

class MyOuter5$MemberClass {
  private int b;
  // ⬇️ this$0 是一个合成字段
  final MyOuter5 this$0;
  
  // 在内部类的构造函数中会为合成字段 this$0 赋值
  MyOuter5$MemberClass(MyOuter5 this$0) {
    this.this$0 = this$0;
    super();
    this.b = MyOuter5.access$000(this.this$0); // ⬅️ 这里通过调用宿主类中的静态合成方法,从而间接访问宿主类的 a 字段。
  }
  
  // ⬇️ 这是编译器合成的静态方法,用于返回内部类中 b 字段的值。宿主类对象通过调用这个静态方法就可以访问内部类的 b 字段。
  static int access$100(MyOuter5$MemberClass x0) {
    return x0.b;
  }
}

参考资料

  1. [Baeldung] Java Nest Based Access Control

  2. Java Virtual Machine Specification 中的 4.7. Attributes