枚举类型


关键字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实例作为一个类型来使用。


  1. 本文来源《Java编程思想(第四版)》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值