JAVA提高二:枚举
JDK5.0中有一个非常有用的特性:枚举,这个特性以前在C语言中出现过,后来JDK出现后,开始觉得没有必要,但随着使用JAVA语言的人数增多,发现大家对枚举的需求非常大,于是又加入了此特性,下面我们来对枚举进行学习。
一、枚举的作用介绍
JDK5.0加入了一个全新类型的“类”——枚举类型。为此引入了一个新的关键字enum。可以这样来定义一个枚举类型:
public enum Color { RED,White,Blue }
然后可以这样来使用:Color myColor = Color.Red;
枚举(enum),是指一个经过排序的、被打包成一个单一实体的项列表。一个枚举的实例可以使用枚举项列表中任意单一项的值。枚举在各个语言当中都有着广泛的应用,通常用来表示诸如颜色、方式、类别、状态等等数目有限、形式离散、表达又极为明确的量。作用简单来说就是:限定某个值得取值范围,就和我们在页面下拉列表一样,只能在有限的枚举值中选择,而不能跳出其范围,为说明枚举的用处,我们举个例子:
public class Entity { private int id; private int type; public int getId() { return id; } public void setId(int id) { this.id = id; } public int getType() { return type; } public void setType(int type) { this.type = type; } }
上面定义了一个非常简单的实体类,其中类型采用的int类型来定义,当然,也不仅仅局限于int型,诸如char和String等也是不在少数。然而,无论使用什么样的类型,这样做都有很多的坏处。通常都是连续、有无穷多个值的量(比如例子中的实体int,那么setType的时候可以传入0 1 2 3 等等int),而类似这种表示类别type的量则是离散的,并且通常情况下只有有限个值。用连续的量去表示离散量,会产生很多问题。例如,针对上述的Entity类,如果要对Entity对象的type属性进行赋值,一般会采用如下方法:
Entity e = new Entity(); e.setId(10); e.setType(2);
这样做的缺点有:(1)代码可读性差、易用性低。由于setType()方法的参数是int型的,在阅读代码的时候往往会让读者感到一头雾水,根本不明白这个2到底是什么意思,代表的是什么类型。当然,要保证可读性,还有这样一个办法就是定义一个常量类,然后常量类中定义如下:
public class Const { public static final int VIDEO = 1;//视频 public static final int AUDIO = 2;//音频 public static final int TEXT = 3;//文字 public static final int IMAGE = 4;//图片 }
然后:
e.setType(Const.AUDIO);
而这样的话,问题又来了。这样做,客户端必须对这些常量去建立理解,才能了解如何去使用这个东西。说白了,在调用的时候,如果用户不到Const类中去看看,还真不知道这个参数应该怎么传、怎么调。像是setType(2)这种用法也是在所难免,因为它完全合法,不是每个人都能够建立起用常量名代替数值,从而增加程序可读性、降低耦合性的意识。
(2)类型不安全。在用户去调用的时候,必须保证类型完全一致,同时取值范围也要正确。像是setType(-1)这样的调用是合法的,但它并不合理,今后会为程序带来种种问题。也许你会说,加一个有效性验证嘛,但是,这样做的话,又会引出下面的第(3)个问题。
(3)耦合性高,扩展性差。假如,因为某些原因,需要修改Const类中常量的值,那么,所有用到这些常量的代码也就都需要修改——当然,要仔细地修改,万一漏了一个,那可不是开玩笑的。同时,这样做也不利于扩展。例如,假如针对类别做了一个有效性验证,如果类别增加了或者有所变动,则有效性验证也需要做对应的修改,不利于后期维护。
如何来解决所遇到的问题呢,那就是采用枚举来进行解决,在解决前,我们先学习一些枚举的知识。
二、用普通类模拟枚举的实现原理
假设我们现在有一个要表示星期的想法,那么传统的方式和上面一样,我们会这样做:
package study.javaenhance; public class WeekDay { private int type; public int getType() { return type; } public void setType(int type) { this.type = type; } }
package study.javaenhance; public class EnumTest { public static void main(String[] args) { WeekDay weekDay = new WeekDay(); weekDay.setType(0); } }
对于开发代码的人员而言,那么星期天,可能有些人用0 也会有些人用7 ,那么这个时候如果不熟悉业务代码就会出现错误现象,那么我们如果没有枚举类的情况,如何优化呢?
我们可以自己来实现枚举,如下:
package study.javaenhance; public class WeekDay { private WeekDay(){};//先把构造方法私有化,以防止外面可以new 出不同的对象来,从而没有办法达到约束的目的。 //定义可以选择的范围 public static final WeekDay SUN = new WeekDay(); public static final WeekDay MON = new WeekDay(); public WeekDay nextDay() { if(this == SUN){ return MON; }else{ return SUN; } } public String toString() { return this==SUN?"SUN":"MON"; } }
然后客户端在使用的时候,因为构造方法私有化了,那么只能选择已经列举好的实例对象,如下:
package study.javaenhance; public class EnumTest { public static void main(String[] args) { WeekDay weekDay = WeekDay.SUN; System.out.println(weekDay.nextDay()); } }
但是上面的代码存在一些缺陷,就是当类型很多的时候,那么就会存在大量的if else 的现象,如何来优化呢?如下:
package study.javaenhance; public abstract class WeekDay { private WeekDay(){};//先把构造方法私有化,以防止外面可以new 出不同的对象来,从而没有办法达到约束的目的。 //定义可以选择的范围 public static final WeekDay SUN = new WeekDay() { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return MON; } }; public static final WeekDay MON = new WeekDay() { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return SUN; } }; public abstract WeekDay nextDay(); /*public WeekDay nextDay() { if(this == SUN){ return MON; }else{ return SUN; } }*/ public String toString() { return this==SUN?"SUN":"MON"; } }
从上面的例子告诉我们,我们即将学习的枚举其实也一个类,只是一个特殊的类而已,继承了enum而已,具体的后面会讲到。
三、枚举的基本应用
使用枚举首先需要知道如何定义枚举,如下:
package study.javaenhance; public class EnumTest { public static void main(String[] args) { WeekDay1 weekDay = WeekDay1.SUN; System.out.println(weekDay.nextDay()); WeekDay weekDay2 = WeekDay.MON; System.out.println(weekDay2); } public enum WeekDay { SUN,MON } }
关键字enum,枚举的基本使用如下:
package study.javaenhance; public class EnumTest { public static void main(String[] args) { WeekDay1 weekDay = WeekDay1.SUN; System.out.println(weekDay.nextDay()); WeekDay weekDay2 = WeekDay.MON; System.out.println(weekDay2); sayDay(WeekDay.SUN); } public enum WeekDay { SUN,MON; } public static void sayDay(WeekDay weekDay) { switch(weekDay) { case SUN: System.out.println("星期天"); break; case MON: System.out.println("星期一"); } } }
四、实现带有构造方法的枚举
结合我们自定义的枚举模拟例子和enum枚举,我们发现我们自定义类是可以加上构造方法来进行参数的传递,以便初始化值,那么枚举enum是否可以呢?
答案是肯定的,使用方法和我们自定义的枚举类似,如下:
package study.javaenhance; public class EnumTest { public static void main(String[] args) { WeekDay1 weekDay = WeekDay1.SUN; System.out.println(weekDay.nextDay()); WeekDay weekDay2 = WeekDay.MON; System.out.println(weekDay2); sayDay(WeekDay.SUN); } public enum WeekDay { SUN(1),MON(),TUE,WED,THI,FRI,SAT; //注意点,构造方法一定要私有化,其它和普通类的方式一样. private WeekDay() { System.out.println("first"); } private WeekDay(int day) { System.out.println("second"); } } public static void sayDay(WeekDay weekDay) { switch(weekDay) { case SUN: System.out.println("星期天"); break; case MON: System.out.println("星期一"); } } }
枚举类型的静态方法:
values()返回枚举所有成员的数组
valueOf()将字符串转换成枚举值;当然需要在范围内的。
package study.javaenhance; public class EnumTest { public static void main(String[] args) { WeekDay1 weekDay = WeekDay1.SUN; System.out.println(weekDay.nextDay()); WeekDay weekDay2 = WeekDay.MON; System.out.println(weekDay2); sayDay(WeekDay.SUN); //枚举类型提供了两个有用的静态方法values()和valueOf() System.out.println(WeekDay.values().length); System.out.println(WeekDay.valueOf("SUN").toString()); System.out.println(WeekDay.valueOf("aa").toString());//会抛异常,不在枚举的范围列表中 } public enum WeekDay { SUN(1),MON(),TUE,WED,THI,FRI,SAT; //注意点,构造方法一定要私有化,其它和普通类的方式一样. private WeekDay() { System.out.println("first"); } private WeekDay(int day) { System.out.println("second"); } } public static void sayDay(WeekDay weekDay) { switch(weekDay) { case SUN: System.out.println("星期天"); break; case MON: System.out.println("星期一"); } } }
五、实现带有抽象方法的枚举
上面我们学习到枚举enum的基本使用,但是我们看到我们自己定义的枚举中可以定义抽象方法,然后去实现对应的功能,那么enum也是可以的,如下所示:
public enum WeekDay { SUN(1) { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return MON; } }, MON() { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return TUE; } }, TUE { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return WED; } }, WED { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return THI; } }, THI { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return FRI; } }, FRI { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return SAT; } }, SAT { @Override public WeekDay nextDay() { // TODO Auto-generated method stub return SUN; } }; //注意点,构造方法一定要私有化,其它和普通类的方式一样. private WeekDay() { System.out.println("first"); } private WeekDay(int day) { System.out.println("second"); } public abstract WeekDay nextDay(); }
客户端调用:
public static void main(String[] args) { WeekDay1 weekDay = WeekDay1.SUN; System.out.println(weekDay.nextDay()); WeekDay weekDay2 = WeekDay.MON; System.out.println(weekDay2); sayDay(WeekDay.SUN); //枚举类型提供了两个有用的静态方法values()和valueOf() System.out.println(WeekDay.values().length); System.out.println(WeekDay.valueOf("SUN").toString()); //System.out.println(WeekDay.valueOf("aa").toString());//会抛异常,不在枚举的范围列表中 //下一天 System.out.println(weekDay2.nextDay()); }
另外,我们可以看大在weekDay2.nextDay() 方法的时候会看到enum 提供一些自带的方法。如下:
//当前对象的名字 System.out.println(weekDay2.name()); //当前对象序号,从0开始 System.out.println(weekDay2.ordinal());
六、枚举的本质
讲枚举的本质前,再看一个例子:
public enum Coin { penny("hello"), nickle("world"), dime("welcome"), quarter("hello world"); private String value; public String getValue() { return value; } Coin(String value) { this.value = value; } public static void main(String[] args) { Coin coin = Coin.quarter; System.out.println(coin.getValue()); } }
定义枚举类型时本质上就是在定义一个类别,只不过很多细节由编译器帮您完成,所以某种程度上,enum关键字的作用就像是class或interface。
当您使用“enum”定义枚举类型时,实质上您定义出来的类型比如class Coin继承自java.lang.Enum类型,而每个枚举的成员其实就是您定义的枚举类型的一个实例(Instance),它们都被预设为final,所以您无法改变它们,它们也是static成员,所以您可以通过类型名称直接使用它们,当然最重要的,它们都是公开的(public)。即枚举中的每个成员默认都是public static final的。
枚举的本质:每个枚举的成员其实就是您定义的枚举类型的一个实例(Instance)。
当定义了一个枚举类型后,在编译的时候就能够确定该枚举类型有多少个实例,这些对象的名字是什么。因为私有所以在运行期间无法再使用该枚举类型创建新的实例。
其他知识扩展:(使用频率较低)
EnumSet的名称说明了其作用,它是在J2SE 5.0后加入的新类别,可以协助您建立枚举值的集合,它提供了一系列的静态方法,可以让您指定不同的集合建立方式。
EnumSet有多重重载的of()方法,用于构造含有指定枚举对象的枚举集合。
与之对应的有complementOf()方法,返回某个枚举集合的补集。
noneOf()方法构造一个指定枚举类型的空枚举集合。之后可以用add()方法加入元素。
copyOf()方法有两种重载形式,一种形式的参数为枚举集合EnumSet,另一种方式的参数为Collection。说明可以利用集合来构造枚举集合,注意如果Collection中有重复元素,只有一个会被加入枚举集合。
下面的代码例子中使用了这些方法:
import java.util.List; import java.util.ArrayList; import java.util.EnumSet; import java.util.Iterator; enum FontConstant { Plain, Bold, Italic, Hello, } public class EnumSetDemo { public static void main(String[] args) { System.out.println("--------------of()-----------------"); // of()方法,构造含有指定元素的枚举集合 EnumSet<FontConstant> enumSet = EnumSet.of(FontConstant.Plain, FontConstant.Bold); showEnumSet(enumSet); System.out.println("------------complementOf()---------------"); // complementOf()方法,构造指定枚举集合的补集 showEnumSet(EnumSet.complementOf(enumSet)); System.out.println("--------------noneOf()-----------------"); // noneOf()方法构造一个指定枚举类型的空枚举集合 EnumSet<FontConstant> enumSet2 = EnumSet.noneOf(FontConstant.class); enumSet2.add(FontConstant.Italic); showEnumSet(enumSet2); System.out.println("---------------copyOf()------------------"); // copyOf()方法的一种重载可以由集合构造枚举集合 // 先构造一个List List<FontConstant> list = new ArrayList<FontConstant>(); list.add(FontConstant.Bold); list.add(FontConstant.Italic); list.add(FontConstant.Plain); list.add(FontConstant.Bold); // 然后使用copyO方法构造一个EnumSet showEnumSet(EnumSet.copyOf(list)); } public static void showEnumSet(EnumSet<FontConstant> enumSet) { for (Iterator<FontConstant> iter = enumSet.iterator(); iter.hasNext();) { System.out.println(iter.next()); } } }
EnumMap的声明是:Class EnumMap<K extends Enum<K>,V>,表明其中的Key是枚举类型。
使用例子如下:
import java.util.EnumMap; import java.util.Map; public class EnumMapDemo { public static void main(String[] args) { Map<Action, String> map = new EnumMap<Action, String>(Action.class); map.put(Action.TURN_LEFT, "向左转"); map.put(Action.SHOOT, "射击"); map.put(Action.TURN_RIGHT, "向右转"); for (Action action : Action.values()) { System.out.println(map.get(action)); } } } enum Action { TURN_LEFT, TURN_RIGHT, SHOOT, }
七、解决前面提到的问题:
对于离散而固定选举的值,建议定义称为枚举类型而不要定义为常量,如下:
public enum TypeEnum { VIDEO, AUDIO, TEXT, IMAGE }
将实体类中的type类型修改如下:
public class Entity { private int id; private TypeEnum type; public int getId() { return id; } public void setId(int id) { this.id = id; } public TypeEnum getType() { return type; } public void setType(TypeEnum type) { this.type = type; } }
这样客户端在赋予值得时候,就做到了限制,不能随便填入。
Entity e = new Entity(); e.setId(10); e.setType(TypeEnum.AUDIO);
在调用setType()时,可选值只有四个,否则会出现编译错误,因此可以看出,枚举是类型安全的,不会出现取值范围错误的问题。同时,客户端不需要建立对枚举中常量值的了解,使用起来很方便,并且可以容易地对枚举进行修改,而无需修改客户端。如果常量从枚举中被删除了,那么客户端将会失败并且将会收到一个错误消息。枚举中的常量名称可以被打印,因此除了仅仅得到列表中项的序号外还可以获取更多信息。这也意味着常量可用作集合的名称,例如HashMap。
综上,我们可以看到,在JDK5中新引入的枚举完美地解决了之前通过常量来表示离散量所带来的问题,大大加强了程序的可读性、易用性和可维护性,并且在此基础之上又进行了扩展,使之可以像类一样去使用,更是为Java对离散量的表示上升了一个台阶。因此,如果在Java中需要表示诸如颜色、方式、类别、状态等等数目有限、形式离散、表达又极为明确的量,应当尽量舍弃常量表示的做法,而将枚举作为首要的选择。