枚举的由来
首先是枚举类型的由来,在编程语言还没有引入枚举类型之前,程序员用来表示枚举类型的模式一般是声明一组具名的int常量,比如表示水果Fruit:1
2
3
4public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int ORANGE_NAVEL = 2;
public static final int ORANGE_TEMPLE = 3;
这叫int枚举模式,这种模式有很多缺陷,在类型安全性和使用方便性上没有任何帮助。因为都是int类型,所以需要apple类型的时候传入orange编译器也不会报错。甚至apple和orange之间还能使用==进行比较而不会出现编译错误,这样带来的后果是灾难性的,隐藏的bug很难找出来。另外这种模式对于debug也很不方便(都是数字,没有太大用处)。也有一种模式使用String代替int,叫String枚举模式,但是导致性能问题,因为很依赖字符串操作;而且字符串书写错误也会造成很多问题,因为编译时并不会报错。
所以java1.5开始,提出了enum类型,也就是今天的主角。使用枚举写上面的代码是这样的:1
2
3
4
5
6public enum Apple {
FUJI,PIPPIN
}
public enum Orange {
NAVEL,TEMPLE
}
看着简单,其实内部还是挺复杂的,而且功能也非常强悍。
枚举的核心
首先要明确的是枚举的本质还是int值。但是这个int值没有完全的暴露给开发者(当然可以通过ordinal获得,其实就是序数),事实上大部分情况我们都不需要这个int值。
枚举类型背后的基本想法很简单,枚举就是通过公有静态final域为每个枚举常量导出实例的类。
枚举没有可以访问的构造器,所以是真正的final。我们不能继承枚举,不能new枚举实例,甚至可能没有实例(如果没有生命枚举常量)。也就是说枚举是实例受控的。
想要了解一个枚举背后的东西,我们反编译下,看看class文件就明白了,我们反编译下Apple枚举:
可以看到,编译后的Apple其实就是一个继承了Enum的普通类,只是多了一个ACC_ENUM标志位。
在看class的内容,成员变量里有两个static final变量,分别是FUJI和PIPPIN,这就是我们声明的那两个枚举变量。这两个变量都有final、static、enum标志位。
当然这里的例子只是讲了最简单的枚举,枚举的巧妙用法在后面说,同时也会通过反编译获得更多的信息。
枚举的用法-方法和域
枚举允许添加任意的方法和域,这使得我们可以将数据与枚举变量关联起来,这个特性会极大的增强枚举的能力。比如下面的例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public enum Person {
MAN(20),WOMEN(15);
private final int height;
private Person(int height){
this.height = height;
}
public int getHeight(){
return height;
}
public int getWeight(){
switch (this){
case MAN:
return height * 10;
case WOMEN:
return height * 5;
default:
break;
}
return 0;
}
}
一个可以返回男女身高体重,还能根据类型区分计算的Person类,是不是很方便,我们反编译下看看:
常量池多了一个int类型的非静态height变量,说明这是变量是要由对象维护的,哪个对象呢?自然是Person的两个实例MAN和WOMAN。
这里多了两个非静态的getHeight和getWeight方法。所以说,在编译器编译过后,枚举就是一个普通类。
枚举的用法-枚举变量和行为
上面介绍的方法几乎能满足所有情况了,但是还是有写情况不满足。比如下面: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
27public enum Fruit {
APPLE(5, 30),
ORANGE(2, 40);
private final int size;
private final int num;
Fruit(int size, int num){
this.size = size;
this.num = num;
}
public int getSize(){
return size;
}
public int getNum(){
return num;
}
public int getTotalWeight(){
switch (this){
case APPLE:
return size * num + 1;
case ORANGE:
return size * num + 2;
default:
break;
}
return size * num;
}
}
计算每种水果的重量的时候会根据不同水果区别计算,那么,假设,有一天新加了BANANA这个水果,但却忘了给switch增加相应的条件,那么返回结果必然是错的。这个时候就可以考虑使用抽象方法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
28public enum Fruit {
APPLE(5, 30){
@Override
int getCustomWeight(int size, int num) {
return size * num + 5;
}
},
ORANGE(2, 40){
@Override
int getCustomWeight(int size, int num) {
return size * num + 6;
}
};
private final int size;
private final int num;
Fruit(int size, int num){
this.size = size;
this.num = num;
}
abstract int getCustomWeight(int size, int num);
public int getSize(){
return size;
}
public int getNum(){
return num;
}
}
这样在声明新的枚举常量时必须实现这个抽象方法,也就不会忘了。一看到这个抽象方法,直觉告诉我们,既然编译后都是普通类,有需要实现抽象方法,那肯定会有新的类产生了,反编译看下
果然,编译后生成了三个内部类,其中前两个分别实现了抽象方法,因为javap不能直接反编译内部类,所以这里无法判断这两个内部类的继承关系,不过应该是继承了外部类,因为下图猜测的:
Fruit拥有的两个实例类型都还是Fruit,所以是Fruit$1和Fruit$2分别继承了Fruit并实现了抽象方法,也就是说具体起作用的是这两个实现类。另外由于Fruit是抽象类,所以还需要一个Fruit$3还继承实现它。
枚举策略
通过上面的分析介绍,我们知道,当希望将枚举常量与数据以及行为联系起来时,可以通过switch语句,缺点是不够安全,可能忘了相应的case,也可以通过抽象方法,但是这可能会导致大量样板代码的出现,比如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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49public enum Fruit {
APPLE(5, 30){
@Override
int getCustomWeight(int size, int num) {
return size * num + 5;
}
},
APPLE_1(5, 30){
@Override
int getCustomWeight(int size, int num) {
return size * num + 5;
}
},
ORANGE(2, 40){
@Override
int getCustomWeight(int size, int num) {
return size * num + 6;
}
};
private final int size;
private final int num;
Fruit(int size, int num){
this.size = size;
this.num = num;
}
abstract int getCustomWeight(int size, int num);
public int getSize(){
return size;
}
public int getNum(){
return num;
}
public int getTotalWeight(){
switch (this){
case APPLE:
return size * num + 1;
case ORANGE:
return size * num + 2;
default:
break;
}
return size * num;
}
}
这里Apple和Apple_1的重量计算方式一模一样,但又不得不都实现一遍,这就是缺陷,那么这时候枚举策略的代码方式就有用了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
27public enum Flower {
Rose_Red(Pricetype.Rose),Rose_Yellow(Pricetype.Rose),Tulip(Pricetype.Tulip);
private Pricetype pricetype;
private Flower(Pricetype pricetype){
this.pricetype = pricetype;
}
public int getTotalMoney(int num) {
return pricetype.price(num);
}
private enum Pricetype {
Rose{
@Override
int price(int num) {
return num * 5;
}
}, Tulip {
@Override
int price(int num) {
return num * 6;
}
};
abstract int price(int num);
}
}
几个方法
valueOf:valueOf让我们根据枚举常量的String名得到枚举常量,Enum类里有实现1
2
3
4
5
6
7
8
9
10public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
//Class类里专门为枚举类维护了一个map,存储String与Enum的键值对
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
values:遍历所有枚举,顺序是序数,该方法在编译时生成
end