普通类是class
,枚举是enum
,看起来完全不是一种东西,而且写起来也差别很大,那么枚举到底是什么?编译完之后的枚举又是什么样子的?
这就是本文探讨的内容。
考虑到不同编译器有时候差别很大,这次使用了两种常见的编译器:
javac
$ javac -version
javac 1.7.0_80
ECJ(Eclipse Compiler for Java)
$ java -jar /d/eclipse/plugins/org.eclipse.jdt.core_3.11.2.v20160128-0629.jar -version
Eclipse Compiler for Java(TM) v20160128-0629, 3.11.2, Copyright IBM Corp 2000, 2015. All rights reserved.
就以jls中使用的Color枚举为例:
/* Color.java */
public enum Color {
RED, GREEN, BLUE;
}
就这段简单的代码,编译之后会变成什么样子呢?
我们需要阅读编译完之后的Color.class
文件,但是很多反编译器不会输出这个文件的全部内容,而是为了尽量与源码吻合,删去了编译时添加的内容。因此使用javap -v -p Color
命令反汇编得到字节码,阅读字节码即可还原出如下内容:
public final class Color extends Enum<Color> {
public static final Color RED;
public static final Color GREEN;
public static final Color BLUE;
// ---------- javac ----------
private static final Color[] $VALUES;
public static Color[] values() {
return $VALUES.clone();
}
// ---------------------------
// ---------- ECJ ----------
// private static final Color[] ENUM$VALUES;
//
// public static Color[] values() {
// Color[] copy = new Color[ENUM$VALUES.length];
// System.arraycopy(ENUM$VALUES, 0, copy, 0, ENUM$VALUES.length);
// return copy;
// }
// -------------------------
public static Color valueOf(String name) {
return (Color) Enum.valueOf(Color.class, name);
}
private Color(String name, int ordinal) {
super(name, ordinal);
}
static {
RED = new Color("RED", 0);
GREEN = new Color("GREEN", 0);
BLUE = new Color("BLUE", 0);
$VALUES = new Color[3];
$VALUES[0] = RED;
$VALUES[1] = GREEN;
$VALUES[2] = BLUE;
}
}
大家需要注意的是,这段Java代码是无法编译通过的。
通过上面的代码我们可以得出一些结论:
enum
,在编译时会变成public final class
,并且自动继承了Enum<E>
。这个动作必须由编译器来完成,直接这样写编译报错。public static final
的常量。private
的构造函数,接收两个参数,一个是枚举对象的名字,一个是位置。在构造函数中直接调用了super(String, int)
,即java.lang.Enum
的protected
构造函数来构造对象。static
代码块,来初始化所有的枚举对象,并添加到自动生成的一个数组常量中存储起来。这个数据常量的名字不是固定的,完全取决于编译器,ECJ编译的是ENUM$VALUES
,javac编译的是$VALUES
。public static
方法values()
,这个是大家很常用的方法,返回所有枚举对象的数组,原来也是在编译时期隐式添加的。两种编译器对此方法的实现稍有不同。public static
方法valueOf(String)
,直接调用了Enum.valueOf
。我们看到,javac编译出的类有个$VALUES
,ECJ编译出的类有个ENUM$VALUES
,这个名字有什么特别之处吗?
首先,我们增加一个枚举值,名字就叫$VALUES
,然后用javac编译,看看会发生什么:
public enum Color {
RED, GREEN, BLUE, $VALUES;
}
javac编译之后大概是这个样子:
public static final Color RED;
public static final Color GREEN;
public static final Color BLUE;
public static final Color $VALUES;
private static final Color[] $VALUES$;
看来编译器发现$VALUES
被占用之后,就改成创建$VALUES$
了。接下来再试试,我们把$VALUES$
也占用了,果然,编译器创建了一个$VALUES$$
。
同样的套路,接下来试一试ECJ。在三种颜色枚举值后面加一个名字叫ENUM$VALUES
的枚举值,发现编译器创建了一个ENUM$VALUES_0
;再加一个ENUM$VALUES_0
,编译器就创建一个ENUM$VALUES_1
。。
调戏编译器虽然好玩,但是我们也能发现一个结论,就是这个变量的命名是随意的,没有规范的。想反射的同学就需要注意了,通过变量名来反射读取这个数组看来是不太靠谱。
上面我们得知编译时会自动生成枚举对象的构造函数,但是在枚举类中自定义构造函数也是很常见的操作,那么会不会发生冲突?到底发生了什么事情?
我们改一下代码,变成下面的样子:
public enum Color {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
public int code;
Color(int code) {
this.code = code;
System.out.println("Color " + this + " initialized, code = " + code);
}
}
同样编译、反汇编之后,翻译得到的构造函数变成了这样:
private Color(String name, int ordinal, int code) {
super(name, ordinal);
this.code = code;
System.out.println("Color " + this + " initialized, code = " + code);
}
原来,如果有构造函数的话,会在你的构造函数上强制添加两个参数,而这两个参数是调用super()
必需的。
我还验证了一下,如果有多个构造函数,每个构造函数都会被添加参数。
我们注意到,编译时会自动添加一个static {}
,来完成所有的枚举对象的构造和初始化。这和普通的类很不一样,因为普通的类是先初始化类,再初始化对象的。那么,我们需要清晰地掌握枚举类的初始化过程,以免出错。
我们来看下面的代码。
注意代码中,有个static {}
来打印RED
,如果打印的时候已经构造好了RED
对象,则打印结果为RED
;如果还没有构造RED
对象,则会打印出null
。
这两种结果截然不同,我们来分析一下:
public enum Color {
RED, GREEN, BLUE;
static final String STR = "STR";
static String str = "str";
static {
System.out.println(RED);
}
Color() {
System.out.println(STR);
// 编译报错:Cannot refer to the static enum field Color.str within an initializer
// System.out.println(str);
}
}
同样编译、反汇编之后,翻译得到的static代码块变成了这样:
static {
RED = new Color("RED", 0);
GREEN = new Color("GREEN", 0);
BLUE = new Color("BLUE", 0);
$VALUES = new Color[3];
$VALUES[0] = RED;
$VALUES[1] = GREEN;
$VALUES[2] = BLUE;
str = "str";
System.out.println(RED);
}
我们看到,枚举对象的构造和赋值先于所有的static
代码。
我们可以得出结论:
枚举类的初始化和枚举对象的初始化过程和普通的类完全不同。普通的类是先完成类的初始化,然后才构造对象,构造对象的时候所有的静态成员都已经准备好了;但是枚举不同,枚举类初始化的第一步就是要构造所有的枚举对象,也就是说,枚举对象构造时,静态成员还完全没有准备好,这也是为什么在枚举对象的构造函数中,引用非final
的static
成员会编译不通过。