java 中 Enum 的执行顺序
Execution order of Enum in java
我有一个关于枚举的问题。
我有一个枚举 class 如下所示
public enum FontStyle {
NORMAL("This font has normal style."),
BOLD("This font has bold style."),
ITALIC("This font has italic style."),
UNDERLINE("This font has underline style.");
private String description;
FontStyle(String description) {
this.description = description;
}
public String getDescription() {
return this.description;
}
}
我想知道这个 Enum 对象是什么时候创建的。
枚举看起来像 'static final' 对象,因为它的值永远不会改变。
因此,出于这个目的,仅在编译时进行初始化是有效的。
但是它在top中调用了自己的构造函数,所以我怀疑它是否可以在我们调用它时生成,例如在switch语句中。
枚举实例仅在加载枚举 class 本身时创建一次。
它们只创建一次非常重要,这样对象身份比较才有效 (==
)。甚至必须调整对象(反)序列化机制来支持这一点。
Enum 实例是在 class linking(分辨率) 期间创建的,这是 [=52 之后的一个阶段=] loading, 就像"normal" class.
的静态字段
Class linking 独立于 class 加载。因此,如果您使用 class 加载程序动态加载 Enum class,则仅当您实际尝试访问其中一个实例时才会实例化常量,例如,当使用 getEnumConstants()
方法时来自 Class
.
这里有一些代码来测试上面的断言:
文件 1:TestEnum.java
public enum TestEnum {
CONST1, CONST2, CONST3;
TestEnum() {
System.out.println( "Initializing a constant" );
}
}
文件 2:Test.java
class Test
{
public static void main( String[] args ) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
try {
Class<?> cls = cl.loadClass( "TestEnum" );
System.out.println( "I have just loaded TestEnum" );
Thread.sleep(3000);
System.out.println( "About to access constants" );
cls.getEnumConstants();
} catch ( Exception e ) {
e.printStackTrace();
System.exit(1);
}
}
}
输出:
I have just loaded TestEnum
...停顿三秒...About to access constants
Initializing a constant
Initializing a constant
Initializing a constant
如果出于任何原因您没有直接使用枚举(仅通过引用其常量之一)而是依靠动态加载枚举,那么这种区别就很重要。
备注:
- 使用
Class.forName()
将同时加载 link class,因此常量将立即实例化。
- 整个class访问一个常量就足够了link,因此所有的常量都会在那个时候被实例化。
是的,枚举是静态常量,但不是编译时常量。就像第一次需要时加载任何其他 classes 枚举一样。如果你稍微改变它的构造函数,你可以很容易地观察到它
FontStyle(String description) {
System.out.println("creating instace of "+this);// add this
this.description = description;
}
并使用像
这样的简单测试代码
class Main {
public static void main(String[] Args) throws Exception {
System.out.println("before enum");
FontStyle style1 = FontStyle.BOLD;
FontStyle style2 = FontStyle.ITALIC;
}
}
如果你将 运行 main
方法,你将看到输出
before enum
creating instace of NORMAL
creating instace of BOLD
creating instace of ITALIC
creating instace of UNDERLINE
这表明枚举 class 已在我们第一次使用枚举时加载(并且其静态字段已被初始化)。
您也可以使用
Class.forName("full.packag.name.of.FontStyle");
如果尚未加载则加载它。
TLDR: 枚举值是在运行时创建的常量,在枚举 class 加载的 初始化 阶段。这是有效的,因为枚举值只创建一次。
长答案:
枚举不是神奇的元素,但需要一些时间才能理解它们的工作原理。枚举行为与 class loading process 相关,可概括为 3 个阶段:
- loading:class 字节码由 classloader
加载
- linking:class层次结构已解决(有一个子阶段称为resolution)
- 初始化:class通过调用静态初始化块
初始化
让我们用下面的枚举来解释一下 class:
package mypackage;
public enum MyEnum {
V1, V2;
private MyEnum() {
System.out.println("constructor "+this);
}
static {
System.out.println("static init");
}
{
System.out.println("block "+this);
}
}
为了理解枚举是如何工作的,让我们使用 javap -c MyEnum
反编译代码。这将使我们了解到:
- 枚举被实现为 java.lang.Enum
的子class
- 枚举值是 class
中的常量(即 public static final
值)
- 所有枚举值都是在静态初始化块的开头创建的,因此它们是在加载过程的 initialize 阶段创建的,所以在字节码 加载和依赖链接阶段。由于它们是在静态初始化块中创建的,因此它只执行一次(而不是每次我们在开关中使用枚举时)。
MyEnum.values()
returns 所有枚举值的列表作为枚举值数组的不可变副本。
反编译后的代码如下:
// 1. an enum is implemented as a special class
public final class mypackage.MyEnum extends java.lang.Enum<mypackage.MyEnum> {
public static final mypackage.MyEnum V1; // 2. enum values are constants of the enum class
public static final mypackage.MyEnum V2;
static {};
Code: // 3. all enum values are created in the static initializer block
// create the enum value V1
0: new #1 // class mypackage/MyEnum
3: dup
4: ldc #14 // String V1
6: iconst_0
7: invokespecial #15 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #19 // Field V1:Lmypackage/MyEnum;
// create the enum value V2
13: new #1 // class mypackage/MyEnum
16: dup
17: ldc #21 // String V2
19: iconst_1
20: invokespecial #15 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #22 // Field V2:Lmypackage/MyEnum;
// create an array to store all enum values
39: iconst_2
40: anewarray #1 // class mypackage/MyEnum
43: dup
44: iconst_0
45: getstatic #19 // Field V1:Lmypackage/MyEnum;
48: aastore
49: dup
50: iconst_1
51: getstatic #22 // Field V2:Lmypackage/MyEnum;
54: aastore
61: putstatic #27 // Field ENUM$VALUES:[Lmypackage/MyEnum;
64: getstatic #29 // Field java/lang/System.out:Ljava/io/PrintStream;
67: ldc #35 // String "static init"
69: invokevirtual #37 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
72: return
public static mypackage.MyEnum[] values();
Code: // 4. it returns an immutable copy of the field ENUM$VALUES
0: getstatic #27 // Field ENUM$VALUES:[Lmypackage/MyEnum;
3: dup
4: astore_0
5: iconst_0
6: aload_0
7: arraylength
8: dup
9: istore_1
10: anewarray #1 // class mypackage/MyEnum
13: dup
14: astore_2
15: iconst_0
16: iload_1
17: invokestatic #67 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V (=immutable copy)
20: aload_2
21: areturn
public static mypackage.MyEnum valueOf(java.lang.String);
Code:
0: ldc #1 // class mypackage/MyEnum
2: aload_0
3: invokestatic #73 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #1 // class mypackage/MyEnum
9: areturn
}
因此,枚举值是在执行静态初始化程序块时创建的,即在 初始化 阶段。这可以通过以下方法之一完成:
- 第一次获取枚举值(例如
System.out.println(MyEnum.V1)
)
- 执行枚举的静态方法时(例如
MyEnum.valueOf()
或MyEnum.myStaticMethod()
)
- with
Class.forName("mypackage.MyEnum")
(加载、链接和初始化阶段)
- 调用时
MyEnum.class.getEnumConstants()
然而,枚举值不会用以下操作初始化(只执行 loading 阶段,并且可能是 linking 阶段):
MyEnum.class.anyMethod()
(当然 getEnumConstants()
除外):基本上我们只访问 class 元数据,而不是实现
Class.forName("myPackage.MyEnum", false, aClassLoader)
:false
值参数告诉class加载器避免初始化阶段
ClassLoader.getSystemClassLoader().loadClass("myPackage.MyEnum")
:明确地只执行 loading 阶段。
关于枚举的其他一些有趣的事实:
Class<MyEnum>.getInstance()
抛出异常:因为枚举 中没有public构造函数
- initialization blocks execution orders seems to be reversed from the usual one(第一个实例初始化器
block V1
,然后是构造函数块constructor V1
,然后是静态初始化器static init
):从反编译代码中,我们看到枚举值初始化发生在静态初始化程序块的开头。对于每个枚举值,这个静态初始化程序创建一个新实例,它调用实例初始化程序块,然后是构造函数块。静态初始化程序以执行自定义静态初始化程序块结束。
我有一个关于枚举的问题。
我有一个枚举 class 如下所示
public enum FontStyle {
NORMAL("This font has normal style."),
BOLD("This font has bold style."),
ITALIC("This font has italic style."),
UNDERLINE("This font has underline style.");
private String description;
FontStyle(String description) {
this.description = description;
}
public String getDescription() {
return this.description;
}
}
我想知道这个 Enum 对象是什么时候创建的。
枚举看起来像 'static final' 对象,因为它的值永远不会改变。 因此,出于这个目的,仅在编译时进行初始化是有效的。
但是它在top中调用了自己的构造函数,所以我怀疑它是否可以在我们调用它时生成,例如在switch语句中。
枚举实例仅在加载枚举 class 本身时创建一次。
它们只创建一次非常重要,这样对象身份比较才有效 (==
)。甚至必须调整对象(反)序列化机制来支持这一点。
Enum 实例是在 class linking(分辨率) 期间创建的,这是 [=52 之后的一个阶段=] loading, 就像"normal" class.
的静态字段Class linking 独立于 class 加载。因此,如果您使用 class 加载程序动态加载 Enum class,则仅当您实际尝试访问其中一个实例时才会实例化常量,例如,当使用 getEnumConstants()
方法时来自 Class
.
这里有一些代码来测试上面的断言:
文件 1:TestEnum.java
public enum TestEnum {
CONST1, CONST2, CONST3;
TestEnum() {
System.out.println( "Initializing a constant" );
}
}
文件 2:Test.java
class Test
{
public static void main( String[] args ) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
try {
Class<?> cls = cl.loadClass( "TestEnum" );
System.out.println( "I have just loaded TestEnum" );
Thread.sleep(3000);
System.out.println( "About to access constants" );
cls.getEnumConstants();
} catch ( Exception e ) {
e.printStackTrace();
System.exit(1);
}
}
}
输出:
I have just loaded TestEnum...停顿三秒...
About to access constants Initializing a constant Initializing a constant Initializing a constant
如果出于任何原因您没有直接使用枚举(仅通过引用其常量之一)而是依靠动态加载枚举,那么这种区别就很重要。
备注:
- 使用
Class.forName()
将同时加载 link class,因此常量将立即实例化。 - 整个class访问一个常量就足够了link,因此所有的常量都会在那个时候被实例化。
是的,枚举是静态常量,但不是编译时常量。就像第一次需要时加载任何其他 classes 枚举一样。如果你稍微改变它的构造函数,你可以很容易地观察到它
FontStyle(String description) {
System.out.println("creating instace of "+this);// add this
this.description = description;
}
并使用像
这样的简单测试代码class Main {
public static void main(String[] Args) throws Exception {
System.out.println("before enum");
FontStyle style1 = FontStyle.BOLD;
FontStyle style2 = FontStyle.ITALIC;
}
}
如果你将 运行 main
方法,你将看到输出
before enum
creating instace of NORMAL
creating instace of BOLD
creating instace of ITALIC
creating instace of UNDERLINE
这表明枚举 class 已在我们第一次使用枚举时加载(并且其静态字段已被初始化)。
您也可以使用
Class.forName("full.packag.name.of.FontStyle");
如果尚未加载则加载它。
TLDR: 枚举值是在运行时创建的常量,在枚举 class 加载的 初始化 阶段。这是有效的,因为枚举值只创建一次。
长答案: 枚举不是神奇的元素,但需要一些时间才能理解它们的工作原理。枚举行为与 class loading process 相关,可概括为 3 个阶段:
- loading:class 字节码由 classloader 加载
- linking:class层次结构已解决(有一个子阶段称为resolution)
- 初始化:class通过调用静态初始化块 初始化
让我们用下面的枚举来解释一下 class:
package mypackage;
public enum MyEnum {
V1, V2;
private MyEnum() {
System.out.println("constructor "+this);
}
static {
System.out.println("static init");
}
{
System.out.println("block "+this);
}
}
为了理解枚举是如何工作的,让我们使用 javap -c MyEnum
反编译代码。这将使我们了解到:
- 枚举被实现为 java.lang.Enum 的子class
- 枚举值是 class 中的常量(即
- 所有枚举值都是在静态初始化块的开头创建的,因此它们是在加载过程的 initialize 阶段创建的,所以在字节码 加载和依赖链接阶段。由于它们是在静态初始化块中创建的,因此它只执行一次(而不是每次我们在开关中使用枚举时)。
MyEnum.values()
returns 所有枚举值的列表作为枚举值数组的不可变副本。
public static final
值)
反编译后的代码如下:
// 1. an enum is implemented as a special class
public final class mypackage.MyEnum extends java.lang.Enum<mypackage.MyEnum> {
public static final mypackage.MyEnum V1; // 2. enum values are constants of the enum class
public static final mypackage.MyEnum V2;
static {};
Code: // 3. all enum values are created in the static initializer block
// create the enum value V1
0: new #1 // class mypackage/MyEnum
3: dup
4: ldc #14 // String V1
6: iconst_0
7: invokespecial #15 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #19 // Field V1:Lmypackage/MyEnum;
// create the enum value V2
13: new #1 // class mypackage/MyEnum
16: dup
17: ldc #21 // String V2
19: iconst_1
20: invokespecial #15 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #22 // Field V2:Lmypackage/MyEnum;
// create an array to store all enum values
39: iconst_2
40: anewarray #1 // class mypackage/MyEnum
43: dup
44: iconst_0
45: getstatic #19 // Field V1:Lmypackage/MyEnum;
48: aastore
49: dup
50: iconst_1
51: getstatic #22 // Field V2:Lmypackage/MyEnum;
54: aastore
61: putstatic #27 // Field ENUM$VALUES:[Lmypackage/MyEnum;
64: getstatic #29 // Field java/lang/System.out:Ljava/io/PrintStream;
67: ldc #35 // String "static init"
69: invokevirtual #37 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
72: return
public static mypackage.MyEnum[] values();
Code: // 4. it returns an immutable copy of the field ENUM$VALUES
0: getstatic #27 // Field ENUM$VALUES:[Lmypackage/MyEnum;
3: dup
4: astore_0
5: iconst_0
6: aload_0
7: arraylength
8: dup
9: istore_1
10: anewarray #1 // class mypackage/MyEnum
13: dup
14: astore_2
15: iconst_0
16: iload_1
17: invokestatic #67 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V (=immutable copy)
20: aload_2
21: areturn
public static mypackage.MyEnum valueOf(java.lang.String);
Code:
0: ldc #1 // class mypackage/MyEnum
2: aload_0
3: invokestatic #73 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #1 // class mypackage/MyEnum
9: areturn
}
因此,枚举值是在执行静态初始化程序块时创建的,即在 初始化 阶段。这可以通过以下方法之一完成:
- 第一次获取枚举值(例如
System.out.println(MyEnum.V1)
) - 执行枚举的静态方法时(例如
MyEnum.valueOf()
或MyEnum.myStaticMethod()
) - with
Class.forName("mypackage.MyEnum")
(加载、链接和初始化阶段) - 调用时
MyEnum.class.getEnumConstants()
然而,枚举值不会用以下操作初始化(只执行 loading 阶段,并且可能是 linking 阶段):
MyEnum.class.anyMethod()
(当然getEnumConstants()
除外):基本上我们只访问 class 元数据,而不是实现Class.forName("myPackage.MyEnum", false, aClassLoader)
:false
值参数告诉class加载器避免初始化阶段ClassLoader.getSystemClassLoader().loadClass("myPackage.MyEnum")
:明确地只执行 loading 阶段。
关于枚举的其他一些有趣的事实:
Class<MyEnum>.getInstance()
抛出异常:因为枚举 中没有public构造函数
- initialization blocks execution orders seems to be reversed from the usual one(第一个实例初始化器
block V1
,然后是构造函数块constructor V1
,然后是静态初始化器static init
):从反编译代码中,我们看到枚举值初始化发生在静态初始化程序块的开头。对于每个枚举值,这个静态初始化程序创建一个新实例,它调用实例初始化程序块,然后是构造函数块。静态初始化程序以执行自定义静态初始化程序块结束。