Java中枚举的执行顺序

9
我是一名有用的助手,可以为您翻译文本。

我有一个关于枚举的问题。

我有一个枚举类,如下所示:

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;
    }
}

我想知道这个枚举对象是何时创建的。

枚举看起来像是一个 'static final' 对象,因为其值永远不会改变。因此,在编译时仅初始化是高效的。

但是它在顶部调用了自己的构造函数,所以我怀疑它是否会在我们调用它时生成,例如在 switch 语句中。

4个回答

11
TLDR:枚举值是在枚举类加载的初始化阶段一次性创建的常量。这样做非常高效,因为枚举值只会被创建一次。
长答案:枚举不是神奇的元素,但需要一些时间来理解它们的工作原理。枚举行为与类加载过程有关,该过程可以概括为三个阶段:
- 加载:类字节码由类加载器加载。 - 链接:解析类层次结构(有一个名为resolution的子阶段)。 - 初始化:通过调用静态初始化块来初始化类。
让我们使用以下枚举类来解释这个过程:
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. 枚举值是类中的常量(即public static final值)。
  3. 所有枚举值都在静态初始化块的开始创建,在加载过程的初始化阶段创建,因此在字节码加载和依赖项链接阶段之后创建。由于它们在静态初始化块中创建,因此仅执行一次(而不是每次我们在switch语句中使用枚举时都执行)。
  4. MyEnum.values()返回所有枚举值的列表,作为枚举值数组的不可变副本。
反编译代码如下:
// 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()
  • 使用Class.forName("mypackage.MyEnum")(它会执行{{加载}}, {{链接}}和{{初始化}}阶段)
  • 调用MyEnum.class.getEnumConstants()

但是,枚举值将不会通过以下操作进行初始化(仅执行{{加载}}阶段和可能的{{链接}}阶段):

  • MyEnum.class.anyMethod()(当然除了getEnumConstants()):基本上我们只访问类元数据,而不是实现
  • Class.forName("myPackage.MyEnum", false, aClassLoader):false值参数告诉类加载器避免{{初始化}}阶段
  • ClassLoader.getSystemClassLoader().loadClass("myPackage.MyEnum"):明确执行{{加载}}阶段。

关于枚举的一些有趣的其他事实:

  • Class<MyEnum>.getInstance() 抛出异常:因为枚举中没有公共构造函数
  • 初始化块的执行顺序似乎与通常的相反(首先是实例初始化器块 V1,然后是构造器块 constructor V1,最后是静态初始化器 static init):从反编译的代码中,我们看到枚举值的初始化发生在静态初始化器块的开头。对于每个枚举值,这个静态初始化器都会创建一个新的实例,该实例调用实例初始化器块和构造器块。静态初始化器以执行自定义静态初始化器块结束。

感谢您对该过程进行了非常全面的审查。 - peresisUser

9

是的,枚举是静态常量,但不是编译时常量。与任何其他类一样,当需要使用枚举时会被首次加载。如果稍微更改它的构造函数,您可以轻松观察到这一点。

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

这表明enum class在我们第一次使用它时被加载(且其静态字段已初始化)。

你也可以使用

Class.forName("full.packag.name.of.FontStyle");

如果尚未加载,则需要加载它的负载。


感谢您提供如此精彩的示例 :D。我印象深刻。bb - Juneyoung Oh

0

枚举实例是在类链接(解析)期间创建的,这是一种阶段,类似于“普通”类的静态字段,而这个阶段是在类加载之后进行的。

类链接与类加载是分开进行的。因此,如果您使用类加载器动态加载Enum类,则只有在实际尝试访问其中一个实例时才会实例化常量,例如使用Class中的getEnumConstants()方法。

以下是一个代码片段,用于测试上述断言:

文件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);
        }
    }
}

输出结果:

我刚刚加载了TestEnum
... 三秒钟的暂停 ...
即将访问常量
初始化一个常量
初始化一个常量
初始化一个常量

如果由于某种原因,您不是简单地使用枚举(只是引用其中一个常量),而是依赖于动态加载它,则区别很重要。

注意事项:

  • 使用Class.forName()将同时加载和链接类,因此常量将立即实例化。
  • 只需访问一个常量即可链接整个类,因此所有常量将在那时被实例化。

3
你这个断言的依据是什么? - user207421
1
@Thilo等:我在我的答案中添加了一个简短的测试,显示枚举常量在第一次访问时被初始化。 - RealSkeptic
@RealSkeptic。但这并没有真正“加载”类。它仍然需要被“解析”。尝试使用Class.forNamecl.loadClass(name, true); - Thilo
@Thilo:说得很好,但这仍然不是在类加载而是在类解析阶段,这是两个不同的阶段。 - RealSkeptic
我已经重新表述了答案以反映上述信息。 - RealSkeptic
显示剩余2条评论

0

枚举实例仅在加载枚举类本身时创建一次。

它非常重要,它们只被创建一次,以便对象标识比较起作用(==)。即使对象(反)序列化机制也必须进行调整以支持此功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接