Java枚举类型记录
枚举和注解
1. 枚举
枚举类型( enum type )是指由一组固定的常量组成合法值的类型,本质上是int值。
Ⅰ. 用enum
代替int
常量
(1)int
枚举模式
// FruitConsts.java
/**
* @author cph
*
* <p>int 枚举模式</p>
*/
public class FruitConsts {
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
}
缺陷:
- 不具备安全性
// 比如,int枚举模式可以参与运算,还可以进行==比较,这是极其不安全的。
@Test
public void testIntEnum() {
int i = (FruitConsts.APPLE_FUJI - FruitConsts.ORANGE_TEMPLE) /
FruitConsts.APPLE_PIPPIN;
}
-
是编译时常量,它的int值会被编译到使用它们的客户端中,如果关联的值发生变化,则客户端必须重新编译
-
没有描述性可言。很难将int枚举常量转换成可打印的字符串(String 枚举模式是int枚举模式的一种变体,会导致性能问题,因为它依赖字符串的比较)
(2)枚举类型
// Apple.java
/**
* @author cph
*/
public enum Apple {
/**
* 苹果的品牌分类
*/
FUJI,
PIPPIN,
GRANNY_SMITH
}
// Orange.java
/**
* @author cph
*/
public enum Orange {
/**
* 桔子的品牌分类
*/
NAVEL,
TEMPLE,
BLOOD
}
优势:
- 本质上是int值
- 没有可访问的构造器,是真正的final类
- 单例的泛型化,本质上是单元素的枚举
- 保证了编译时的类型安全
// Operation1.java
/**
* @author cph
*
* <p>
* 这段代码很脆弱,如果添加了新的枚举类型,却忘记给switch添加条件,在运用新的运算时,就会运行失败
* </p>
*/
public enum Operation1 {
/**
* 加、减、乘、除、取余
*/
PLUS, MINUS, TIMES, DIVIDE, MOD;
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;
default:
}
// 没有throw语句,代码就无法编译
throw new AssertionError("Unknown op: " + this);
}
}
// 测试
@Test
public void testOperation1() {
double apply = Operation1.PLUS.apply(1, 2);
System.out.println(apply);
/*
使用MOD运算符时引发:java.lang.AssertionError: Unknown op: MOD
double apply1 = Operation1.MOD.apply(3, 2);
System.out.println(apply1);
*/
}
上述代码可以使用特定于常量的方法实现,如下:
// Operation2.java
/**
* @author cph
*
* <p>
* 对第一版运算符枚举进行了改进。
* 在添加新的常量时,就不会忘记提供apply方法,因为编译器会提示你抽象方法必须被覆盖,否则编译不通过。
* 避免了运算操作不存在而导致程序报错。
* </p>
*/
public enum Operation2 {
/**
* 加、减、乘、除、取余
*/
PLUS {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS {
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override
public double apply(double x, double y) {
return x / y;
}
},
MOD {
@Override
public double apply(double x, double y) {
return x % y;
}
};
public abstract double apply(double x, double y);
}
// 测试
@Test
public void testOperation2() {
// 运行成功, apply = 1.0
double apply = Operation2.MOD.apply(3, 2);
System.out.println(apply);
}
经过改进,可以将特定于常量的方法与特定于常量的数据结合起来,如下:
// Operation3.java
/**
* @author cph
*
* <p>
* 特定的常量方法实现可以与特定于常量的数据结合起来
* </p>
*/
public enum Operation3 {
/**
* 加、减、乘、除、取余
*/
PLUS("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
@Override
public double apply(double x, double y) {
return x / y;
}
},
MOD("%") {
@Override
public double apply(double x, double y) {
return x % y;
}
};
private static final Map<String, Operation3> STRING_TO_ENUM = Stream.of(values()).
collect(Collectors.toMap(Object::toString, op -> op));
private final String symbol;
Operation3(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public abstract double apply(double x, double y);
public static Optional<Operation3> fromString(String symbol) {
return Optional.ofNullable(STRING_TO_ENUM.get(symbol));
}
}
// 测试
@Test
public void testOperation3() {
for (Operation3 op : Operation3.values()) {
double x = 3.0, y = 2.0;
double apply = op.apply(x, y);
System.out.printf("%.2f %s %.2f = %.2f%n", x, op, y, apply);
}
}
/*
测试结果:
3.00 + 2.00 = 5.00
3.00 - 2.00 = 1.00
3.00 * 2.00 = 6.00
3.00 / 2.00 = 1.50
3.00 % 2.00 = 1.00
*/
使用Lambda表达优化后,代码更加简洁:
// Operation.java
public enum Operation {
/**
* 加、减、乘、除、取余
*/
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y),
MOD("%", (x, y) -> x % y);
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override
public String toString() {
return symbol;
}
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}
- 包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间
Ⅱ. 策略枚举
特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得 更加困难了。比如下面的例子:
// PayrollDay1.java
/**
* @author cph
*
* <p>
* 计算一周的工资
* 优势:
* - 代码简洁
* 劣势:
* - 难以维护。
* 如果添加了新的枚举元素,没有在switch中添加相应语句,就会导致计算结果出错。
* 为了实现安全的计算,可能会增加样板代码,降低可读性,增加出错概率。
* </p>
*/
public enum PayrollDay1 {
/**
* 周一至周日
*/
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY, SUNDAY;
public static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minuteWorked, int payRate) {
int basePay = minuteWorked * payRate;
int overtimePay;
switch (this) {
case SATURDAY:
case SUNDAY:
overtimePay = basePay / 2;
break;
default:
overtimePay = minuteWorked <= MINS_PER_SHIFT ? 0
: (minuteWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
使用策略枚举优化上述例子,如下:
// PayrollDay2.java
/**
* @author cph
*
* <p>
* <b>策略枚举</b>改进上述计算工资的方法。
*
* 这种模式虽然没有switch语句简洁,但是更加安全,更加灵活
* </p>
*/
public enum PayrollDay2 {
/**
* 周一至周日
*/
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay2(PayType payType) {
this.payType = payType;
}
PayrollDay2() {
this(PayType.WEEKDAY);
}
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
/**
* 策略枚举
*/
private enum PayType {
/**
* 加班类型:工作日,周末
*/
WEEKDAY{
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0
: (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
public static final int MINS_PER_SHIFT = 8 * 60;
abstract int overtimePay(int minsWorked, int payRate);
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
Ⅲ. 使用实例域代替序数
ordinal()方法返回的序数是根据枚举在枚举类型中的顺序决定的,当更换顺序时,就会使ordinal()的返回值改变,可能会造成不必要的Bug,一般情况下,尽量避免使用ordinal方法。
/**
* @author cph
*
* <title>
* 使用实例域代替序数
* 每个枚举都有一个ordinal()方法,返回每个枚举常量在类型中的位置。
* - 一般情况下,尽量避免使用ordinal方法。
* </title>
*/
public enum Ensemble {
// 1 - 10, 12
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int numberOfMusicians) {
this.numberOfMusicians = numberOfMusicians;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
Ⅳ. 用接口模拟可扩展的枚举
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础 枚举类型来对它进行模拟。比如:
// Operation.class
/**
* @author cph
* create datetime 2021/6/20 20:06
*/
public interface Operation {
double apply(double x, double y);
}
// BasicOperation.class
/**
* @author cph
*/
public enum BasicOperation implements Operation{
/**
* 加、减、乘、除、取余
*/
PLUS("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
@Override
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;
}
}
// ExtendedOperation.class
/**
* @author cph
*/
public enum ExtendedOperation implements Operation{
/**
* 求幂、取余
*/
EXP("^") {
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
MOD("%") {
@Override
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
// 测试
@Test
public void test() {
double x = 3.0;
double y = 2.0;
operate(Arrays.asList(ExtendedOperation.values()), x, y);
operate(Collections.singletonList(BasicOperation.PLUS), x, y);
}
private void operate(Collection<? extends Operation> opSet, double x, double y) {
opSet.forEach(op -> System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
}
/*
3.000000 ^ 2.000000 = 9.000000
3.000000 % 2.000000 = 1.000000
3.000000 + 2.000000 = 5.000000
*/
参考:《Effective Java》