zhen's blog

编译思想,编译人生

Java枚举细节

枚举的简单使用

在java中,我们可以使用enum关键字来定义枚举:

1
2
3
public enum Color {
RED, GREEN, BLUE;
}

就像上面一样,我们定义了一个名为Color的枚举类,包含了RED、GREEN、BLUE三个常量。当我们使用枚举类的时候,直接通过枚举类名.枚举常量即可。就像如下的形式:

1
2
3
4
5
6
7
8
9
10
void f(Color c) {
switch (c) {
case RED: System.out.println("It's RED"); break;
case GREEN: System.out.println("It's GREEN"); break;
default: System.out.println("It's BLUE");
}
}
// 调用
f(Color.RED);
// 输出 "It's RED"

枚举的本质

虽然Java提供枚举类的定义,但是实际上他并不是Java中一个新的对象类型,我们通过对Color枚举类进行反编译,得到如下的反编译结果:

1
2
3
4
5
6
7
8
9
10
11
$ javap Color.class 
// 反编译结果
Compiled from "Color.java"
public final class EnumerationAndAnnotation.Color extends java.lang.Enum<EnumerationAndAnnotation.Color> {
public static final EnumerationAndAnnotation.Color RED;
public static final EnumerationAndAnnotation.Color GREEN;
public static final EnumerationAndAnnotation.Color BLUE;
public static EnumerationAndAnnotation.Color[] values();
public static EnumerationAndAnnotation.Color valueOf(java.lang.String);
static {};
}

我们可以看到,枚举类实际上在编译的过程中,被编译器进行调整,它并不是一个新的类型,本质上依然是一个类(Color),这个类继承了java.lang.Enum,而对于每一个枚举常量,实际上是public static final修饰的枚举类的静态实例对象。

同时注意,编译器会为我们添加两个新的static方法:values() 和 valueOf(java.lang.String),其实分别作用是返回枚举类中定义的所有的枚举常量,以及根据枚举名来获取枚举常量(注意,这里就是定义枚举常量的枚举名)。

当然,由于每一个枚举常量实际上是实现了java.lang.Enum的枚举类的一个静态实例对象,而这个过程是编译器为我们进行的,所以,自然,我们可以在枚举类中定义任何的方法、变量,以及构造函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum Color {
RED("RED"), GREEN("GREEN"), BLUE("BLUE");

// 定义类中的实例变量
private String colorName;
// 定义构造函数,注意上面的枚举常量必须满足这种构造方式
Color(String colorName) {
this.colorName = colorName;
}
// 定义实例对象的方法
public void printColorName() {
System.out.println("Color name is " + colorName);
}
// 重写toString方法
@Override
public String toString() {
return colorName;
}
}

自然,我们可以枚举常量当作一个枚举的实例化对象,调用枚举类中的方法:

1
2
3
4
public static void main(String[] args) {
System.out.println(Color.RED); // RED
Color.RED.printColorName(); // Color name is RED
}

为什么枚举构造器不能访问枚举的静态域

查看下面这段代码:

1
2
3
4
5
6
7
public enum Color {
RED, GREEN, BLUE;
private static int value = 0;
Color() {
System.out.println(value); // 编译错误!构造器无法访问静态变量
}
}

我们可以看到这里编译不通过,提示我们枚举构造器不能够访问枚举的静态域(以及静态变量)。我们知道,一般的类中,静态域以及静态变量是优于实例对象的变量、方法的初始化的。这里简要复习一下类中变量的加载机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 辅助静态变量的初始化
class Init {
public Init(String init) {
System.out.println("Init()" + init);
}
}
class Fa {
// 父类静态变量
static final Init initFa = new Init("Fa's static");
// 父类实例变量
Init initFa2 = new Init("Fa's no static");
// 父类静态域
static {
System.out.println("Fa's static");
}
// 父类构造方法
Fa() {
System.out.println("Fa()");
}
}
class Su extends Fa {
// 子类静态域
static {
System.out.println("Su's static");
}
// 子类静态变量
static final Init initSu = new Init("Su's static");
// 子类实例变量
Init initSu2 = new Init("Su's no static");
// 子类构造函数
Su() {
System.out.println("Su()");
}
}

接下来,我们new出Su实例对象,并观察输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
new Su();
}
/* 输出
Init()Fa's static // 1
Fa's static // 2
Su's static // 3
Init()Su's static // 4
Init()Fa's no static// 5
Fa() // 6
Init()Su's no static// 7
Su() // 8
*/

我们可以看到,有static修饰的始终优于实例对象的相关的初始化的,在输出中 1 - 4 是static修饰部分,5 - 8是实例域部分。此外,在继承情形下,父类由于子,输出中 1- 2 是父类static域的初始化,3 - 4 是子类static域的初始化。在static域加载完成之后,才开始加载父类非static域,最后加载子类的非static域。注意,都为static修饰的情况下,加载顺序根绝定义时候的顺序而来,1、2与3、4就可以看出。

看到这里,也许会有点疑问,既然静态域加载优于实例域(包含构造函数),那为什么在枚举类中就不行呢?让我们回到前面对枚举类的反编译,其实答案就出来了。反编译的过程我们可以看到,我们的枚举常量实际上是我们枚举类的静态实例化对象,在编译器的修改下,我们运行加载枚举类的过程中,枚举常量是static修饰的,其他静态域也是static修饰的,枚举常量又排在其他静态域的前面,按照上面的额初始化顺序,首先就会调用构造器实例化枚举常量对象,此时,枚举类中的其他静态域都还没来得及初始化,自然在构造函数中不能访问静态域了。有人可能想说,那我静态域放到枚举常量前面,让他先加载怎么样?很遗憾,Java不允许这样做:

1
2
3
4
public enum Color { // 编译不通过!!!
private static int value = 0;
RED, GREEN, BLUE;
}

使用抽象函数为枚举统一方法

上面的内容探讨了枚举类的一些基础,这里提一些关于使用枚举的代码策略设计。
有的时候,我们想要给枚举常量定义某一些通用的方法,同时,针对不同的枚举,该通用方法呈现不同的具体内容。例如,我现在有一个如下的Color枚举类,当针对不同的Color常量的时候,能有一个方式给我返回该颜色的十六进制颜色码。也许你会如下来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum Color {
RED, GREEN, BLUE;
public String getHexCode() {
switch (this) {
case RED:
return "#FF0000";
case GREEN:
return "#00FF00";
default:
return "#0000FF";
}
}
}

以上的方式较为简洁与易读,但是存在问题:枚举常量越来越多,case会不断增长,如果编写代码的过程由于不注意,增加颜色常量的时候,忘记了增加对应的case,那么编译是不会有任何的问题的,但是却隐含的将增加的颜色常量也返回的是BLUE的十六进制颜色码。

鉴于上述的问题,我们需要某种方式来防止我们犯错,能够想到的,就是通过编译器来告诉我们。于是,我们在枚举类中定一个抽象方法getHexCode,于是乎,对于每一个枚举常量,编译器会提示我们实现具体实例的getHexCode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum Color {
RED {
@Override
public String getHexCode() {
return "#FF0000";
}
}, GREEN {
@Override
public String getHexCode() {
return "#00FF00";
}
}, BLUE {
@Override
public String getHexCode() {
return "#0000FF";
}
};

abstract public String getHexCode();
}

在这样的定义下,代码牺牲了一定的简洁性,但是易读性丝毫不输于最开始的方式。针对Color枚举类,我们定义了抽象方法,表明了对于Color中的每一个枚举常量,都应该有getHexCode方法,返回自己的十六进制颜色码。如果我们新添加了枚举常量,而没有实现该方法,编译器会报错警告我们。