目录
枚举之前--int常量
枚举类型
枚举类型的特性
自定义方法和域的设置
实例的统一行为
实例的不同行为
switch语句
抽象方法
公共接口
策略枚举
用实例域代替默认序数
枚举集合
EnumSet
EnumMap
枚举类型用于定义一组固定的命名常量,在枚举类型中定义的每一个枚举常量都是枚举类型的一个实例,而且这个实例是不可变的。
枚举类型是Java 5版本引入的,在引入之前通常使用int常量来表示枚举类型,每一个int常量代表一个类型实例:
public class FruitMarket {
public static final int APPLE = 0;
public static final int ORANGE = 1;
public static final int BANANA = 2;
public static final int PINEAPPLE = 3;
}
int常量的确能够轻松的列举出有限的枚举,但是有几个问题:
一是int常量没有安全性可言,只要是int值相同,不同类型的枚举都视为一样:
class FruitMarket {
public static final int APPLE = 0;
public static final int ORANGE = 1;
public static final int BANANA = 2;
public static final int PINEAPPLE = 3;
}
class VegetableMarket{
public static final int CARROT = 0;
public static final int CABBAGE = 1;
public static final int TOMATO = 2;
public static final int CUCUMBER = 3;
}
public class Application{
public static void main(String[] args){
VegetableMarket.CARROT == FruitMarket.APPLE;
}
}
二是Int常量作为静态常量在编译时就会被赋值,一旦常量做了改变(或者有了新增),就要重新编译文件,而且要重新检视应用了这些常量的Java文件。
三是Int常量没有丰富的专用方法,比如遍历所有的枚举、将具体的常量值映射到对应的String标识等等,都要通过循环遍历和switch语句来实现。
四是Int常量受限于Integer类型,无法顺畅的加入一些枚举特有的方法。
但是Int常量的优势在于其存储的轻量化,所以如果是需要轻量化的枚举(比如只需要作为简单的标识作用),Int常量还是可以考虑的。
Java的枚举类型对于枚举的使用做了针对性的设计,同时配置了齐全的功能。
一是通过公有的静态final域为每一个枚举常量导出一个枚举实例。
二是没有可以外部访问的构造器,进一步保证了外部无法通过构造器来新增枚举实例,但是可以允许有私有构造器,将枚举实例与相关的数据对应起来:
public enum Planet{
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.972e+24, 6.378e6),
MARS(6.419e+23, 3.396e6),
JUPITER(1.899e+27, 7.149e7),
SATURN(5.685e+26, 6.052e7),
URANUS(8.681e+25, 2.556e7),
NEPTUNE(1.002e+25, 1.188e6);
private double mass;
private double radius;
private double surfaceGravity;
private final static double G = 6.67300E-11;
Planet(double mass, double radius){
this.mass = mass;
this.radius = radius;
surfaceGravity = G*mass/(radius*radius);
}
}
//这里将Planet实例与其质量(mass)、半径(radius)和重力常量对应起来。
三是枚举类提供了丰富的内置方法,其中绝大部分都是继承自java.lang.Enum基类(除了values()方法)
:
name()
:
Day.MONDAY
,name()
方法将返回 "MONDAY"
。ordinal()
:
Day.MONDAY
,ordinal()
方法将返回 0
,因为它是第一个枚举常量。toString()
:
name()
方法相同。toString()
方法返回枚举常量的名称。compareTo(E other)
:
other
是同一枚举类型的另一个枚举常量。other
枚举常量的相对顺序。valueOf(Class
:
IllegalArgumentException
。values()
:
可以看到绝大部分方法都是继承了Object类和实现Comparable与Serializable接口所实现的方法。
枚举类型由于不是基本数据类型,所以在构建的时候可以添加自定义方法和域,这些方法和域可以实现将各种支持性数据和它对应的常量对应起来(比如将Apple实例与其图片、介绍等内容关联起来),自定义域的定义的在前面说私有构造器的时候已经举过相关例子了,这里把自定义的方法扩展一下:
public enum Planet{
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.972e+24, 6.378e6),
MARS(6.419e+23, 3.396e6),
JUPITER(1.899e+27, 7.149e7),
SATURN(5.685e+26, 6.052e7),
URANUS(8.681e+25, 2.556e7),
NEPTUNE(1.002e+25, 1.188e6);
private final double mass;
private final double radius;
private final double surfaceGravity;
private final static double G = 6.67300E-11;
Planet(double mass, double radius){
this.mass = mass;
this.radius = radius;
surfaceGravity = G*mass/(radius*radius);
}
public double getMass(){
return mass;
}
public double getRadius(){
return radius;
}
public double surfaceWeight(double mass){
return mass*surfaceGravity;
}
}
这里作者强调由于枚举类本身是不可变的,因此所有的域都要改成final,而且要做好封装,能私有化的尽量都私有化,通过公有接口来调用。
比如四则运算,每一个枚举实例的运算规则都不相同,这种实例的不同行为可以有多种实现方式:
public enum Operation{
PLUS, MINUS, TIMES, DIVIDE;
public double apply(double x, double y){
switch(this){
case PLUS: return x+y;
case MINUS: return x-y;
case TIMES: return x*y;
case DIVIDE: return x/y;
}
throw new AssertionError("Unknow op:"+this);
}
}
//由switch语句来实现apply方法
这种方法比较简洁,但是有几个问题:
一是必须声明throw方法,不然无法通过编译,但是枚举类保证了实例的范围,所以这段代码是永远不会被执行的。
二是一旦新增了枚举常量,就需要记得给switch方法中再增加一个case。
package Operation;
public enum BasicOperation{
PLUS("+"){
public double apply(double x, double y){
return x + y;
}
},
MINUS("-"){
public double apply(double x, double y){
return x-y;
}
},
MULTIPLY("*"){
public double apply(double x, double y){
return x * y;
}
},
DIVIDE("/"){
public double apply(double x, double y){
return x / y;
}
};
public abstract double apply(double x, double y);//在枚举类中设置抽象方法,然后让各个实例来自行实现。
private final String symbol;
BasicOperation(String symbol){
this.symbol = symbol;
}
@Override
public String toString(){
return symbol;
}
}
这个方法将公共方法抽象到了类中,实现了高内聚,但是还可以进一步的抽象为公共接口。
package Operation;
//将apply方法抽象到接口当中
public interface Operation {
public double apply(double x, double y);
}
//具体的枚举类通过实现接口来调用接口方法apply
public enum BasicOperation implements Operation{
PLUS("+"){
public double apply(double x, double y){
return x + y;
}
},
MINUS("-"){
public double apply(double x, double y){
return x-y;
}
},
MULTIPLY("*"){
public double apply(double x, double y){
return x * y;
}
},
DIVIDE("/"){
public double apply(double x, double y){
return x / y;
}
};
private final String symbol;
BasicOperation(String symbol){
this.symbol = symbol;
}
@Override
public String toString(){
return symbol;
}
}
公共接口的方式进一步提升了抽象的层次,使得任何枚举类都可以通过实现接口来调用相关方法,这种方式适合新建另一个枚举类的时候使用(比如新建一个AdvancedOperation类实现了幂运算和开方运算)。
策略枚举是一种较为特殊的方式,它适用的场景是对于枚举实例设置一个新的分类方式,并基于这个分类方式实行差异化的方法,比如把周一到周日分为工作日和休息日,其中工作日超过八小时的工作时间会产生加班工资,在休息日所有工作都产生加班工资:
package PayrollDay;
public class PayrollDay {
MONDAY(PayType.WEEKDAY),TUESDAY(PayType.WEEKDAY),WEDNESDAY(PayType.WEEKDAY),THURSDAY(PayType.WEEKDAY),FRIDAY(PayType.WEEKDAY),SATURDAY(PayType.WEEKEND),SUNDAY(PayType.WEEKEND);
private final PayType, payType;
GoodPayrollDay(PayType payType){
this.payType = payType;
}
int pay(int minutesWorked, int payRate){
return payType.pay(minutesWorked, payRate);
}
}
//这里将PayTYpe设置为新的分类方式,并且作为基本枚举类PayrollDay的一个域映射到每一个实例上,而pay的实现方法都定义在PayTYpe中
enum PayType{
WEEKDAY{
int overtimePay(int minutes, int PayRate){
return minutes <= MINS_PER_SHIFT ? 0 : (minutes - MINS_PER_SHIFT) * PayRate/2;
};
},
WEEKEND{int overtimePay(int minutes, int PayRate){
return minutes * PayRate/2;
}
};
abstract int overtimePay(int minutes, int PayRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutes, int PayRate){
int basePay = minutes*PayRate;
return basePay + overtimePay(minutes, PayRate);
}
}
枚举类有默认实现的ordinal()方法来返回每个枚举常量在类型中对应的数字位置,但是这个数字并不是程序员可以直接控制的,而且没有实际的意义,因此作者建议自行定义关联值并将它保存在一个实例域中:
package Ensemble;
public enum ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10);
private int numberOfMusicians;
ensemble(int size){
this.numberOfMusicians = size;
}
public int numberOfMusicians(){
return numberOfMusicians;
}
}
EnumSet是同一个枚举类常量的集合,在枚举类出现之前的int常量时期,往往使用位域操作来实现int常量的集合运算:
public class text {
public static final int STYLE_BOLD = 1<<0;
public static final int STYLE_ITALIC = 1<<1;
public static final int STYLE_UNDERLINE = 1<<2;
public static final int STYLE_STRIKETHROUGH = 1<<3;
public static int applyStyle(int style){
return style;
};
}
这里由int常量来表示枚举,由于int有32位,这意味着可以使用int类型来表示最多32中不同的状态,并且通过or运算来做并集(比如将0001和1000合并成为1001)。
但是这种方式有很大的问题,一是它继承了int常量所有的缺陷,二是难以遍历位域运算形成的集合,三是在编写API的时候,必须先预测最多需要多少位,同时要选择合适的类型(如果特性较多可能要使用long类型),不然会导致溢出。
所以EnumSet可以完美解决上述的缺陷,它实现了Set接口的集合处理方法,使得对于集合的各种处理功能更加丰富。
public class Text{
public enum Style{BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}
publi void applyStyles(Set