在某些情况下,一个类的对象是有限而且固定的,比如季节类,它只有4个对象:春、夏、秋、冬。这种实例有限而且固定的类,在Java里被称为枚举类。
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
package
com.demo2;
public
class
Season {
public
static
final
Season SPRING =
new
Season(
"春天"
,
"趁春踏青"
);
public
static
final
Season SUMMER =
new
Season(
"夏天"
,
"夏日炎炎"
);
public
static
final
Season FALL =
new
Season(
"秋天"
,
"秋高气爽"
);
public
static
final
Season WINTER =
new
Season(
"冬天"
,
"围炉观雪"
);
public
static
Season getSeason(
int
seasonNum) {
switch
(seasonNum) {
case
1
:
return
SPRING;
case
2
:
return
SUMMER;
case
3
:
return
FALL;
case
4
:
return
WINTER;
default
:
return
null
;
}
}
private
final
String name;
private
final
String desc;
private
Season(String name, String desc) {
this
.name = name;
this
.desc = desc;
}
public
String getName() {
return
name;
}
public
String getDesc() {
return
desc;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
package
com.demo2;
public
class
Test2 {
public
Test2(Season s) {
System.out.println(s.getName() +
"-----"
+ s.getDesc());
}
public
static
void
main(String[] args) {
new
Test2(Season.SPRING);
}
}
|
如果需要手动实现枚举,可以采用如下设计方式:
-
通过private把构造器隐藏起来。
-
把这个类的所有实例都使用public static final修饰的类变量来保存。
-
如果有必要,提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例。
PS:有些程序员喜欢使用简单地定义几个静态常量来表示枚举类。
例如:
1
2
3
4
|
public
static
final
int
SEASON_SPRING=
1
;
public
static
final
int
SEASON_SUMMER=
2
;
public
static
final
int
SEASON_FALL=
3
;
public
static
final
int
SEASON_WINTER=
4
;
|
这种方式存在几个问题:
-
类型不安全。例子中都是整型,所以季节可以进行+,-,*,/等int运算。
-
没有命名空间。这些静态常量只能靠前缀“SEASON_”来划分所属类型。
-
打印输出,意义不明确。如果使用System.out.println()打印SEASON_SPRING,输出是1,不是SPRING。
2 枚举类Enum
JDK 5新增了一个关键字Enum,它与class,interface的地位相同,用来定义枚举类。枚举类其实是一个特殊的类,它可以有自己的Field,方法,构造函数,可以实现一个或多个接口。
2.1 定义枚举类
示例:
1
2
3
4
5
|
package
com.demo2;
public
enum
SeasonEnum{
SPRING, SUMMER, FALL, WINTER;
}
|
编译该段代码,将生成一个SeasonEnum.class文件,说明枚举类是一种特殊的类。
定义枚举类时,需要显式地列出所有的枚举值,例如上面的SPRING, SUMMER, FALL, WINTER;所示,所有的枚举值之间以英文逗号(,)隔开,枚举值列举结束后,以英文分号作为结束。这些枚举值代表了该枚举类的所有可能实例。
2.2 枚举类本质
将2.1中生成的SeasonEnum.class文件反编译:
1
|
javap -c SeasonEnum.
class
> SeasonEnum.txt
|
结果如下:
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
Compiled from
"SeasonEnum.java"
public
final
class
com.demo2.SeasonEnum
extends
java.lang.Enum<com.demo2.SeasonEnum> {
public
static
final
com.demo2.SeasonEnum SPRING;
public
static
final
com.demo2.SeasonEnum SUMMER;
public
static
final
com.demo2.SeasonEnum FALL;
public
static
final
com.demo2.SeasonEnum WINTER;
static
{};
Code:
0
:
new
#
1
// class com/demo2/SeasonEnum
3
: dup
4
: ldc #
15
// String SPRING
6
: iconst_0
7
: invokespecial #
16
// Method "<init>":(Ljava/lang/String;I)V
10
: putstatic #
20
// Field SPRING:Lcom/demo2/SeasonEnum;
13
:
new
#
1
// class com/demo2/SeasonEnum
16
: dup
17
: ldc #
22
// String SUMMER
19
: iconst_1
20
: invokespecial #
16
// Method "<init>":(Ljava/lang/String;I)V
23
: putstatic #
23
// Field SUMMER:Lcom/demo2/SeasonEnum;
26
:
new
#
1
// class com/demo2/SeasonEnum
29
: dup
30
: ldc #
25
// String FALL
32
: iconst_2
33
: invokespecial #
16
// Method "<init>":(Ljava/lang/String;I)V
36
: putstatic #
26
// Field FALL:Lcom/demo2/SeasonEnum;
39
:
new
#
1
// class com/demo2/SeasonEnum
42
: dup
43
: ldc #
28
// String WINTER
45
: iconst_3
46
: invokespecial #
16
// Method "<init>":(Ljava/lang/String;I)V
49
: putstatic #
29
// Field WINTER:Lcom/demo2/SeasonEnum;
52
: iconst_4
53
: anewarray #
1
// class com/demo2/SeasonEnum
56
: dup
57
: iconst_0
58
: getstatic #
20
// Field SPRING:Lcom/demo2/SeasonEnum;
61
: aastore
62
: dup
63
: iconst_1
64
: getstatic #
23
// Field SUMMER:Lcom/demo2/SeasonEnum;
67
: aastore
68
: dup
69
: iconst_2
70
: getstatic #
26
// Field FALL:Lcom/demo2/SeasonEnum;
73
: aastore
74
: dup
75
: iconst_3
76
: getstatic #
29
// Field WINTER:Lcom/demo2/SeasonEnum;
79
: aastore
80
: putstatic #
31
// Field ENUM$VALUES:[Lcom/demo2/SeasonEnum;
83
:
return
public
static
com.demo2.SeasonEnum[] values();
Code:
0
: getstatic #
31
// Field ENUM$VALUES:[Lcom/demo2/SeasonEnum;
3
: dup
4
: astore_0
5
: iconst_0
6
: aload_0
7
: arraylength
8
: dup
9
: istore_1
10
: anewarray #
1
// class com/demo2/SeasonEnum
13
: dup
14
: astore_2
15
: iconst_0
16
: iload_1
17
: invokestatic #
39
// Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
20
: aload_2
21
: areturn
public
static
com.demo2.SeasonEnum valueOf(java.lang.String);
Code:
0
: ldc #
1
// class com/demo2/SeasonEnum
2
: aload_0
3
: invokestatic #
47
// Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6
: checkcast #
1
// class com/demo2/SeasonEnum
9
: areturn
}
|
java.lang.Enum的源码结构如下:
根据字节码和java.lang.Enum源码结构可以得出如下结论:
-
使用enum定义的枚举类默认直接继承了java.lang.Enum类,而不是直接继承java.lang.Object。其中java.lang.Enum实现了java.lang.Comparable<E>, java.lang.Serializable两个接口。
-
使用enum定义、非抽象的枚举类默认会使用final修饰,因此非抽象枚举类不能派生子类。
-
枚举类的构造函数只能使用private访问控制符。如果省略了构造函数的访问控制符,默认会使用private修饰;如果强制指定访问控制符,只能使用private。
-
枚举类的所有实例必须在枚举类的第一行显示列出,否则这个枚举类永远不都不能产生实例。列出这些实例时,系统会自动添加public statci final修饰符,无须程序员显示添加。
Java实现枚举的本质:
-
java.lang.Enum定义枚举对象的组成成分。一个枚举对象包含两个属性“ordinal”和“name”,一系列实例方法,例如toString、compareTo等等。“ordinal”是索引值,根据枚举声明的位置赋值,从0开始。“name”是枚举对象的名称。例如public enum SeasonEnum{SPRING, SUMMER, FALL, WINTER;}中,SPRING的索引值为0,name为“SPRING”;SUMMER的索引值为1,name为“SUMMER”.........。
-
编译时,编译器(javac)在自定义枚举类的.class文件中,添加static{}初始化块,初始化块包括生成枚举对象的指令。在示例字节码中,static{}初始化块里依次生成SPRING枚举对象并赋值索引值0,name为“SPRING”,生成SUMMER枚举对象并赋值索引值1,name为“SUMMER”..........最后,在SeasonEnum类中还定义了一个静态成员“ENUM$VALUES”,它是一个SeasonEnum数组,依据索引对象的“ordinal”值按顺序存放索引对象。
2.3 枚举类与switch结构
如果需要使用枚举类的某个实例,可以使用“枚举类.某个实例”的形式,例如SeasonEnum.SPRING。
示例:
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
|
package
com.demo2;
public
class
Test {
public
static
void
judge(SeasonEnum s) {
switch
(s) {
case
SPRING:
System.out.println(
"春天,趁春踏青"
);
break
;
case
SUMMER:
System.out.println(
"夏天,夏日炎炎"
);
break
;
case
FALL:
System.out.println(
"秋天,秋高气爽"
);
break
;
case
WINTER:
System.out.println(
"冬天,围炉观雪"
);
break
;
}
}
public
static
void
main(String[] args) {
judge(SeasonEnum.FALL);
}
}
|
上面程序中的switch表达式中,使用了SeasonEnum对象作为表达式,这是JDK 5增加枚举后对switch的扩展:switch的控制表达式可以是任何枚举类型。不仅如此,当switch控制表达式使用枚举类型时,后面case表达式中的值直接使用枚举值名字,无须添加枚举类作为限定。
3 枚举类与构造函数
枚举类是一种特殊类,它也可以有自己的Field、方法和构造函数。但是从枚举“对象是有限而且固定的”含义层面上,枚举对象应该是状态不可变的对象,即枚举类的所有Field都应该用final修饰。因为Field都使用了final修饰符来修饰,所以必须在构造函数里为这些Field指定初始化值(或者在定义Field时指定默认值,或者在初始化块中指定初始值,但这两种情况并不常见),因此应该为枚举类显式定义带有参数的构造函数。
一旦为枚举类显式定义了带参数的构造函数,列出枚举值时,就必须对应地传入参数。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package
com.demo2;
public
enum
SeasonEnum {
SPRING(
"趁春踏青"
), SUMMER(
"夏日炎炎"
), FALL(
"秋高气爽"
), WINTER(
"围炉观雪"
);
private
final
String desc;
private
SeasonEnum(String desc) {
this
.desc = desc;
}
public
String getDesc() {
return
desc;
}
}
|
不难看出,上面程序中SPRING("趁春踏青"), SUMMER("夏日炎炎"), FALL("秋高气爽"), WINTER("围炉观雪");等同于如下代码:
1
2
3
4
|
private
static
final
SeasonEnum SPRING=
new
SeasonEnum(
"趁春踏青"
);
private
static
final
SeasonEnum SUMMER=
new
SeasonEnum(
"夏日炎炎"
);
private
static
final
SeasonEnum FALL=
new
SeasonEnum(
"秋高气爽"
);
private
static
final
SeasonEnum WINTER=
new
SeasonEnum(
"围炉观雪"
);
|
4 枚举类与接口
枚举类也可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法。
例如:
1
2
3
4
5
|
package
com.demo2;
public
interface
SeasonDesc {
void
info();
}
|
1
2
3
4
5
6
7
8
9
10
11
|
package
com.demo2;
public
enum
SeasonEnum
implements
SeasonDesc {
SPRING, SUMMER, FALL, WINTER;
@Override
public
void
info() {
System.out.println(
"这是一个定义春夏秋冬的枚举类"
);
}
}
|
枚举类实现接口不过如此,与普通类实现接口完全一样:使用implements实现接口,实现接口里包含的抽象方法。
如果由枚举类来实现接口里的方法,则每个枚举类对象在调用该方法时,都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举对象在调用该方法时,呈现出不同的行为方式,则可以让每个枚举对象分别来实现该方法。
示例:
1
2
3
4
5
|
package
com.demo2;
public
interface
SeasonDesc {
void
info();
}
|
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
|
package
com.demo2;
public
enum
SeasonEnum
implements
SeasonDesc {
SPRING {
@Override
public
void
info() {
System.out.println(
"趁春踏青"
);
}
},
SUMMER {
@Override
public
void
info() {
System.out.println(
"夏日炎炎"
);
}
},
FALL {
@Override
public
void
info() {
System.out.println(
"秋高气爽"
);
}
},
WINTER {
@Override
public
void
info() {
System.out.println(
"围炉观雪"
);
}
};
}
|
编译该代码,可以看到生成了”SeasonEnum.class“、”SeasonEnum$1.class“、”SeasonEnum$2.class“、”SeasonEnum$3.class“、”SeasonEnum$4.class“,这几个.class文件证明了结论:SPRING、SUMMER、FALL和WINTER实际上是SeasonEnum匿名子类的实例,而不是SeasonEnum类的实例。
反编译SeasonEnum.class:
1
|
javap -c SeasonEnum.
class
> se.txt
|
SeasonEnum.class字节码:
1
2
3
|
public
abstract
class
com.demo2.SeasonEnum
extends
java.lang.Enum<com.demo2.SeasonEnum>
implements
com.demo2.SeasonDesc {
...........
}
|
反编译SeasonEnum$1.class:
1
|
javap -c SeasonEnum$
1
.
class
> se1.txt
|
SeasonEnum$1.class字节码(SeasonEnum$2.class、SeasonEnum$3.class、SeasonEnum$4.class同理):
1
2
3
|
class
com.demo2.SeasonEnum$
1
extends
com.demo2.SeasonEnum {
..........
}
|
PS:枚举类不是应该用final修饰的吗?怎么还能派生子类呢?
并不是所有的枚举类都使用了final修饰。非抽象的枚举类才默认使用final修饰。对于一个抽象的枚举类而言(只要它包含了抽象方法,它就是抽象枚举类),系统会默认使用abstract修饰,而不是使用final修饰。
每个枚举对象提供不同的实现方式,从而让不同的枚举对象调用该方法时,具有不同的行为方式。
5 枚举类与抽象方法
如果不使用接口,而是直接在枚举类中定义一个抽象方法,然后再让每个枚举对象提供不同的实现方式。
示例:
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
|
package
com.demo2;
public
enum
SeasonEnum {
SPRING {
@Override
public
void
info() {
System.out.println(
"趁春踏青"
);
}
},
SUMMER {
@Override
public
void
info() {
System.out.println(
"夏日炎炎"
);
}
},
FALL {
@Override
public
void
info() {
System.out.println(
"秋高气爽"
);
}
},
WINTER {
@Override
public
void
info() {
System.out.println(
"围炉观雪"
);
}
};
abstract
void
info();
}
|
编译该代码,可以看到生成了”SeasonEnum.class“、”SeasonEnum$1.class“、”SeasonEnum$2.class“、”SeasonEnum$3.class“、”SeasonEnum$4.class“文件。这跟“4 枚举类与接口”的原理是一样的:SPRING、SUMMER、FALL和WINTER实际上是SeasonEnum匿名子类的实例,而不是SeasonEnum类的实例。
SeasonEnum是一个包含抽象方法的枚举类,但是“public enum SeasonEnum”没有使用abstract修饰,这是因为编译器(javac)会在编译生成.class时,自动添加abstract。同时,因为SeasonEnum包含一个抽象方法,所以它的所有枚举对象都必须实现抽象方法,否则编译器会报错。