Java 语法糖(五): 成员内部类
参考文献
- https://mp.weixin.qq.com/s/xl1ibkfUc00NY8HCa_fcPg (深入理解Java内部类)
- https://www.benf.org/other/cfr/inner-class-fake-friends.html
- https://docs.oracle.com/javase/specs/jls/se14/html/jls-8.html#jls-8.1.3
正文
内部类的种类
https://docs.oracle.com/javase/specs/jls/se14/html/jls-8.html#jls-8.1.3 提到了内部类(inner class
)
An inner class is a nested class that is not explicitly or implicitly declared
static
.
An inner class may be a non-static
member class (§8.5), a local class (§14.3), or an anonymous class (§15.9.5). A member class of an interface is implicitlystatic
(§9.5) so is never considered to be an inner class.
根据这个描述,内部类(inner class
)可以分为如下三种
- 成员(
member
)内部类 - 局部(
local
)内部类 - 匿名(
anonymous
)内部类
我们先来了解 成员内部类
成员内部类
我们用名为 Master.java
的文件来进行探索(其完整内容如下)
/**
* Master 类是外围类, Slave 类是 Master 的成员内部类
*/
public class Master {
private int cnt = 0;
/**
* 生成一个 Slave 的实例, 并对其 someValue 字段赋值
*/
public Slave generateAndAssign(int someValue) {
Slave slave = new Master.Slave();
slave.someValue = someValue;
return slave;
}
private class Slave {
private int id = Master.this.cnt++;
private int someValue;
}
}
在上述程序中,Master
类是外围类, Slave
类是 Master
类的成员内部类。那么会有几个问题
- 成员内部类是如何实现的?
- 成员内部类是如何访问外围类的?
- 外围类是如何访问成员内部类的?
问题一: 成员内部类是如何实现的?
执行如下的命令来编译 Master.java
javac Master.java
就会看到生成了如下 class
文件
Master.class
Master$1.class
Master$Slave.class
Master.class
和 Master
类对应,
Master$Slave.class
和 Slave
类对应。
其实在虚拟机看来,成员内部类就是一个普通的类(Master$1.class
也对应一个内部类,在 generateAndAssign(...)
方法中创建 Slave
类的实例时,会用到 Master$1
,这里就不展开了)。
问题二: 成员内部类是如何访问外围类的?
先说结论,具体如下。
成员内部类会持有外围类对象的引用。如果成员内部类需要访问外围类的私有字段或私有方法,则编译器会在外围类中合成一个相应的包访问级别(package access
)的静态方法,供成员内部类访问。
我们再看一下 Master.java
中的代码,其中有如下几行
private class Slave {
private int id = Master.this.cnt++;
private int someValue;
}
这里有两个问题
- 代码里的
Master.this
是什么? -
Slave
类为何可以直接访问Master
类的private
字段(也就是名为cnt
的字段)?
如果我们用 javap
来解析刚才生成的class
文件的内容的话,也能了解内部类的运作机制,但是 javap
的解析结果太底层了,我们可以借助 cfr
工具(在 https://www.benf.org/other/cfr/
页面可以下载对应的 jar
包) 来查看 class
文件的内容。
这里我补充一句,cfr
虽然方便,但是它支持的选项比较杂,我感觉它的行为比较复杂,所以也不必完全以 cfr
的解析结果为准(可以适当参考 javap
的解析结果)。所谓尽信书则不如无书。
我下载了 cfr-0.150.jar
。先用它查看 Master$Slave.class
的内容(完整的命令如下)
# 假如想用 javap 的话,可以参考下一行
# javap -v -p 'Master$Slave'
java -jar cfr-0.150.jar 'Master$Slave' --removeinnerclasssynthetics false
执行上述命令后得到的完整结果如下
/*
* Decompiled with CFR 0.150.
*/
private class Master.Slave {
private int id;
private int someValue;
final /* synthetic */ Master this$0;
private Master.Slave(Master master) {
this.this$0 = master;
this.id = Master.access$208(this.this$0);
}
/* synthetic */ Master.Slave(Master master, Master.1 var2_2) {
this(master);
}
static /* synthetic */ int access$102(Master.Slave slave, int n) {
slave.someValue = n;
return slave.someValue;
}
}
观察上述内容后,会发现
-
Slave
类有两个构造函数 -
Slave
类中有一个名为access$102
的方法,这个方法是编译器自动合成的 -
Slave
类中有一个字段类型为Master
,名称为this$0
,这个字段也是编译器自动合成的
我们用如下命令查看 Master.class
的内容
# 假如想用 javap 的话,可以参考下一行
# javap -v -p 'Master'
java -jar cfr-0.150.jar 'Master' --removeinnerclasssynthetics false
完整的结果如下
/*
* Decompiled with CFR 0.150.
*/
public class Master {
private int cnt = 0;
public Slave generateAndAssign(int n) {
Slave slave = new Slave(this, null);
Slave.access$102(slave, n);
return slave;
}
static /* synthetic */ int access$208(Master master) {
return master.cnt++;
}
private class Slave {
private int id;
private int someValue;
final /* synthetic */ Master this$0;
private Slave(Master master) {
this.this$0 = master;
this.id = Master.access$208(this.this$0);
}
/* synthetic */ Slave(Master master, 1 var2_2) {
this(master);
}
static /* synthetic */ int access$102(Slave slave, int n) {
slave.someValue = n;
return slave.someValue;
}
}
}
刚才提到两个小问题
- 代码里的
Master.this
是什么?Slave
类为何可以直接访问Master
类的private
字段(也就是名为cnt
的字段)?
简要的解答如下
- 代码里的
Master.this
是成员内部类持有的外围类对象的引用,该引用是在成员内部类的构造函数中被赋值的 -
Slave
类并不能直接访问Master
类的cnt
字段(因为该字段是private
的),编译会在Master
类中合成包访问级别的静态方法,Slave
类通过调用该静态方法来间接操作Master
对象的cnt
字段
这样回答可能还是有点抽象,我在 cfr
解析结果的基础上,加了一些注释来进行解释,具体如下
public class Master {
private int cnt = 0;
public Slave generateAndAssign(int n) {
// 这里调用了 Slave 类的第二个构造函数(第二个构造函数是包可见级别的)
// Slave 类的第二个构造函数会调用第一个构造函数
Slave slave = new Slave(this, null);
Slave.access$102(slave, n);
return slave;
}
// 编译器合成了这个方法
// 这个方法是包访问级别的,所以对 Slave 类是可见的
// Slave 类的对象通过调用这个方法,来操作 Master 类中的 cnt 字段
static /* synthetic */ int access$208(Master master) {
return master.cnt++;
}
private class Slave {
private int id;
private int someValue;
// 编译器合成了这个字段
// 成员内部类会持有外围类对象的引用,这个引用就保存在 this$0 字段中
final /* synthetic */ Master this$0;
// Slave 类有两个构造函数,这是第一个
// 成员内部类会持有外围类对象的引用,这个引用就保存在 this$0 字段中
private Slave(Master master) {
this.this$0 = master;
this.id = Master.access$208(this.this$0);
}
// Slave 类有两个构造函数,这是第二个
// 这个构造函数有两个入参
// 第二个参数的类型是 Master$1,第二个参数相当于一个占位符
// 这个构造函数是编译器合成的
// 这个构造函数是包访问级别的,所以对 Master 类是可见的
/* synthetic */ Slave(Master master, 1 var2_2) {
this(master);
}
// 编译器合成了这个方法
// 这个方法是包访问级别的,所以对 Master 类是可见的
// Master 类的对象通过调用这个方法,来操作 Slave 类中的 someValue 字段
static /* synthetic */ int access$102(Slave slave, int n) {
slave.someValue = n;
return slave.someValue;
}
}
}
第三个问题: 外围类访问成员内部类?
在解答了第二个问题的基础上,我就直接给出第三个问题的解答了。
- 如果外围类需要访问成员内部类的私有构造函数,则编译器会在成员内部类中合成相应的(包访问级别的)构造函数,供外围类访问
- 如果外围类需要访问成员内部类的私有成员,则编译器会在成员内部类中合成相应的(包访问级别的)静态方法,供外围类访问