Java学习--枚举
枚举类型enum,地位等同于class,interface
使用enum定义的枚举类型,也是一种变量类型,可用于声明变量
枚举的一些特征
1.它不能有public的构造函数,这样做可以保证客户代码没有办法新建一个enum的实例。
2.所有枚举值都是public static final的。注意这一点只是针对于枚举值,我们可以和在普通类里面定义变量一样定义其它任何类型的非枚举变量,这些变量可以用任何你想用的修饰符。
3.Enum默认实现了java.lang.Comparable接口。
4.Enum覆载了了toString方法,因此我们如果调用Color.Blue.toString()默认返回字符串”Blue”.
5.Enum提供了一个valueOf方法,这个方法和toString方法是相对应的。调用valueOf(“Blue”)将返回Color.Blue.因此我们在自己重写toString方法的时候就要注意到这一点,一把来说应该相对应地重写valueOf方法。
6.Enum还提供了values方法,这个方法使你能够方便的遍历所有的枚举值。
7.Enum还有一个ordnal的方法,这个方法返回枚举值在枚举类种的顺序,这个顺序根据枚举值声明的顺序而定,这里Color.Red.ordinal()返回0。
什么情况下使用枚举
当某一个事物的状态或者类别等仅限于有限的几种时,就可以使用枚举
枚举一般用来表示一组类型相同的常量
例如一年四季
public enum SeasonEnum { SPRING, SUMMER, FALL, WINTER; }
例如星期,一周有七天,这样就可以使用枚举来表示
public enum Weekdays { MON, TUE, WED, THU, FRI, SAT, SUN; }
例如一个英雄的状态 (走, 跑, 攻击, 防御, 死亡)
public enum HeroStatus { WALKING, RUNNING, ATTACKING, DEFENDING, DEAD; }
枚举的实现原理
枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。
每个枚举类型都继承自 java.lang.Enum,并自动添加了 values 和 valueOf 方法。因为类只能单继承,而枚举类默认继承了ENUM类,所以无法继承其他的类,但可以实现接口
而每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了该枚举类。
所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。
另外通过把 clone、readObject、writeObject 这三个方法定义为 final 的,同时实现是抛出相应的异常。这样保证了每个枚举类型及枚举常量都是不可变的。可以利用枚举的这两个特性来实现线程安全的单例。
Enum 类的静态方法
values() 方法
返回该枚举类所有静态常量的数组
Enum 对象的常用方法
getDeclaringClass() 方法
返回和当前枚举常量的枚举类型对应的 class 对象
name() 和 toString()方法是一样的
返回枚举常量的名称,这个名称是字符串类型的.两个方法一样
ordinal() 方法
返回的枚举常量的序号,值得注意的是 Enum 对象的序号是从0开始计数的,
枚举类的常用操作
枚举类的遍历
switch 来匹配 Enum 中的内容
for (SeasonEnum season: SeasonEnum.values()) { System.out.println(season.name()); System.out.println(season.ordinal()); switch (season) { case SPRING: System.out.println("this is spring"); break; case SUMMER: System.out.println("this is spring"); break; case FALL: System.out.println("this is spring"); break; case WINTER: System.out.println("this is spring"); break; } }
枚举类的优点
使用枚举类型的变量作为参数,可以使参数的值仅限于枚举类型的几种常量,保证了安全性
枚举类型可以遍历,便于操作
在Java 1.5之前,没有枚举类型时,我们是怎样表示枚举的。public class PlanetWithoutEnum { public static final int PLANET_MERCURY = 0;
public
class
PlanetWithoutEnum {
public
static
final
int
PLANET_MERCURY =
0
;
public
static
final
int
PLANET_VENUS =
1
;
public
static
final
int
PLANET_EARTH =
2
;
public
static
final
int
PLANET_MARS =
3
;
public
static
final
int
PLANET_JUPITER =
4
;
public
static
final
int
PLANET_SATURN =
5
;
public
static
final
int
PLANET_URANUS =
6
;
public
static
final
int
PLANET_NEPTUNE =
7
;
}
这种叫int枚举模式,当然你也可以使用String枚举模式,无论采用何种方式,这样的做法,在类型安全和使用方便性上都很差。
如果变量planet表示一个行星,使用者可以给这个值赋与一个不在我们枚举值里面的值,比如 planet = 9,这是哪个行星估计也只有天知道了;
如果方法的参数需要传入一个代表行星的整数,实际中代表八大行星的数只有0-7,但传入其他整数也不会报错,因为形参的类型为int,使用枚举就可以避免这种问题,保证了安全性
再者,我们很难计算出到底有多少个行星,我们也很难对行星进行遍历操作等等。
现在我们用枚举来创建我们的行星。
public enum Planet { MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE; }
使用枚举,我们实现了一个功能,就是任何一个Planet类型的变量,都可以由编译器来保证,传到给参数的任何非null对象一定属于这八个行星之一。
枚举的使用实例
Java允许我们给枚举类型添加任意的属性和方法,这里引言书中的代码,大家自行体会一下枚举的构造器、公共方法、枚举遍历等知识点。
public enum Planet { 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); private final double mass; // In kilograms private final double radius; // In meters private final double surfaceGravity; // In m / s^2 // Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11; // Constructor Planet(double mass, double radius) { this.mass = mass; this.radius = radius; surfaceGravity = G * mass / (radius * radius); } public double mass() { return mass; } public double radius() { return radius; } public double surfaceGravity() { return surfaceGravity; } public double surfaceWeight(double mass) { return mass * surfaceGravity; // F = ma } }
在公共方法里,使用switch去判断枚举类型,然后执行不同的操作
public enum OperationUseSwitch { PLUS, MINUS, TIMES, pIDE; double apply(double x, double y) { switch (this) { case PLUS: return x + y; case MINUS: return x + y; case TIMES: return x + y; case pIDE: return x + y; } // 如果this不属于上面四种操作符,抛出异常 throw new AssertionError("Unknown operation: " + this); } }
这段代码确实实现了我们的需求,但是有两个弊端。
首先是我们不得不在最后抛出异常或者在switch里加上default,不然无法编译通过,但是很明显,程序的分支是不会进入异常或者default的。
其次,这段代码非常脆弱,如果我们添加了新的操作类型,却忘了在switch里添加相应的处理逻辑,执行新的运算操作时,就会出现问题。
还好,Java枚举提供了一种功能,叫做 特定于常量的方法实现。
我们只需要在枚举类型中声明一个抽象方法,然后在各个枚举常量中去覆盖这个方法,实现如下:
public enum Operation { PLUS { double apply(double x, double y) { return x + y; } }, MINUS { double apply(double x, double y) { return x - y; } }, TIMES { double apply(double x, double y) { return x * y; } }, pIDE { double apply(double x, double y) { return x / y; } }; abstract double apply(double x, double y); }
这样,也就再也不会出现添加新操作符后忘记添加对应的处理逻辑的情况了,因为编译器就会提示我们必须覆盖apply方法。
不过,这种 特定于常量的方法实现 有一个缺点,那就是你很难在枚举常量之间共享代码。
我们以星期X的枚举为例,周一到周五是工作日,执行一种逻辑,周六周日,休息日,执行另一种逻辑。
如果还是使用 特定于常量的方法实现,写出来的代码可能就是这样的:
public enum DayUseAbstractMethod { MONDAY { @Override void apply() { dealWithWeekDays();//伪代码 } }, TUESDAY { @Override void apply() { dealWithWeekDays();//伪代码 } }, WEDNESDAY { @Override void apply() { dealWithWeekDays();//伪代码 } }, THURSDAY { @Override void apply() { dealWithWeekDays();//伪代码 } }, FRIDAY { @Override void apply() { dealWithWeekDays();//伪代码 } }, SATURDAY { @Override void apply() { dealWithWeekEnds();//伪代码 } }, SUNDAY { @Override void apply() { dealWithWeekEnds();//伪代码 } }; abstract void apply(); }
很明显,我们这段代码里面有相当多的重复代码。
那么要怎么优化呢,我们不妨这样想,星期一星期二等等是一种枚举,那么工作日和休息日,难道不也是一种枚举吗,我们能不能给Day的构造函数传入一个工作日休息日的DayType枚举呢?这也就是书中给出的一种叫策略枚举 的方法,代码如下:
public enum Day { MONDAY(DayType.WEEKDAY), TUESDAY(DayType.WEEKDAY), WEDNESDAY(DayType.WEEKDAY), THURSDAY(DayType.WEEKDAY), FRIDAY(DayType.WEEKDAY), SATURDAY(DayType.WEEKEND), SUNDAY(DayType.WEEKEND); private final DayType dayType; Day(DayType daytype) { this.dayType = daytype; } void apply() { dayType.apply(); } private enum DayType { WEEKDAY { @Override void apply() { System.out.println("hi, weekday"); } }, WEEKEND { @Override void apply() { System.out.println("hi, weekend"); } }; abstract void apply(); } }
通过策略枚举的方式,我们把Day的处理逻辑委托给了DayType,个中奥妙,读者可以细细体会。
枚举集合 EnumSet的使用
EnumSet提供了非常方便的方法来创建枚举集合,下面这段代码,感受一下
public class Text { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } // Any Set could be passed in, but EnumSet is clearly best public void applyStyles(Set<Style> styles) { // Body goes here for(Style style : styles){ System.out.println(style); } } // Sample use public static void main(String[] args) { Text text = new Text(); text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC)); } }
这个例子里,我们使用了EnumSet.of方法,轻松创建了枚举集合。
枚举Map EnumMap的使用
假设对于香草(Herb),有一个枚举属性Type(一年生、多年生、两年生)
Herb:
public class Herb { public enum Type { ANNUAL, PERENNIAL, BIENNIAL } private final String name; private final Type type; Herb(String name, Type type) { this.name = name; this.type = type; } @Override public String toString() { return name; } }
现在,假设我们有一个Herb数组,我们需要对这个Herb数组按照Type进行分类存放。
所以接下来,我们需要创建一个Map,value肯定是Herb的集合了,那么用什么作为key呢?
有的人会使用枚举类型的ordinal()方法,这个函数返回int类型,表示枚举遍历在枚举类里的位置,这样做,缺点很明显,由于你的key的类型是int,不能保证传入的int一定能和枚举类里的变量对应上。
所以,在key的选择上,毫无疑问,只能使用枚举类型,也即Herb.Type。
最后还有一个问题,要使用什么Map? Java为枚举类型专门提供了一种Map,叫EnumMap,相比较与其他Map,这种Map在处理枚举类型上更快,有兴趣的同学可以研究一下这个map的内部实现。
下面让我们看看怎么使用EnumMap:
public static void main(String[] args) { Herb[] garden = { new Herb("Basil", Type.ANNUAL), new Herb("Carroway", Type.BIENNIAL), new Herb("Dill", Type.ANNUAL), new Herb("Lavendar", Type.PERENNIAL), new Herb("Parsley", Type.BIENNIAL), new Herb("Rosemary", Type.PERENNIAL) }; // Using an EnumMap to associate data with an enum - Page 162 Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>( Herb.Type.class); for (Herb.Type t : Herb.Type.values()) herbsByType.put(t, new HashSet<Herb>()); for (Herb h : garden) herbsByType.get(h.type).add(h); System.out.println(herbsByType); }
总结
和int枚举相比,Enum枚举的在类型安全和使用便利上的优势是不言而喻的。
Enum为枚举提供了丰富的功能,如文章中提到的特定于常量的方法实现和策略枚举。
EnumSet和EnumMap是两个为枚举而设计的集合,在实际开发中,用到枚举集合时,请优先考虑这两个。