关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用。这是一种非常有用的功能。
基本enum特征
我们调用enum的values()方法可以遍历enum实例。values()方法返回enum实例的数组,而且该数组中的元素严格保持其在enum中声明时的顺序,因此你可以在循环中使用values()返回的数组。
创建enum时,编译器会为你生成一个相关的类,这个类继承自java.lang.Enum。下面的例子演示了Enum提供的一些功能:
enum Shrubbery { GROUND,CRAWLING,HANGING}
public class Test {
public static void main(String[] args) {
for(Shrubbery s : Shrubbery.values()){
System.out.print(s.ordinal()+",");
System.out.print(s.name()+",");
System.out.print(s.compareTo(Shrubbery.GROUND)+",");
System.out.print(s.equals(Shrubbery.CRAWLING)+",");
System.out.println(s == Shrubbery.HANGING);
}
}
//输出:
//0,GROUND,0,false,false
//1,CRAWLING,1,true,false
//2,HANGING,2,false,true
}
向enum中添加方法
除了不能继承自一个enum之外,我们基本上可以将enum看作一个常规的类。也就是说,我们可以向enum中添加方法。甚至可以有main()方法。
一般来说,我们希望每个枚举实例能够返回对自身的描述,而不仅仅只是默认的toString()实现,这只能返回枚举实例的名字。为此,你可以提供一个构造器,专门负责处理这个额外的信息,然后添加一个方法,返回这个描述信息。看一看下面的示例:
enum OzWitch {
WEST("is WESST"),
NORTH("is NORTH"),
EAST("is EAST"),
SOUTH("is SOUTH");
private String str;
private OzWitch(String str) {
this.str = str;
}
public String getStr(){return str;}
public static void main(String[] args) {
for(OzWitch o:OzWitch.values()){
System.out.println(o+","+o.getStr());
}
}
//输出:
//WEST,is WESST
//NORTH,is NORTH
//EAST,is EAST
//SOUTH,is SOUTH
}
注意,如果你打算定义自己的方法,那么必须在enum实例序列的最后添加一个分号。同时Java要求你必须先定义enum实例,如果在定义enum实例之前定义了任何方法或属性,那么在编译时就会得到错误信息。
enum中的构造器与方法和普通的类没有区別,因为除了有少许限制之外enum就是一个普通的类。所以我们可以使用enum做许多事情(虽然我们一般只使用普通的枚举类型)。
在这个例子中,虽然我们有意识地将enum的构造器声明为private,但对于它的可访问性而言,其实并没有什么变化,因为(即使不声明为private)我们只能在enum定义的内部使用其构造器创建enum实例。一旦ennm的定义结束,编译器就不允许我们再使用其构造器来创建任何实例了。
switch语句中的enum
在switeh中使用enum,是enum提供的一项非常便利的功能。一般来说,在switch中只能使用整数值,而枚举实例天生就具备整数值的次序,并且可以通过ordinal()方法取得其次序(显然编译器帮我们做了类似的工作),因此我们可以在switch语句中使用enum。
虽然一般情况下我们必须使用enum类型来修饰一个enum实例,但是在case话句中却不必如此。例:
enum OzWitch {
WEST("is WESST"),
NORTH("is NORTH"),
EAST("is EAST"),
SOUTH("is SOUTH");
private String str;
private OzWitch(String str) {
this.str = str;
}
public String getStr(){return str;}
public static void main(String[] args) {
OzWitch o = OzWitch.SOUTH;
switch(o){
case WEST:
System.out.println("WEST");
break;
case NORTH:
System.out.println("NORTH");
break;
case EAST:
System.out.println("EAST");
break;
case SOUTH:
System.out.println("SOUTH");
break;
}
}
//输出:
//SOUTH
}
values()的神秘之处
前面已经提到,编译器为你创建的enum类都继承自Enum类。然而,如果你研究一下Enum 类就会发现,它并没有vaIues()方法。可我们明明已经用过该方法了,难道存在某种“隐藏的方法吗?”
values()是由编译器添加的static方法。编译器还会添加valueOf()方法。这可能有点令人迷惑,Enum类不是已经有valaeof()方法了吗。 不过Enum中的valueOf()方法需要两个参数,而这个新增的方法只需一个参数。
编译器会将ennum标记为final类,所以无法继承自enum。还会有一个static的初始化子句,稍后我们将学习如何重定义该句。
由于values()方法是由编译器插入到enum定义中的static方法,所以,如果你将enum实例向上转型为Enum,那么value()方法就不可访问了。不过在class中有一个getEnumConstants()方法,所以即使Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有enum实例。
enum OzWitch {
WEST(),
NORTH(),
EAST(),
SOUTH();
public static void main(String[] args) {
OzWitch o = OzWitch.EAST;
for(OzWitch to:o.getClass().getEnumConstants()){
System.out.println(to);
}
}
//输出
//WEST
//NORTH
//EAST
//SOUTH
}
因为getEnumConstant()是Class上的方法,所以你甚至可以对不是枚举的类调用此方法。
实现,而非继承
我们已经知道所有的enum都继承自java.lang.Enum类。由于Java不支持多重继承,所以你的enum不能再继承其他类:
enum Notposible extends Pet{ … //won’t work
然而在我们创建一个新的enum时可以同时实现一个或多个接口:
enum OzWitch implements Generator<OzWitch>{
WEST(),
NORTH(),
EAST(),
SOUTH();
public static void main(String[] args) {
OzWitch o = OzWitch.EAST;
for(OzWitch to:o.getClass().getEnumConstants()){
System.out.println(to);
}
}
@Override
public OzWitch next() {
return null;
}
}
不过你必须要有一个enum实例才能调用next()方法。
使用接口组织枚举
无法从enum继承子类有时很令人沮丧。这种需求有时源自我们希望扩展原enum中的元素,有时是因为我们希望使用子类将一个enum中的元素进行分组。
在一个接口的内部创建实现该接口的枚举,以此将元素进行分组,可以达到将枚举元素分类组织的目的。举例来说,假设你想用enum来表示不同类别的食物,同时还希望每个enum元素仍然保持Food类型。那可以这样实现:
interface Food{
enum Appetizer implements Food{
SALAD,SOUP
}
enum MainCourse implements Food{
LASAGNE,BURRITO
}
}
对于enum而言,实现接口是使其子类化的唯一办法,所以嵌入在Food中的每个enum都实现了Food接口。现在,在下面的程序中,我们可以说“所有东西都是某种类型的Food”:
public class Test {
public static void main(String[] args) {
Food f = Appetizer.SOUP;
}
}
然而,当你需要与一大堆类型打交道时,接口就不如enum好用了。例如,如果你想创建一个“枚举的枚举”,那么可以创建一个新的enum,然后用其实例包装Food中的每一个enum类:
enum Course{
Appetizer(Food.Appetizer.class),
MainCourse(Food.MainCourse.class);
private Food[] values;
private Course(Class<? extends Food> c) {
this.values = c.getEnumConstants();
}
}
public class Test {
public static void main(String[] args) {
for (Course c : Course.values()) {
for(Food f : c.getValues()){
System.out.println(f);
}
System.out.println("-----");
}
}
//输出:
//SALAD
//SOUP
//-----
//LASAGNE
//BURRITO
//-----
}
在上面的程序中,每一个Course的实例都将其对应的Class对象作为构造器的参数。通过getEnumConstants()方法,可以从该Class对象中取得某个Food子类的所有enum实例。
使用EnumSet代替标识
Set是一种集合,只能向其中添加不重复的对象。当然enum也要求其成员都是唯一的,所以enum看起来也具有集合的行为。不过由于不能从enum中删除或添加元素,所以它只能算是不太有用的集合。Java SE5引入EnumSet是为了通过enum创建一种替代品,以替代传统的基于int的“位标志”。这种标志可以用来表示某种“开/关”信息,不过使用这种标志我们最终操作的只是一些bit,而不是这些bit想要表达的概念,因此很容易写出令人难以理解的代码。
EnumSet的设计充分考虑到了速度因素,因为它必须与非常高效的bit标志相竞争,(其操作与HashSet相比非常地快)。就其内部而言,它(可能)就是将一个long值作为比特向量,所以EnumSet非常快速高效。使用EnumSet的优点是,它在说明一个二进制位是否存在时,具有更好的表达能力,并且无需担心性能。例:
enum Course{
STAIR1,STAIR2,STAIR3
}
public class Test {
public static void main(String[] args) {
EnumSet<Course> e = EnumSet.noneOf(Course.class);
e.add(Course.STAIR1);
System.out.println(e);
e.addAll(EnumSet.of(Course.STAIR1,Course.STAIR2,Course.STAIR3));
System.out.println(e);
e.removeAll(EnumSet.range(Course.STAIR1, Course.STAIR2));
System.out.println(e);
//...
}
//输出:
//[STAIR1]
//[STAIR1, STAIR2, STAIR3]
//[STAIR3]
}
EnumSet的基础是Iong,一个long值有64位,而一个enum实例只需要一位bit表示其是否存在。
也就是说,在不超过一个long的表达能力的情况下,你的EnumSet可以应用于最多不超过64个元素的enum。EnumSet可以应用于多过64个元素的enum,所以我猜测,Enum会在必要的时候增加一个long。
使用EnumMap
EnumMap是一种特殊的Map,它要求其中的键(key)必须来自一个enum。由于enum本身的限制,所以EnumMap在内部可由数组实现。因此EnumMap的速度很快,我们可以放心地使用enum实例在EnunMap中进行査找操作。不过我们只能将enum的实例作为键来调用put()方法,其他操作与使用一般的Map差不多。
常量相关的方法
Java的enum有一个非常有趣的特性,即它允许程序员为enum实例编写方法,从而为每个emm实例给予各自不同的行为。要实现常量相关的方法你需要为enmn定义一个或多个abstract方法,然后为每个enum实例实现该抽象方法。例:
enum Course{
STAIR1 {
@Override
String getInfo() {
return "STAIR1_INFO";
}
},STAIR2 {
@Override
String getInfo() {
return "STAIR2_INFO";
}
},STAIR3 {
@Override
String getInfo() {
return "STAIR3_INFO";
}
};
abstract String getInfo();
}
public class Test {
public static void main(String[] args) {
for(Course c : Course.values()){
System.out.println(c.getInfo());
}
}
//输出:STAIR1_INFO
//STAIR2_INFO
//STAIR3_INFO
}
通过相应的enum实例,我们可以调用其上的方法。这通常也称为*表驱动的代码(tabledriven code,请注意它与前面提到的命令模式的相似之处)。
在面向对象的程序设计中,不同的行为与不同的类关联。而通过常量相关的方法,每个enum实例可以具备自己独特的行为,这似乎说明每个enum实例就像一个独特的类。在上面的例子中,enum实例似乎被当作其“超类” Course来使用,在调用getlnfo()方法时,体现出多态的行为。
然而enum实例与类的相似之处也仅限于此了。我们并不能真的将enum实例作为一个类型来使用。
- 本文来源《Java编程思想(第四版)》