枚举类型
枚举类型在java中就是enum。我认为枚举类(enum)与 枚举要区别,枚举是一个概念,而java枚举类enum,应该是一种实现了枚举的枚举模式。
在编程语言中还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具名的int常量。
例如int枚举模式:
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;
常量枚举模式的不足(五宗罪)
- 它在类型安全性和便利性方面没有任何帮助**。如果你将apple传到想要orange的方法中,编译器也不会出现警告**,还会用==操作符将apple与orange进行对比,甚至更糟糕。apple和orange是完全不一样的东西,但是使用常量表示后可能变成了同样的东西(如都是int常量),编译器无法辨别,这是排查问题的一大障碍。
- 遍历所有枚举常量,并没有很便利的方法。
- int枚举模式的程序十分脆弱。因为int枚举是编译时常量,被编译到使用到它们的客户端代码中去(与JVM优化相关,用常量字面量代替引用)。如果枚举常量关系到的int发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以运行,但是它们的行为就是不确定的。这就是API兼容,而二进制不兼容。
**
- 将int枚举常量翻译成包含有用信息的字符串,并没有很便利的方法。如果将这种常量打印出来,或者在调试器中显示出来,你所看到的就是个数字,并没有多大用处。
- 如果是String枚举模式,虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作。还有的是,字符串可能会有书写错误,但是编译时不会检测出来,运行时才会报错。
解常量枚举模式的忧
public enum Apple {
APPLE_FUJI,
APPLE_PIPPIN,
APPLE_GRANNY_SMITH;
}
public enum Orange {
ORANGE_NAVEL,
ORANGE_TEMPLE,
ORANGE_BLOOD;
}
java枚举类型是功能是否齐全的类,功能比其他语言的对等物要强大的多,Java的枚举本质上是int值,注意并不等于int值。
java枚举类型背后的基本想法非常简单:他们就是通过公有的静态fianl域为每一个枚举常量导出实例的类。
因为没有可以访问的构造器(private),枚举类型是真正单位final。因为客户端既不能创建枚举类型的实例,也不能对他进行扩展,因此很可能没有实例,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的。他们是当单例的泛型化。
良药
- 枚举类提供了编译时的类型安全。不会出现方法参数要求是Apple但是传入Orange却也合法的情况,更不会出现Apple等于Orange的情况。每个枚举类都有属于自己的命名空间,包含同名的常量的多个枚举类型可以在一个系统中和平安全共处。 ```java // AppleUtil public static equal(Apple a1, Apple a2){…}
Apple apple = Apple.APPLE_FUJI; Orange orange = Orange.ORANGE_NAVEL; AppleUtil.equal(apple, orange);// 编译错误
- **避免了编译器对常量的优化带来的困扰。**增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码,因为避免了编译器对常量的优化,常量值并没有被编译到客户端代码中。
<br />
- **可以遍历所有枚举常量。如**`Apple.values()`。
**
- **每个枚举实例都对应一个toString方法,可以将枚举实例转换成可打印的字符串。**
```java
enum Apple{
FUJI(0,"富士苹果");
private final int code;
private final String name;
Apple(int code, String name) {
this.code = code;
this.name = name;
}
@Override
public String toString() {
return "Apple{" + "code=" + code +
", name='" + name + '\'' +
'}';
}
}
enum实战
枚举类的数据域和方法。
将数据与常量关联、添加方法:
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
}
}
public class PlanetDemo {
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values()) {
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
//args[0]=30输出结果
//Weight on MERCURY is 11.337201
//Weight on VENUS is 27.151530
//Weight on EARTH is 30.000000
//Weight on MARS is 11.388120
//Weight on JUPITER is 75.890383
//Weight on SATURN is 31.965423
//Weight on URANUS is 27.145664
//Weight on NEPTUNE is 34.087906
}
}
行为关联枚举常量
有时候会使用枚举类实例来作为逻辑条件来分发至不同的方法,如采用枚举类来写加、减、乘、除的运算。代码如下:
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
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("Unknown op: " + this);
}
}
字段代码可行,但是不太好看。如果没有throw语句,他就不能通过遍历(可能没有返回值),虽然从技术角度来看代码的结束部分是可以执行到的,只有一种情况就是:添加了新的枚举常量,却忘记给switch添加相应条件。这时候,throw语句就成了检测bug的语句了。
为了减少这种失误,enum提供了更加强大的解决方案,就是将枚举常量和行为关联起来:
public enum Operation {
PLUS {
@Override
double apply(double x, double y) {
return x + y;
}
},
MINUS {
@Override
double apply(double x, double y) {
return x - y;
}
},
TIMES {...},
DIVIDE {...};
abstract double apply(double x, double y);
}
甚至同时关联数据和行为:
public enum Operation {
PLUS("+") {
@Override
double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {...},
TIMES("*") {...},
DIVIDE("/") {...};
private String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
abstract double apply(double x, double y);
}
public class OperationDemo {
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values()) {
System.out.println(String.format("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
}
//输入2 4
//2.000000 + 4.000000 = 6.000000
//2.000000 - 4.000000 = -2.000000
//2.000000 * 4.000000 = 8.000000
//2.000000 / 4.000000 = 0.500000
}
}
如果添加新的常量,你就不可能会忘记提供apply方法,因为如果你真的忘记了,编译器也会提醒你,因为枚举类型中的抽象方法必须被他所有常量中的具体方法所覆盖。
另外,贴心的enum有一个自动产生的valueOf(String)方法,他将常量(不是数据)的名字转变成枚举实例。但是,toString()和valueOf是相反的一对操作。valueOf是通过名称获取枚举常量对象。而toString()是通过枚举常量获取枚举常量的名称。如果覆盖了toString,需要考虑编写一个fromString方法。我们通常需要在enum
中新增个静态常量来获取。如:
public enum Operation {
...
private String symbol;
public static final Map<String, Operation> OPERS_MAP = Maps.newHashMap();
static {
for (Operation op : Operation.values()) {
OPERS_MAP.put(op.toString(), op);
}
}
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public Operation fromString(String name){
return OPERS_MAP.get(name);
}
}
这里可能会有这样的疑惑:每个枚举实例都在调用构造函数时将自身添加到OPERS_MAP中可以吗?根据以往的经验好像没问题,毕竟static语句会比构造器先执行啊。实际上,这样会导致编译时错误。因为enum类的构造器运行的时候,这些静态域还没有被实例化。不过,编译时常量域除外。注意 public static final Map<String, Operation>
不是编译时常量域(字面量)。
**
策略枚举(一定要看)
行为关联常量固然在很多时候都好用,但是它也有一个不可忽视的缺点,即如果每个枚举常量都有公共的部分处理该怎么办,如果每个枚举常量关联的方法里都有公共的部分,那不仅不美观,还违反了DRY原则。
如何同时克服行为关联常量和数据关联常量的缺点呢?抽象,抽象行为,再包装一层,这就是策略模式,策略枚举啦。
**
public enum PayRoll {
MONDY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURSDAY(PayType.WEEKDAY),
FRIDAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayRoll(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
private enum PayType {
WEEKDAY {
@Override
double overtimePay(double hoursWorked, double payRate) {
double overtime = hoursWorked - HOURS_PER_SHIFT;
return overtime <= 0 ? 0 : overtime * payRate / 2;
}
},
WEEKEND {
@Override
double overtimePay(double hoursWorked, double payRate) {
return hoursWorked * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hoursWorked, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}
真是高明,大人真的是又高又硬啊。
鸡蛋里挑骨头
枚举类有一个小小的缺点,即装载和初始化枚举时,会有时间上和空间上的成本。除了受资源约束的设备,如手机和面包机之外,在实践中不必太在意这个问题。