Java 的内部类 (inner class) 为何可以访问宿主类的成员(第一部分)
这个问题可以分为 2 部分 ⬇️
- 内部类如果可以访问宿主类的成员,那么照理说,应该要先获取宿主类的实例。否则巧妇难为无米之炊。
- 内部类访问宿主类的
private成员时,为什么没有权限问题。
本文只讨论上面提到的第 1 部分。
要点
内部类可以分为以下 3 种类型
- Member Class (成员内部类)
- Local Class (局部内部类)
- Anonymous Class (匿名内部类)
对上述 3 种类型而言,当内部类的实例(简称为 )被创建时,宿主类的一个实例(简称为 )会被作为参数传递给内部类的构造函数。这样,在需要的时候, 就可以通过 来访问宿主类的成员了。
正文
根据 Java Language Specification 中的 8.1.3. Inner Classes and Enclosing Instances 小节 的描述,一共有 3 种类型的内部类(inner class) ⬇️
这 3 种类型列举如下,我们分别来看。
- Member Class (成员内部类)
- Local Class (局部内部类)
- 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 的内容 ⬇️
由此可见,当 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() 方法中,
B的实例通过访问A类型的this$1字段,可以调用A类上定义的g()方法B的实例通过访问A类型的this$1字段,然后再通过this$1访问MNA类型的this$0字段,这样就可以调用MNA上定义的f()方法
小总结
在调用 成员内部类 的构造函数时,其中一个参数会是宿主类的实例(简称为 )。于是, 成员内部类 的实例就可以通过 来访问宿主类的成员了。
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
编译之后,会生成如下 2 个 class 文件 ⬇️
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);
}
}
小总结
在调用 局部内部类 的构造函数时,其中一个参数会是宿主类的实例(简称为 )。于是, 局部内部类 的实例就可以通过 来访问宿主类的成员了。
Case 3: Anonymous Class (匿名内部类)
下方代码的 f(int) 方法里会生成一个匿名内部类,这个匿名内部类 implement 了 Runnable 接口。在这个匿名内部类的 run() 方法中,会用到 a 和 b。
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
编译后,会生成如下 2 个 class 文件 ⬇️
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);
}
}
小总结
在调用 匿名内部类 的构造函数时,其中一个参数会是宿主类的实例(简称为 )。于是,匿名内部类 就可以通过 来访问宿主类的成员了。
其他
如果本文对您有帮助,可以继续阅读 [Java] 内部类 (inner class) 为何可以访问宿主类的成员 (第二部分)。