Enum#values() 是否在每次调用时分配内存?

2022-09-01 10:45:39

我需要在Java中将序号值转换为枚举值。这很简单:int

MyEnumType value = MyEnumType.values()[ordinal];

该方法是隐式的,我无法找到它的源代码,因此存在问题。values()

是否分配新数组?如果是这样,我是否应该在首次调用时缓存数组?假设转换将经常被调用。MyEnumType.values()


答案 1

是的。

Java没有机制让我们创建不可修改的数组。因此,如果返回相同的可变数组,我们冒着有人可能为每个人更改其内容的风险。values()

因此,在不可修改的数组被引入Java之前,为了安全起见,必须返回包含所有值的新/单独的数组。values()

我们可以用运算符测试它:==

MyEnumType[] arr1 = MyEnumType.values();
MyEnumType[] arr2 = MyEnumType.values();
System.out.println(arr1 == arr2);       //false

如果要避免重新创建此数组,可以简单地存储它并在以后重用结果。有几种方法可以做到这一点,比如。values()

  • 您可以创建私有数组,并仅允许通过 getter 方法访问其内容,例如

    private static final MyEnumType[] VALUES = values();// to avoid recreating array
    
    MyEnumType getByOrdinal(int){
        return VALUES[int];
    }
    
  • 您可以将结果存储在不可修改的集合中,以确保其内容不会被更改(现在此类列表可以公开)。values()List

    public static final List<MyEnumType> VALUES = Collections.unmodifiableList(Arrays.asList(values()));
    

答案 2

从理论上讲,该方法每次都必须返回一个新数组,因为Java没有不可变数组。如果它总是返回相同的数组,则无法防止调用方通过修改数组来相互混淆。values()

我找不到它的源代码

该方法没有普通的源代码,是编译器生成的。对于javac,生成该方法的代码位于com.sun.tools.javac.comp.Lower.visitEnumDef中。对于 ECJ(Eclipse 的编译器),代码位于 org.eclipse.jdt.internal.compiler.codegen.CodeStream.generateSyntheticBodyForEnumValues 中。values()values()

查找该方法实现的一种更简单方法是反汇编已编译的枚举。首先创建一些愚蠢的枚举:values()

enum MyEnumType {
    A, B, C;

    public static void main(String[] args) {
        System.out.println(values()[0]);
    }
}

然后编译它,并使用JDK中包含的javap工具对其进行反汇编:

javac MyEnumType.java && javap -c -p MyEnumType

在输出中可见的是编译器生成的所有枚举隐式成员,包括 (1) 每个枚举常量的字段,(2) 包含所有常量的隐藏数组,(3) 实例化每个常量并将每个常量分配给其命名字段和数组的静态初始值设定项块,以及 (4) 通过调用数组并返回结果来工作的方法:static final$VALUESvalues().clone()$VALUES

final class MyEnumType extends java.lang.Enum<MyEnumType> {
  public static final MyEnumType A;

  public static final MyEnumType B;

  public static final MyEnumType C;

  private static final MyEnumType[] $VALUES;

  public static MyEnumType[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[LMyEnumType;
       3: invokevirtual #2                  // Method "[LMyEnumType;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[LMyEnumType;"
       9: areturn

  public static MyEnumType valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class MyEnumType
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class MyEnumType
       9: areturn

  private MyEnumType(java.lang.String, int);
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: invokespecial #6                  // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
       6: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: invokestatic  #8                  // Method values:()[LMyEnumType;
       6: iconst_0
       7: aaload
       8: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      11: return

  static {};
    Code:
       0: new           #4                  // class MyEnumType
       3: dup
       4: ldc           #10                 // String A
       6: iconst_0
       7: invokespecial #11                 // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #12                 // Field A:LMyEnumType;
      13: new           #4                  // class MyEnumType
      16: dup
      17: ldc           #13                 // String B
      19: iconst_1
      20: invokespecial #11                 // Method "<init>":(Ljava/lang/String;I)V
      23: putstatic     #14                 // Field B:LMyEnumType;
      26: new           #4                  // class MyEnumType
      29: dup
      30: ldc           #15                 // String C
      32: iconst_2
      33: invokespecial #11                 // Method "<init>":(Ljava/lang/String;I)V
      36: putstatic     #16                 // Field C:LMyEnumType;
      39: iconst_3
      40: anewarray     #4                  // class MyEnumType
      43: dup
      44: iconst_0
      45: getstatic     #12                 // Field A:LMyEnumType;
      48: aastore
      49: dup
      50: iconst_1
      51: getstatic     #14                 // Field B:LMyEnumType;
      54: aastore
      55: dup
      56: iconst_2
      57: getstatic     #16                 // Field C:LMyEnumType;
      60: aastore
      61: putstatic     #1                  // Field $VALUES:[LMyEnumType;
      64: return
}

但是,该方法必须返回一个新数组的事实并不意味着编译器必须使用该方法。编译器可能会检测到 的使用情况,并且看到数组未被修改,它可以绕过该方法并使用基础数组。上述方法的解汇编表明javac不会进行这样的优化。values()MyEnumType.values()[ordinal]$VALUESmain

我还测试了ECJ。反汇编显示 ECJ 还初始化了一个隐藏数组来存储常量(尽管 Java langspec 不需要这样做),但有趣的是,它的方法更喜欢创建一个空白数组,然后用 填充它,而不是调用 。无论采用哪种方式,每次都返回一个新数组。像javac一样,它不会尝试优化序号查找:values()System.arraycopy.clone()values()

final class MyEnumType extends java.lang.Enum<MyEnumType> {
  public static final MyEnumType A;

  public static final MyEnumType B;

  public static final MyEnumType C;

  private static final MyEnumType[] ENUM$VALUES;

  static {};
    Code:
       0: new           #1                  // class MyEnumType
       3: dup
       4: ldc           #14                 // String A
       6: iconst_0
       7: invokespecial #15                 // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #19                 // Field A:LMyEnumType;
      13: new           #1                  // class MyEnumType
      16: dup
      17: ldc           #21                 // String B
      19: iconst_1
      20: invokespecial #15                 // Method "<init>":(Ljava/lang/String;I)V
      23: putstatic     #22                 // Field B:LMyEnumType;
      26: new           #1                  // class MyEnumType
      29: dup
      30: ldc           #24                 // String C
      32: iconst_2
      33: invokespecial #15                 // Method "<init>":(Ljava/lang/String;I)V
      36: putstatic     #25                 // Field C:LMyEnumType;
      39: iconst_3
      40: anewarray     #1                  // class MyEnumType
      43: dup
      44: iconst_0
      45: getstatic     #19                 // Field A:LMyEnumType;
      48: aastore
      49: dup
      50: iconst_1
      51: getstatic     #22                 // Field B:LMyEnumType;
      54: aastore
      55: dup
      56: iconst_2
      57: getstatic     #25                 // Field C:LMyEnumType;
      60: aastore
      61: putstatic     #27                 // Field ENUM$VALUES:[LMyEnumType;
      64: return

  private MyEnumType(java.lang.String, int);
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: invokespecial #31                 // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
       6: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #35                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: invokestatic  #41                 // Method values:()[LMyEnumType;
       6: iconst_0
       7: aaload
       8: invokevirtual #45                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      11: return

  public static MyEnumType[] values();
    Code:
       0: getstatic     #27                 // Field ENUM$VALUES:[LMyEnumType;
       3: dup
       4: astore_0
       5: iconst_0
       6: aload_0
       7: arraylength
       8: dup
       9: istore_1
      10: anewarray     #1                  // class MyEnumType
      13: dup
      14: astore_2
      15: iconst_0
      16: iload_1
      17: invokestatic  #53                 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
      20: aload_2
      21: areturn

  public static MyEnumType valueOf(java.lang.String);
    Code:
       0: ldc           #1                  // class MyEnumType
       2: aload_0
       3: invokestatic  #59                 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #1                  // class MyEnumType
       9: areturn
}

但是,JVM 仍然有可能进行优化,以检测数组被复制然后丢弃并避免它的事实。为了测试这一点,我运行了以下一对基准测试程序,这些程序在循环中测试序号查找,一个每次调用,另一个使用数组的私有副本。序数查找的结果被分配给一个字段,以防止它被优化掉:values()volatile

enum MyEnumType1 {
    A, B, C;

    public static void main(String[] args) {
        long t = System.nanoTime();
        for (int n = 0; n < 100_000_000; n++) {
            for (int i = 0; i < 3; i++) {
                dummy = values()[i];
            }
        }
        System.out.printf("Done in %.2f seconds.\n", (System.nanoTime() - t) / 1e9);
    }

    public static volatile Object dummy;
}

enum MyEnumType2 {
    A, B, C;

    public static void main(String[] args) {
        long t = System.nanoTime();
        for (int n = 0; n < 100_000_000; n++) {
            for (int i = 0; i < 3; i++) {
                dummy = values[i];
            }
        }
        System.out.printf("Done in %.2f seconds.\n", (System.nanoTime() - t) / 1e9);
    }

    public static volatile Object dummy;
    private static final MyEnumType2[] values = values();
}

我在服务器VM上的Java 8u60上运行了这个。使用该方法的每次测试大约需要 10 秒,而使用私有阵列的每次测试大约需要 2 秒。使用 JVM 参数表明,使用该方法时存在大量垃圾回收活动,而使用私有数组时则没有垃圾回收活动。在客户端 VM 上运行相同的测试时,专用阵列仍然很快,但方法变得更慢,需要一分多钟才能完成。调用也花费的时间越长,定义的枚举常量就越多。所有这些都表明,该方法确实每次都会分配一个新数组,并且避免它可能是有利的。values()-verbose:gcvalues()values()values()values()

请注意,java.util.EnumSetjava.util.EnumMap 都需要使用枚举常量数组。为了提高性能,他们调用 JRE 专有代码,该代码将 的结果缓存在 存储在 中的共享数组中。您可以通过调用 自己访问该共享阵列,但是依赖它是不安全的,因为此类API不是任何规范的一部分,可以在任何Java更新中更改或删除。values()java.lang.Classsun.misc.SharedSecrets.getJavaLangAccess().getEnumConstantsShared(MyEnumType.class)

结论:

  • enum 方法的行为必须像总是分配一个新数组一样,以防调用方修改它。values()
  • 在某些情况下,编译器或 VM 可能会优化该分配,但显然它们不会。
  • 在性能关键型代码中,非常值得使用您自己的阵列副本。