背景
在日常开发过程中,我们有时候需要查看 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件的内容。如果我们对 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件的结构有基本的了解,那么就会事半功倍。由于这个话题很大,而且我自己的水平也有限,所以每次只写一个很小的主题。本文的主题是理解 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件中字段的 <math xmlns="http://www.w3.org/1998/Math/MathML"> access flags \text{access flags} </math>access flags。
要点
目前(JDK26), <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件中字段的 <math xmlns="http://www.w3.org/1998/Math/MathML"> access flags \text{access flags} </math>access flags 一共支持 <math xmlns="http://www.w3.org/1998/Math/MathML"> 9 9 </math>9 种类型。对每一种类型,本文都提供了对应的代码进行验证。
正文
The Java® Virtual Machine Specification 中的 4.5. Fields 小节详细介绍了 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件中 <math xmlns="http://www.w3.org/1998/Math/MathML"> field_info \text{field\_info} </math>field_info 的结构,它的开头是这样的 ⬇️

<math xmlns="http://www.w3.org/1998/Math/MathML"> u2 \text{u2} </math>u2 的含义
The Java® Virtual Machine Specification 中的 Chapter 4. The class File Format 开头解释了 <math xmlns="http://www.w3.org/1998/Math/MathML"> u2 \text{u2} </math>u2 的含义 ⬇️ (如下图红色框所示)

<math xmlns="http://www.w3.org/1998/Math/MathML"> u2 \text{u2} </math>u2 表示 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 byte \text{2 byte} </math>2 byte 的无符号数
字段的 <math xmlns="http://www.w3.org/1998/Math/MathML"> access flags \text{access flags} </math>access flags
The Java® Virtual Machine Specification 中的 4.5. Fields 小节的开头提供了一个表格 ⬇️ (表格的名称是 Table 4.5-A. Field access and property flags)
| Flag Name | Value | Interpretation |
|---|---|---|
ACC_PUBLIC |
0x0001 | Declared public; may be accessed from outside its package. |
ACC_PRIVATE |
0x0002 | Declared private; accessible only within the defining class and other classes belonging to the same nest (§5.4.4). |
ACC_PROTECTED |
0x0004 | Declared protected; may be accessed within subclasses. |
ACC_STATIC |
0x0008 | Declared static. |
ACC_FINAL |
0x0010 | Declared final; never directly assigned to after object construction (JLS §17.5). |
ACC_VOLATILE |
0x0040 | Declared volatile; cannot be cached. |
ACC_TRANSIENT |
0x0080 | Declared transient; not written or read by a persistent object manager. |
ACC_SYNTHETIC |
0x1000 | Declared synthetic; not present in the source code. |
ACC_ENUM |
0x4000 | Declared as an element of an enum class. |
表格中一共列举了 <math xmlns="http://www.w3.org/1998/Math/MathML"> 9 \text{9} </math>9 种情况。我们分别来看。
前 <math xmlns="http://www.w3.org/1998/Math/MathML"> 7 \text{7} </math>7 种情况
请将以下代码保存为 <math xmlns="http://www.w3.org/1998/Math/MathML"> Simple.java \text{Simple.java} </math>Simple.java
java
public class Simple {
public int f1;
private int f2;
protected int f3;
static int f4;
final int f5 = 1;
volatile int f6;
transient int f7;
}
使用如下的命令可以编译 <math xmlns="http://www.w3.org/1998/Math/MathML"> Simple.java \text{Simple.java} </math>Simple.java
bash
javac Simple.java
编译之后,当前目录会多出一个名为 <math xmlns="http://www.w3.org/1998/Math/MathML"> Simple.class \text{Simple.class} </math>Simple.class 的文件。使用如下的命令可以查看 <math xmlns="http://www.w3.org/1998/Math/MathML"> Simple.class \text{Simple.class} </math>Simple.class 的详细内容
bash
javap -v -p Simple
完整的结果略有点长,我把和本文相关的部分复制到下方了 ⬇️ (... 表示本文不关心的内容)
text
...
public class Simple
...
{
public int f1;
descriptor: I
flags: (0x0001) ACC_PUBLIC
private int f2;
descriptor: I
flags: (0x0002) ACC_PRIVATE
protected int f3;
descriptor: I
flags: (0x0004) ACC_PROTECTED
static int f4;
descriptor: I
flags: (0x0008) ACC_STATIC
final int f5;
descriptor: I
flags: (0x0010) ACC_FINAL
ConstantValue: int 1
volatile int f6;
descriptor: I
flags: (0x0040) ACC_VOLATILE
transient int f7;
descriptor: I
flags: (0x0080) ACC_TRANSIENT
public Simple();
...
}
...
不难验证,前 <math xmlns="http://www.w3.org/1998/Math/MathML"> 7 \text{7} </math>7 种情况都出现了。
第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 8 \text{8} </math>8 种情况: <math xmlns="http://www.w3.org/1998/Math/MathML"> ACC_SYNTHETIC \text{ACC\_SYNTHETIC} </math>ACC_SYNTHETIC
为了看到这种情况,我们需要生成合成字段。考虑到创建内部类的实例时,内部类的对象会持有宿主类的引用,而这个引用会保存在内部类的合成字段中。
请将以下代码保存为 <math xmlns="http://www.w3.org/1998/Math/MathML"> Case8.java \text{Case8.java} </math>Case8.java
java
class Case8 {
int a;
class Inner {
int b = a;
}
}
使用如下的命令可以编译 <math xmlns="http://www.w3.org/1998/Math/MathML"> Case8.java \text{Case8.java} </math>Case8.java
bash
javac -parameters Case8.java
编译之后,当前目录会多出两个文件 ⬇️
Case8.classCase8$Inner.class
使用如下命令可以查看 Case8$Inner.class 的详细内容
bash
javap -v -p Case8\$Inner
完整的结果略有点长,我把和本文相关的部分复制到下方了 ⬇️ (... 表示本文不关心的内容)
text
...
class Case8$Inner
...
{
int b;
descriptor: I
flags: (0x0000)
final Case8 this$0;
descriptor: LCase8;
flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC
Case8$Inner(Case8);
...
}
...
可以看到 Case8$Inner 这个类中的确有一个合成字段 this$0。借助 <math xmlns="http://www.w3.org/1998/Math/MathML"> javap \text{javap} </math>javap 命令,我把以下两个 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件手动反编译了一下
Case8.classCase8$Inner.class
结果如下 ⬇️
java
// 以下代码是我手动反编译的结果,内容仅供参考,可能有不准确甚至错误的地方
class Case8 {
int a;
Case8() {
super();
}
}
class Case8$Inner {
int b;
// 下方这个字段是合成字段,在 java 代码中看不到。
final Case8 this$0;
Case8$Inner(Case8 this$0) {
this.this$0 = this$0;
super();
this.b = this.this$0.a;
}
}
简要的类图如下 
第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 9 \text{9} </math>9 种情况: <math xmlns="http://www.w3.org/1998/Math/MathML"> ACC_ENUM \text{ACC\_ENUM} </math>ACC_ENUM
第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 9 \text{9} </math>9 种情况与枚举类型有关。我们写个枚举类来验证一下。请将以下代码保存为 <math xmlns="http://www.w3.org/1998/Math/MathML"> Direction.java \text{Direction.java} </math>Direction.java
java
public enum Direction {
EAST,
WEST,
SOUTH,
NORTH;
}
使用如下的命令可以编译 <math xmlns="http://www.w3.org/1998/Math/MathML"> Direction.java \text{Direction.java} </math>Direction.java
bash
javac Direction.java
编译之后,当前目录会多出名为 <math xmlns="http://www.w3.org/1998/Math/MathML"> t e x t D i r e c t i o n . c l a s s text{Direction.class} </math>textDirection.class 的文件。使用如下命令可以查看 <math xmlns="http://www.w3.org/1998/Math/MathML"> Direction.class \text{Direction.class} </math>Direction.class 的详细内容
bash
javap -v -p Direction
完整的结果略有点长,我把和本文相关的部分复制到下方了 ⬇️ (... 表示本文不关心的内容)
text
...
public final class Direction extends java.lang.Enum<Direction>
...
{
public static final Direction EAST;
descriptor: LDirection;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final Direction WEST;
descriptor: LDirection;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final Direction SOUTH;
descriptor: LDirection;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final Direction NORTH;
descriptor: LDirection;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
private static final Direction[] $VALUES;
descriptor: [LDirection;
flags: (0x101a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
...
}
...
不难验证, <math xmlns="http://www.w3.org/1998/Math/MathML"> Direction \text{Direction} </math>Direction 中定义的 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 个枚举值(即, <math xmlns="http://www.w3.org/1998/Math/MathML"> EAST, WEST, SOUTH, NORTH \text{EAST, WEST, SOUTH, NORTH} </math>EAST, WEST, SOUTH, NORTH),在 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件中,其实是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 个静态字段,而且这 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 个字段的 <math xmlns="http://www.w3.org/1998/Math/MathML"> access flags \text{access flags} </math>access flags 中, <math xmlns="http://www.w3.org/1998/Math/MathML"> ACC_ENUM \text{ACC\_ENUM} </math>ACC_ENUM 都处于置位状态(也就是说这一位的值是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1)。至于 <math xmlns="http://www.w3.org/1998/Math/MathML"> java \text{java} </math>java 语言中,实现枚举类型的更多细节,可以参考我之前写的一篇文章:[Java] 浅析枚举的实现
参考资料
The Java® Virtual Machine Specification 中的
其他
"简要的类图"是怎么画出来的
我用了 IntelliJ IDEA (Community Edition) 中 PlantUML 的插件来绘制那张图,用到的代码如下 ⬇️
puml
@startuml
title 简要的类图
caption \n\n
' caption 的内容是为了防止掘金平台自动生成的水印遮盖图中的文字
class Case8 {
int a
Case8()
}
class Case8$Inner {
int b
final Case8 this$0
Case8$Inner(Case8 this$0)
}
note left of Case8$Inner::Case8$Inner
可以认为构造函数的代码是这样的 <:point_down:>
<code>
Case8$Inner(Case8 this$0) {
this.this$0 = this$0;
super();
this.b = this.this$0.a;
}
</code>
end note
@enduml