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

118 阅读4分钟

内部类 (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 成员。

我们对访问对方 private 字段/方法的情况进行总结 ⬇️

如何处理
宿主类 HostHost 的实例访问内部类 InnerInner 的 private 字段内部类 InnerInner 中合成 access$xxx(Inner) 方法,用于提供内部类的 private 字段
宿主类 HostHost 的实例调用内部类 InnerInner 的 private 方法内部类 InnerInner 中合成 access$xxx(Inner) 方法,并在这个方法内调用 InnerInnerprivate 方法
内部类 InnerInner 的实例访问宿主类 HostHost 的 private 字段宿主类 HostHost 中合成 access$xxx(Host) 方法,用于提供宿主类的 private 字段
内部类 InnerInner 的实例调用宿主类 HostHost 的 private 方法宿主类 HostHost 中合成 access$xxx(Host) 方法,并在这个方法内调用 HostHostprivate 方法

代码

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

public class MyOuter5 {
  private int a;
  private String f1() {
    return "f1() in MyOuter5 is called";
  }

  // a member class
  class MemberClass {
    private int b = a;
    private String f2() {
      return "f2() in MemberClass is called";
    }
    private String f3() {
      return MyOuter5.this.f1();
    }
  }
  
  void f() {
    MemberClass mc = new MemberClass();
    int temp = mc.b;
    mc.f2();
  }
}

以下命令可以编译 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 #11                 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LMyOuter5;

  private java.lang.String f1();
    descriptor: ()Ljava/lang/String;
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #17                 // String f1() in MyOuter5 is called
         2: areturn
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   LMyOuter5;

  void f();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=3, locals=3, args_size=1
         0: new           #19                 // class MyOuter5$MemberClass
         3: dup
         4: aload_0
         5: invokespecial #21                 // Method MyOuter5$MemberClass."<init>":(LMyOuter5;)V
         8: astore_1
         9: aload_1
        10: invokestatic  #24                 // Method MyOuter5$MemberClass.access$200:(LMyOuter5$MemberClass;)I
        13: istore_2
        14: aload_1
        15: invokestatic  #28                 // Method MyOuter5$MemberClass.access$300:(LMyOuter5$MemberClass;)Ljava/lang/String;
        18: pop
        19: return
      LineNumberTable:
        line 19: 0
        line 20: 9
        line 21: 14
        line 22: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   LMyOuter5;
            9      11     1    mc   LMyOuter5$MemberClass;
           14       6     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      #7                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0    x0   LMyOuter5;

  static java.lang.String access$100(MyOuter5);
    descriptor: (LMyOuter5;)Ljava/lang/String;
    flags: (0x1008) ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method f1:()Ljava/lang/String;
         4: areturn
      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();
  }
  
  private java.lang.String f1() {
    return "String f1() in MyOuter5 is called";
  }
  
  void f() {
    MemberClass mc = new MyOuter5$MemberClass(this);
    int temp = MyOuter5$MemberClass.access$200(mc); // ⬅️ 这里调用内部类的静态方法 access$200(该静态方法是 package access 的,所以宿主类可以正常调用),从而间接访问内部类的 private 字段
    MyOuter5$MemberClass.access$300(mc); // ⬅️ 这里调用内部类的静态方法 access$300(该静态方法是 package access 的,所以宿主类可以正常调用),从而间接调用内部类的 f2 方法
  }
  
  // ⬇️ 这是编译器合成的静态方法,用于返回宿主类中 a 字段的值。内部类对象通过调用这个静态方法就可以访问宿主类的 a 字段了。
  static int access$000(MyOuter5 x0) {
    return x0.a;
  }
  
  static java.lang.String access$100(MyOuter5 x0) {
    return x0.f1();
  }
}

让我们用如下命令查看 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      #11                 // Field this$0:LMyOuter5;
         5: aload_0
         6: invokespecial #15                 // Method java/lang/Object."<init>":()V
         9: aload_0
        10: aload_0
        11: getfield      #11                 // Field this$0:LMyOuter5;
        14: invokestatic  #21                 // Method MyOuter5.access$000:(LMyOuter5;)I
        17: putfield      #7                  // Field b:I
        20: return
      LineNumberTable:
        line 8: 0
        line 9: 9
        line 8: 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

  private java.lang.String f2();
    descriptor: ()Ljava/lang/String;
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #27                 // String f2() in MemberClass is called
         2: areturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   LMyOuter5$MemberClass;

  private java.lang.String f3();
    descriptor: ()Ljava/lang/String;
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #11                 // Field this$0:LMyOuter5;
         4: invokestatic  #29                 // Method MyOuter5.access$100:(LMyOuter5;)Ljava/lang/String;
         7: areturn
      LineNumberTable:
        line 14: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   LMyOuter5$MemberClass;

  static int access$200(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      #7                  // Field b:I
         4: ireturn
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0    x0   LMyOuter5$MemberClass;

  static java.lang.String access$300(MyOuter5$MemberClass);
    descriptor: (LMyOuter5$MemberClass;)Ljava/lang/String;
    flags: (0x1008) ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method f2:()Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 8: 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 字段。
  }
  
  private java.lang.String f2() {
    return "String f2() in MemberClass is called";
  }
  
  private java.lang.String f3() {
    return MyOuter5.access$100(this.this$0); // ⬅️ 这里通过调用宿主类中的静态合成方法,从而间接调用宿主类的 f1 方法。
  }
  
  // ⬇️ access$200 是编译器合成的静态方法,用于返回内部类中 b 字段的值。宿主类对象通过调用这个静态方法就可以访问内部类的 b 字段。
  static int access$200(MyOuter5$MemberClass x0) {
    return x0.b;
  }
  
  // ⬇️ access$300 是编译器合成的静态方法。宿主类对象通过调用这个静态方法就可以间接调用内部类的 f2 方法。
  static java.lang.String access$300(MyOuter5$MemberClass x0) {
    return x0.f2();
  }
}

解释

结合上面的 java 代码,我们来看一下宿主类和内部类是如何互相访问对方的 private 成员的。 一共有 4 种情况

  1. 宿主类访问内部类的 private 字段
  2. 宿主类调用内部类的 private 方法
  3. 内部类访问宿主类的 private 字段
  4. 内部类调用宿主类的 private 方法
1. 宿主类访问内部类的 private 字段 (例子中是读取内部类的 b 字段)

MyOuter5.java 的第 20 行是 int temp = mc.b;,从 class 文件来看,实际上执行的逻辑是 int temp = MyOuter5$MemberClass.access$200(mc);

用时序图表示是这样的 ⬇️

sequenceDiagram
MyOuter5 instance->>MyOuter5$MemberClass: access$200(mc)
MyOuter5$MemberClass-->>MyOuter5 instance: return mc.b
2. 宿主类调用内部类的 private 方法 (例子中是调用内部类的 f2 方法)

MyOuter5.java 的第 21 行是 mc.f2();,从 class 文件来看,实际执行的逻辑是 MyOuter5$MemberClass.access$300(mc);

用时序图表示是这样的 ⬇️

sequenceDiagram
MyOuter5 instance->>MyOuter5$MemberClass: access$300(mc)
MyOuter5$MemberClass->>mc: f2()
mc-->>MyOuter5$MemberClass: return "f2() in MemberClass is called"
MyOuter5$MemberClass-->>MyOuter5 instance: return "f2() in MemberClass is called"
3. 内部类访问宿主类的 private 字段 (例子中是读取 MyOuter5a 字段)

MyOuter5.java 的第 9 行是 private int b = a; ,从 class 文件来看,实际执行的逻辑是 this.b = MyOuter5.access$000(this.this$0);

用时序图表示是这样的 ⬇️

sequenceDiagram
inner class instance->>MyOuter5: access$000(this.this$0)
MyOuter5-->>inner class instance: return this$0.a
4. 内部类调用宿主类的 private 方法 (例子中是调用 MyOuter5f1 方法)

MyOuter5.java 的第 14 行是 return MyOuter5.this.f1();,从 class 文件来看,实际执行的逻辑是 return MyOuter5.access$100(this.this$0);

用时序图表示是这样的 ⬇️

sequenceDiagram
inner class instance->>MyOuter5: access$100(this.this$0)
MyOuter5->>this$0: f1()
this$0-->>MyOuter5: return "f1() in MyOuter5 is called"
MyOuter5-->>inner class instance: return "f1() in MyOuter5 is called"

参考资料

  1. [Baeldung] Java Nest Based Access Control
  2. Java Virtual Machine Specification 中的 4.7. Attributes
  3. JEP 181: Nest-Based Access Control