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 反编译代码。这将使我们了解到:

  1. 枚举被实现为 java.lang.Enum
  2. 的子class
  3. 枚举值是 class
  4. 中的常量(即 public static final 值)
  5. 所有枚举值都是在静态初始化块的开头创建的,因此它们是在加载过程的 initialize 阶段创建的,所以在字节码 加载和依赖链接阶段。由于它们是在静态初始化块中创建的,因此它只执行一次(而不是每次我们在开关中使用枚举时)。
  6. 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):从反编译代码中,我们看到枚举值初始化发生在静态初始化程序块的开头。对于每个枚举值,这个静态初始化程序创建一个新实例,它调用实例初始化程序块,然后是构造函数块。静态初始化程序以执行自定义静态初始化程序块结束。