Java枚举的使用方法详解
Java枚举的使用方法详解
前言 你代码中的flag和status,都应该用枚举来替代
很多人都说,枚举在实际开发中很少用到,甚至就没用到。因为,他们的代码往往是这样子的:
publicclassConstant{ /* *以下几个变量表示英雄的状态 */ publicfinalstaticintSTATUS_WALKING=0;//走 publicfinalstaticintSTATUS_RUNNINGING=1;//跑 publicfinalstaticintSTATUS_ATTACKING=2;//攻击 publicfinalstaticintSTATUS_DEFENDING=3;//防御 publicfinalstaticintSTATUS_DEAD=4;//挂了 /* *以下几个变量表示英雄的等级 */ //此处略去N行代码 }
然后,他们是这样使用这个类的:
hero.setStatus(Contant.STATUS_ATTACKING);
嗯,然后他们就说,“我在实际开发中很少用到枚举”
当然,他们的意思是说很少用到枚举Enum这个类。
但是,我想说的是,上面这些代码,通通应该用Enum去实现。
为什么?
因为他们的代码完全建立在对队友的信任,假设来了个奇葩队友,做了这件事:
hero.setStatus(666);
你说,屏幕上的英雄会怎么样呢?
总之,假如你在实际编程中经常使用这样的代码,那是时候好好学习一下Enum了。
枚举初探 为什么要使用枚举类型
生活中处处都有枚举,包括“天然的枚举”,比如行星、一周的天数,也包括我们设计出来的枚举,比如csdn的tab标签,菜单等。
Java代码中表示枚举的方式,大体上有两种,一是int枚举,而是Enum枚举,当然,我们都知道,Enum枚举才是Java提供的真正枚举。
那么,为什么我们要使用Enum枚举类型呢?先来看看在Java1.5之前,没有枚举类型时,我们是怎样表示枚举的。
以八大行星为例,每个行星对应一个int值,我们大概会这样写
publicclassPlanetWithoutEnum{ publicstaticfinalintPLANET_MERCURY=0; publicstaticfinalintPLANET_VENUS=1; publicstaticfinalintPLANET_EARTH=2; publicstaticfinalintPLANET_MARS=3; publicstaticfinalintPLANET_JUPITER=4; publicstaticfinalintPLANET_SATURN=5; publicstaticfinalintPLANET_URANUS=6; publicstaticfinalintPLANET_NEPTUNE=7; }
这种叫int枚举模式,当然你也可以使用String枚举模式,无论采用何种方式,这样的做法,在类型安全和使用方便性上都很差。
如果变量planet表示一个行星,使用者可以给这个值赋与一个不在我们枚举值里面的值,比如planet=9,这是哪个行星估计也只有天知道了;再者,我们很难计算出到底有多少个行星,我们也很难对行星进行遍历操作等等。
现在我们用枚举来创建我们的行星。
publicenumPlanet{ MERCURY,VENUS,EARTH,MARS,JUPITER,SATURN,URANUS,NEPTUNE; }
上面这个是最简单的枚举,我们姑且叫做Planet1.0,这个版本的行星枚举,我们实现了一个功能,就是任何一个Planet类型的变量,都可以由编译器来保证,传到给参数的任何非null对象一定属于这八个行星之一。
然后,我们对Planet进行升级,Java允许我们给枚举类型添加任意的方法,这里引言书中的代码,大家自行体会一下枚举的构造器、公共方法、枚举遍历等知识点。
publicenumPlanet{ MERCURY(3.302e+23,2.439e6), VENUS(4.869e+24,6.052e6), EARTH(5.975e+24,6.378e6), MARS(6.419e+23,3.393e6), JUPITER(1.899e+27,7.149e7), SATURN(5.685e+26,6.027e7), URANUS(8.683e+25,2.556e7), NEPTUNE(1.024e+26,2.477e7); privatefinaldoublemass;//Inkilograms privatefinaldoubleradius;//Inmeters privatefinaldoublesurfaceGravity;//Inm/s^2 //Universalgravitationalconstantinm^3/kgs^2 privatestaticfinaldoubleG=6.67300E-11; //Constructor Planet(doublemass,doubleradius){ this.mass=mass; this.radius=radius; surfaceGravity=G*mass/(radius*radius); } publicdoublemass(){ returnmass; } publicdoubleradius(){ returnradius; } publicdoublesurfaceGravity(){ returnsurfaceGravity; } publicdoublesurfaceWeight(doublemass){ returnmass*surfaceGravity;//F=ma } }
//注:这里对书中的代码做了微调 publicclassWeightTable{ publicstaticvoidmain(String[]args){ printfWeightOnAllPlanets(8d); } publicstaticvoidprintfWeightOnAllPlanets(doubleearthWeight){ doublemass=earthWeight/Planet.EARTH.surfaceGravity(); for(Planetp:Planet.values()) System.out.printf("Weighton%sis%f%n",p,p.surfaceWeight(mass)); } }
运行WeightTable,打印结果如下:
WeightonMERCURYis3.023254 WeightonVENUSis7.240408 WeightonEARTHis8.000000 WeightonMARSis3.036832 WeightonJUPITERis20.237436 WeightonSATURNis8.524113 WeightonURANUSis7.238844 WeightonNEPTUNEis9.090108
在这个小程序里,我们用到了枚举的values()方法,这个方法返回了枚举类型里的枚举变量的集合,非常实用。
枚举进阶 计算器运算符枚举类
上一小节的例子里,我们用到了枚举类的公共方法,这一节,我们以计算器运算符Operation枚举类为例,看看怎么实现对于
每一个枚举对象,执行不同的操作。
首先,我们很容易想到的一个方法,在公共方法里,使用switch去判断枚举类型,然后执行不同的操作,代码如下:
publicenumOperationUseSwitch{ PLUS,MINUS,TIMES,DIVIDE; doubleapply(doublex,doubley){ switch(this){ casePLUS: returnx+y; caseMINUS: returnx+y; caseTIMES: returnx+y; caseDIVIDE: returnx+y; } //如果this不属于上面四种操作符,抛出异常 thrownewAssertionError("Unknownoperation:"+this); } }
这段代码确实实现了我们的需求,但是有两个弊端。
首先是我们不得不在最后抛出异常或者在switch里加上default,不然无法编译通过,但是很明显,程序的分支是不会进入异常或者default的。
其次,这段代码非常脆弱,如果我们添加了新的操作类型,却忘了在switch里添加相应的处理逻辑,执行新的运算操作时,就会出现问题。
还好,Java枚举提供了一种功能,叫做特定于常量的方法实现。
我们只需要在枚举类型中声明一个抽象方法,然后在各个枚举常量中去覆盖这个方法,实现如下:
publicenumOperation{ PLUS{ doubleapply(doublex,doubley){ returnx+y; } }, MINUS{ doubleapply(doublex,doubley){ returnx-y; } }, TIMES{ doubleapply(doublex,doubley){ returnx*y; } }, DIVIDE{ doubleapply(doublex,doubley){ returnx/y; } }; abstractdoubleapply(doublex,doubley); }
这样,也就再也不会出现添加新操作符后忘记添加对应的处理逻辑的情况了,因为编译器就会提示我们必须覆盖apply方法。
不过,这种特定于常量的方法实现有一个缺点,那就是你很难在枚举常量之间共享代码。
我们以星期X的枚举为例,周一到周五是工作日,执行一种逻辑,周六周日,休息日,执行另一种逻辑。
如果还是使用特定于常量的方法实现,写出来的代码可能就是这样的:
publicenumDayUseAbstractMethod{ MONDAY{ @Override voidapply(){ dealWithWeekDays();//伪代码 } }, TUESDAY{ @Override voidapply(){ dealWithWeekDays();//伪代码 } }, WEDNESDAY{ @Override voidapply(){ dealWithWeekDays();//伪代码 } }, THURSDAY{ @Override voidapply(){ dealWithWeekDays();//伪代码 } }, FRIDAY{ @Override voidapply(){ dealWithWeekDays();//伪代码 } }, SATURDAY{ @Override voidapply(){ dealWithWeekEnds();//伪代码 } }, SUNDAY{ @Override voidapply(){ dealWithWeekEnds();//伪代码 } }; abstractvoidapply(); }
很明显,我们这段代码里面有相当多的重复代码。
那么要怎么优化呢,我们不妨这样想,星期一星期二等等是一种枚举,那么工作日和休息日,难道不也是一种枚举吗,我们能不能给Day的构造函数传入一个工作日休息日的DayType枚举呢?这也就是书中给出的一种叫策略枚举的方法,代码如下:
publicenumDay{ MONDAY(DayType.WEEKDAY),TUESDAY(DayType.WEEKDAY),WEDNESDAY( DayType.WEEKDAY),THURSDAY(DayType.WEEKDAY),FRIDAY(DayType.WEEKDAY),SATURDAY( DayType.WEEKDAY),SUNDAY(DayType.WEEKDAY); privatefinalDayTypedayType; Day(DayTypedaytype){ this.dayType=daytype; } voidapply(){ dayType.apply(); } privateenumDayType{ WEEKDAY{ @Override voidapply(){ System.out.println("hi,weekday"); } }, WEEKEND{ @Override voidapply(){ System.out.println("hi,weekend"); } }; abstractvoidapply(); } }
通过策略枚举的方式,我们把Day的处理逻辑委托给了DayType,个中奥妙,读者可以细细体会。
枚举集合 EnumSet的使用
EnumSet提供了非常方便的方法来创建枚举集合,下面这段代码,感受一下
publicclassText{ publicenumStyle{ BOLD,ITALIC,UNDERLINE,STRIKETHROUGH } //AnySetcouldbepassedin,butEnumSetisclearlybest publicvoidapplyStyles(Set