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

我需要将一个序数int值转换为Java中的枚举值。 这很简单:

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

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

MyEnumType.values()是否分配了一个新数组? 如果确实如此,我应该在第一次调用时缓存数组吗? 假设转换将经常被调用。

是的, MyEnumType.values()每次都会创建一个用枚举元素填充的新数组。 您可以使用==运算符简单地测试它。

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

Java没有让我们创建不可修改数组的机制。 因此,如果values()总是返回相同的数组实例,那么我们就有可能会有人为每个人更改其内容。
因此,直到将不可修改的数组机制引入Java,避免数组可变性问题values()方法必须创建并返回原始数组的副本。

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

  • 你可以创建私有数组,并允许只通过getter方法访问其内容

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

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

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

我无法找到它的源代码

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

查找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)每个枚举常量的static final字段,(2)包含所有常量的隐藏$VALUES数组,(3)静态初始化块实例化每个常量并将每个常量分配给它的命名字段和数组,以及(4)通过在$VALUES数组上调用.clone()并返回结果的values()方法:

 final class MyEnumType extends java.lang.Enum { 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."":(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 "":(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 "":(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 "":(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 } 

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

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

 final class MyEnumType extends java.lang.Enum { 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 "":(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 "":(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 "":(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."":(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上运行它。 使用values()方法的每个测试大约需要10秒,而使用私有arrays的每个测试大约需要2秒。 使用-verbose:gc JVM参数显示使用values()方法时存在重要的垃圾收集活动,而使用私有数组时则没有。 在客户端虚拟机上运行相同的测试,私有arrays仍然很快,但是values()方法变得更慢,花了一分多钟才完成。 调用values()也需要更长的时间来定义更多的枚举常量。 所有这些都表明values()方法确实每次都会分配一个新数组,并且避免它可能是有利的。

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

结论:

  • 枚举values()方法必须表现得好像总是分配一个新数组,以防来电者修改它。
  • 编译器或虚拟机可能会在某些情况下优化分配,但显然他们没有。
  • 在性能关键代码中,非常值得使用您自己的数组副本。