内部类 (inner class) 为何可以访问宿主类的成员 (第三部分)
背景
[Java] 内部类 (inner class) 为何可以访问宿主类的成员 (第二部分) 中提到,宿主类的 class 文件中有 NestMembers 属性,内部类的 class 文件中有 NestHost 属性,虚拟机通过查看这两个属性,就可以知道宿主类和内部类的对应关系,从而允许它们访问对方的成员。
但 NestMembers 属性和 NestHost 属性都是 Java 11 才开始出现的属性(Java Virtual Machine Specification 中的 4.7. Attributes 有如下表格),那么在更早的版本(例如 Java 10)中,宿主类和内部类之间的关系如何表示呢?
结论
在 Java 11 之前,javac 编译器会在宿主类/内部类中添加合成方法,使得宿主类和内部类可以间接地访问对方的 private 成员。
我们对访问对方 private 字段/方法的情况进行总结 ⬇️
| 如何处理 | |
|---|---|
宿主类 的实例访问内部类 的 private 字段 | 内部类 中合成 access$xxx(Inner) 方法,用于提供内部类的 private 字段 |
宿主类 的实例调用内部类 的 private 方法 | 内部类 中合成 access$xxx(Inner) 方法,并在这个方法内调用 的 private 方法 |
内部类 的实例访问宿主类 的 private 字段 | 宿主类 中合成 access$xxx(Host) 方法,用于提供宿主类的 private 字段 |
内部类 的实例调用宿主类 的 private 方法 | 宿主类 中合成 access$xxx(Host) 方法,并在这个方法内调用 的 private 方法 |
代码
我们用以下 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 种情况
- 宿主类访问内部类的
private字段 - 宿主类调用内部类的
private方法 - 内部类访问宿主类的
private字段 - 内部类调用宿主类的
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 字段 (例子中是读取 MyOuter5 的 a 字段)
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 方法 (例子中是调用 MyOuter5 的 f1 方法)
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"