理论上,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() 方法,该方法通过在 $VALUES 数组上调用 .clone() 并返回结果来工作:
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
}
然而,values() 方法必须返回一个新数组这一事实并不意味着编译器必须使用该方法。编译器可能会检测到MyEnumType.values()[ordinal] 的使用,并且看到数组没有被修改,它可以绕过该方法并使用底层的$VALUES 数组。上面main方法的反汇编表明javac确实没有做这样的优化。
我还测试了 ECJ。反汇编显示 ECJ 还初始化了一个隐藏数组来存储常量(尽管 Java langspec 不需要这样做),但有趣的是,它的values() 方法更喜欢创建一个空白数组然后用System.arraycopy 填充它,而不是调用@ 987654344@。无论哪种方式,values() 每次都会返回一个新数组。和 javac 一样,它不会尝试优化序数查找:
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 上运行它。使用values() 方法的每个测试大约需要 10 秒,而使用私有数组的每个测试大约需要 2 秒。使用-verbose:gc JVM 参数表明,在使用values() 方法时存在大量垃圾收集活动,而在使用私有数组时则没有。在客户端 VM 上运行相同的测试,私有阵列仍然很快,但 values() 方法变得更慢,需要一分钟多的时间才能完成。定义的枚举常量越多,调用 values() 也需要更长的时间。所有这些都表明values() 方法确实每次都分配一个新数组,避免它可能是有利的。
注意java.util.EnumSet 和java.util.EnumMap 都需要使用枚举常量数组。为了提高性能,他们调用 JRE 专有代码来缓存 values() in a shared array 存储在 java.lang.Class 中的结果。您可以通过调用 sun.misc.SharedSecrets.getJavaLangAccess().getEnumConstantsShared(MyEnumType.class) 自己访问该共享数组,但依赖它是不安全的,因为此类 API 不是任何规范的一部分,并且可以在任何 Java 更新中更改或删除。
结论:
- 枚举
values() 方法必须表现得好像它总是分配一个新数组,以防调用者修改它。
- 在某些情况下,编译器或 VM 可能会优化该分配,但显然它们不会。
- 在对性能要求严格的代码中,最好使用自己的数组副本。