浅析 java 中枚举(enum)的实现
Java 从 1.5 开始支持枚举(enum) 类型,大家应该都已经用过枚举类型了,在 class 文件层面,枚举是如何实现的呢?本文对此进行分析。
要点
正文
代码及准备工作
代码
我们用一个简单的 enum class 来进行讨论 ⬇️(请将以下代码保存为 Direction.java)
public enum Direction {
EAST("东"),
WEST("西"),
SOUTH("南"),
NORTH("北");
private final String desc;
Direction(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
public static void main(String[] args) {
for (Direction d : Direction.values()) {
System.out.printf("For Direction instance: %s, its ordinal is: %s%n", d.name(), d.ordinal());
}
}
}
在 Direction 的 main(String[]) 方法中,我们调用了 Direction 类中的 3 个方法 ⬇️
name()ordinal()values()(⬅️ 它是Direction的静态方法)
但是在 Direction.java 中,并没有显式定义这 3 个方法,那么这 3 个方法是从哪里来的呢?我们来进行探索吧。
相关的命令
# ⬇️ 用这个命令可以编译 Direction.java
javac -g -parameters Direction.java
# ⬇️ 用这个命令可以查看 Direction.class 的简要内容
javap -p Direction
# ⬇️ 用这个命令可以查看 Direction.class 的详细内容
javap -v -p Direction
用 javac -g -parameters Direction.java 命令编译 Direction.java 后,会得到 Direction.class 文件。
用 javap -p Direction 命令可以查看 Direction.class 文件的简要内容。其结果如下 ⬇️
Compiled from "Direction.java"
public final class Direction extends java.lang.Enum<Direction> {
public static final Direction EAST;
public static final Direction WEST;
public static final Direction SOUTH;
public static final Direction NORTH;
private final java.lang.String desc;
private static final Direction[] $VALUES;
public static Direction[] values();
public static Direction valueOf(java.lang.String);
private Direction(java.lang.String);
public java.lang.String getDesc();
public static void main(java.lang.String[]);
private static Direction[] $values();
static {};
}
可见 Direction extend 了 java.lang.Enum。
我画了张简单的类图⬇️
用 javap -v -p Direction 命令可以查看 Direction.class 文件的详细内容,
但完整的结果有点长,这里就不展示了。基于 javap -v -p Direction 命令给出的结果,我们可以尝试把对应的 java 代码写出来 ⬇️ (其中的注释是我额外添加的)
// 以下代码是我手动转化的,不保证绝对准确,仅供参考
public final class Direction extends java.lang.Enum<Direction> {
public static final Direction EAST;
public static final Direction WEST;
public static final Direction SOUTH;
public static final Direction NORTH;
private final java.lang.String desc;
// $VALUES 是合成字段,在 java 代码中找不到它,
// 为了便于理解,我把它也展示出来了
private static final Direction[] $VALUES;
// 按照 https://docs.oracle.com/javase/specs/jls/se24/html/jls-8.html#jls-8.9.3 的描述,
// 需要有这个方法 ⬇️
public static Direction[] values() {
return (Direction[]) $VALUES.clone();
}
// 按照 https://docs.oracle.com/javase/specs/jls/se24/html/jls-8.html#jls-8.9.3 的描述,
// 需要有这个方法 ⬇️
public static Direction valueOf(java.lang.String name) {
return (Direction) Enum.valueOf(Direction.class, name);
}
// $enum$name 和 $enum$ordinal 都是编译器合成的参数,在 java 代码中找不到它,
// 为了便于理解,我把它们也展示出来了
private Direction(java.lang.String $enum$name, int $enum$ordinal, java.lang.String desc) {
super($enum$name, $enum$ordinal);
this.desc = desc;
}
public java.lang.String getDesc() {
return this.desc;
}
public static void main(java.lang.String[]) {
// 略
}
// $values 是合成方法,在 java 代码中找不到它,
// 为了便于理解,我把它也展示出来了
private static Direction[] $values() {
return new Direction[] {EAST, WEST, SOUTH, NORTH};
}
static {
EAST = new Direction("EAST", 0, "东");
WEST = new Direction("WEST", 1, "西");
SOUTH = new Direction("SOUTH", 2, "南");
NORTH = new Direction("NORTH", 3, "北");
$VALUES = $values();
};
}
要点 1: 每个枚举常量都是其所属 enum class 里的一个 public static final 实例
Direction.java 中定义的常量 EAST/WEST/SOUTH/NORTH 在 class 文件中是什么呢?
Java Language Specification 中的 8.9.3. Enum Members 小节 中提到 ⬇️
For each enum constant
cdeclared in the body of the declaration of E, E has an implicitly declaredpublicstaticfinalfield of type E that has the same name asc.
由此可见,EAST/WEST/SOUTH/NORTH 应该是 Direction 这个 class 里的静态字段。
在 javap -p Direction 命令提供的结果中,确实可以找到以下 4 个字段,它们都是 Direction 的实例 ⬇️
public static final Direction EASTpublic static final Direction WESTpublic static final Direction SOUTHpublic static final Direction NORTH
这 4 个字段在类图中的位置如下 ⬇️
要点 2: java.lang.Enum 中定义了 name/ordinal 字段,并提供了 name()/ordinal() 方法用于访问这两个字段
在 Direction.java 中,我们通过枚举常量调用了 name()/ordinal() 方法
在 Enum.java 里可以看到,java.lang.Enum 里定义了 name/ordinal 字段(为节省空间,其他代码用 "..." 表示,下同)
...
/**
* The name of this enum constant, as declared in the enum declaration.
* Most programmers should use the {@link #toString} method rather than
* accessing this field.
*/
private final String name;
...
/**
* The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*
* Most programmers will have no use for this field. It is designed
* for use by sophisticated enum-based data structures, such as
* {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*/
private final int ordinal;
...
这两个字段既是 final 的 ,又是 private 的,这样就有两个问题 ⬇️
-
name/ordinal字段是什么时候被赋值的? - 如何访问
name/ordinal字段?
name/ordinal 字段是什么时候被赋值的?
javap -v -p Direction 命令的输出中包含了 Direction 的构造函数。
构造函数的部分是这样的 ⬇️
private Direction(java.lang.String);
descriptor: (Ljava/lang/String;ILjava/lang/String;)V
flags: (0x0002) ACC_PRIVATE
Code:
stack=3, locals=4, args_size=4
0: aload_0
1: aload_1
2: iload_2
3: invokespecial #31 // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
6: aload_0
7: aload_3
8: putfield #35 // Field desc:Ljava/lang/String;
11: return
LineNumberTable:
line 9: 0
line 10: 6
line 11: 11
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 this LDirection;
0 12 3 desc Ljava/lang/String;
MethodParameters:
Name Flags
$enum$name synthetic
$enum$ordinal synthetic
desc
Signature: #99 // (Ljava/lang/String;)V
我们可以手动将其转化为对应的 java 代码 ⬇️
// $enum$name 和 $enum$ordinal 都是编译器合成的参数,在 java 代码中找不到它,
// 为了便于理解,我把它们也展示出来了
private Direction(java.lang.String $enum$name, int $enum$ordinal, java.lang.String desc) {
super($enum$name, $enum$ordinal);
this.desc = desc;
}
其中 super($enum$name, $enum$ordinal) 调用了 java.lang.Enum 的构造函数(其实这里是句废话,因为除了 Object class 之外,所有 class 的构造函数都会直接/间接调用其父类的构造函数),我们再去看 java.lang.Enum 的构造函数的逻辑 ⬇️
...
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum class declarations.
*
* @param name The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
...
java.lang.Enum 的构造函数中只对 name/ordinal 字段进行了赋值,没有做其他事情(其实它也会调用自己的父类,即 java.lang.Object 的构造函数,不过源码里没有体现,这里就略去了)。
更新后的任务列表如下 ⬇️
-
name/ordinal字段是什么时候被赋值的?- 在
java.lang.Enum的构造函数中
- 在
- 如何访问
name/ordinal字段?
如何访问 name/ordinal 字段?
由于 name/ordinal 字段都是 private 的,所以 Direction (以及其他枚举类)无法直接访问它们。而 Enum.java 中提供了 name()/ordinal() 方法,我们可以通过调用 name()/ordinal() 方法来访问 name/ordinal 字段。由于 name()/ordinal() 方法都是 final 的,这就可以保证 name()/ordinal() 方法总是会返回 this.name/this.ordinal 字段,而子类无法 override name()/ordinal() 方法。
更新后的任务列表如下 ⬇️
-
name/ordinal字段是什么时候被赋值的?- 在
java.lang.Enum的构造函数中
- 在
- 如何访问
name/ordinal字段?- 可以通过调用
name()/ordinal()方法来访问name/ordinal字段
- 可以通过调用
简单汇总一下相关的信息,可以得到如下的表格 ⬇️
| 字段 | 是否 private | 何时被赋值 | 对应的 getter 方法 |
|---|---|---|---|
name | ✅ | 在 java.lang.Enum 的构造函数中 | java.lang.Enum 中的 name() |
ordinal | ✅ | 在 java.lang.Enum 的构造函数中 | java.lang.Enum 中的 ordinal() |
要点 3: 枚举类中会出现(隐式声明的) public static E[] values() 方法
Java Language Specification 中的 8.9.3. Enum Members 小节 提到 ⬇️
- An implicitly declared method
publicstaticE[]values(), which returns an array containing the enum constants of E, in the same order as they appear in the body of the declaration of E.
所以 Direction.class 中会有一个自动生成的 values() 静态方法。
在 javap -v -p Direction 命令所提供的结果中,可以找到与 values() 方法对应的内容 ⬇️
...
public static Direction[] values();
descriptor: ()[LDirection;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #16 // Field $VALUES:[LDirection;
3: invokevirtual #20 // Method "[LDirection;".clone:()Ljava/lang/Object;
6: checkcast #21 // class "[LDirection;"
9: areturn
LineNumberTable:
line 1: 0
...
可以认为对应的 java 代码如下 ⬇️
public static Direction[] values() {
return (Direction[]) $VALUES.clone();
}
但这里的 $VALUES 是什么?
在 javap -v -p Direction 命令的输出中,可以找到 $VALUES 对应的内容 ⬇️
...
private static final Direction[] $VALUES;
descriptor: [LDirection;
flags: (0x101a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
...
可见 $VALUES 是一个合成静态字段(从 ACC_SYNTHETIC 这个 flag 推知它是合成字段)。
另一个问题随之而来,这个字段是如何被赋值的呢?
既然是静态 final 字段,在 class 文件中,应该只能在 <clinit> 中赋值了,我们去看看 <clinit> 部分 ⬇️
...
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: new #1 // class Direction
3: dup
4: ldc #72 // String EAST
6: iconst_0
7: ldc #73 // String 东
9: invokespecial #75 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
12: putstatic #3 // Field EAST:LDirection;
15: new #1 // class Direction
18: dup
19: ldc #78 // String WEST
21: iconst_1
22: ldc #79 // String 西
24: invokespecial #75 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
27: putstatic #7 // Field WEST:LDirection;
30: new #1 // class Direction
33: dup
34: ldc #81 // String SOUTH
36: iconst_2
37: ldc #82 // String 南
39: invokespecial #75 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
42: putstatic #10 // Field SOUTH:LDirection;
45: new #1 // class Direction
48: dup
49: ldc #84 // String NORTH
51: iconst_3
52: ldc #85 // String 北
54: invokespecial #75 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
57: putstatic #13 // Field NORTH:LDirection;
60: invokestatic #87 // Method $values:()[LDirection;
63: putstatic #16 // Field $VALUES:[LDirection;
66: return
LineNumberTable:
line 2: 0
line 3: 15
line 4: 30
line 5: 45
line 1: 60
...
我手动把它转化成了 java 代码 ⬇️
static {
EAST = new Direction("EAST", 0, "东");
WEST = new Direction("WEST", 1, "西");
SOUTH = new Direction("SOUTH", 2, "南");
NORTH = new Direction("NORTH", 3, "北");
$VALUES = $values();
}
现在我们来看 $values() 方法里做了些什么。
private static Direction[] $values();
descriptor: ()[LDirection;
flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=4, locals=0, args_size=0
0: iconst_4
1: anewarray #1 // class Direction
4: dup
5: iconst_0
6: getstatic #3 // Field EAST:LDirection;
9: aastore
10: dup
11: iconst_1
12: getstatic #7 // Field WEST:LDirection;
15: aastore
16: dup
17: iconst_2
18: getstatic #10 // Field SOUTH:LDirection;
21: aastore
22: dup
23: iconst_3
24: getstatic #13 // Field NORTH:LDirection;
27: aastore
28: areturn
LineNumberTable:
line 1: 0
$values() 是一个合成方法,我们可以认为它对应的 java 代码是这样的 ⬇️
// $values() 是合成方法,在 java 代码中找不到它,
// 为了便于理解,我把它也展示出来了
private static Direction[] $values() {
return new Direction[] {EAST, WEST, SOUTH, NORTH};
}
所以 static 语句块里做的事情可以这么概括 ⬇️
- 为
EAST/WEST/SOUTH/NORTH这4个静态字段赋值。 - 通过
$VALUES = $values()这个语句给$VALUES这个静态字段赋值。
这里涉及的几个字段/方法的名称比较像,容易看混,我画了张表格来整理相关信息 ⬇️
| 名称 | 是什么 | 如果是方法的话,方法内的逻辑是什么 | 说明 |
|---|---|---|---|
$values() | 静态方法 | return new Direction[]{EAST, WEST, SOUTH, NORTH} | 平时我们 不会 直接调用它 |
$VALUES | 静态字段,类型是 Direction[] | - | 平时我们 不会 直接访问它 |
values() | 静态方法 | return (Direction[]) $VALUES.clone() | 这个方法是 public 的,我们 可以 调用 |
小结
在类图中可以看到本文所提到的 3 个要点 ⬇️
正文完。
其他
画“Enum (枚举类型) 的思维导图”所用到的代码
借助 PlantUML,可以用如下代码画出那张图
@startmindmap
'https://plantuml.com/mindmap-diagram
title <i>Enum</i> (枚举类型) 的思维导图
* <i>Enum</i> (枚举类型)
** 每个枚举常量都是其所属枚举类型里的一个 <i>public static final</i> 实例
** <i>name</i> 和 <i>ordinal</i>
*** <b><i>java.lang.Enum</i></b> 中定义了 <i>name<i> 字段以及 <i>name()</i> 方法
*** <b><i>java.lang.Enum</i></b> 中定义了 <i>ordinal</i> 字段以及 <i>ordinal()</i> 方法
** 枚举类型中会出现(隐式声明的) <i>public static E[] values()</i> 方法
@endmindmap
画“Direction 的类图”所用到的代码
借助 PlantUML,可以用如下代码画出那张图
@startuml
title <i>Direction</i> 的类图
caption <b>注意: 有些字段和方法没有画</b>
abstract class java.lang.Enum<E extends Enum<E>>
enum Direction
java.lang.Enum <|-- Direction : extends java.lang.Enum<Direction>
abstract class java.lang.Enum {
- final String name
- final int ordinal
+ final String name()
+ final int ordinal()
# Enum(String, int)
+ {static} <T extends Enum<T>> T valueOf(Class<T>, String)
}
enum Direction {
+ {static} final Direction EAST
+ {static} final Direction WEST
+ {static} final Direction SOUTH
+ {static} final Direction NORTH
- final String desc
- {static} final Direction[] $VALUES
+ {static} Direction[] values()
+ {static} Direction valueOf(String)
- Direction(String)
+ String getDesc()
+ {static} void main(String[])
- {static} Direction[] $values()
}
@enduml