枚举类型
枚举类型在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方法,可以将枚举实例转换成可打印的字符串。**```javaenum Apple{FUJI(0,"富士苹果");private final int code;private final String name;Apple(int code, String name) {this.code = code;this.name = name;}@Overridepublic 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 kilogramsprivate final double radius; // In metersprivate final double surfaceGravity; // In m / s^2// Universal gravitational constant in m^3 / kg s^2private static final double G = 6.67300E-11;// ConstructorPlanet(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 {@Overridedouble apply(double x, double y) {return x + y;}},MINUS {@Overridedouble apply(double x, double y) {return x - y;}},TIMES {...},DIVIDE {...};abstract double apply(double x, double y);}
甚至同时关联数据和行为:
public enum Operation {PLUS("+") {@Overridedouble apply(double x, double y) {return x + y;}},MINUS("-") {...},TIMES("*") {...},DIVIDE("/") {...};private String symbol;Operation(String symbol) {this.symbol = symbol;}@Overridepublic 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;}@Overridepublic 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 {@Overridedouble overtimePay(double hoursWorked, double payRate) {double overtime = hoursWorked - HOURS_PER_SHIFT;return overtime <= 0 ? 0 : overtime * payRate / 2;}},WEEKEND {@Overridedouble 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);}}}
真是高明,大人真的是又高又硬啊。
鸡蛋里挑骨头
枚举类有一个小小的缺点,即装载和初始化枚举时,会有时间上和空间上的成本。除了受资源约束的设备,如手机和面包机之外,在实践中不必太在意这个问题。
