枚举类型

枚举类型在java中就是enum。我认为枚举类(enum)与 枚举要区别,枚举是一个概念,而java枚举类enum,应该是一种实现了枚举的枚举模式

在编程语言中还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具名的int常量。
例如int枚举模式:

  1. public static final int APPLE_FUJI = 0;
  2. public static final int APPLE_PIPPIN = 1;
  3. public static final int APPLE_GRANNY_SMITH = 2;
  4. public static final int ORANGE_NAVEL = 0;
  5. public static final int ORANGE_TEMPLE = 1;
  6. public static final int ORANGE_BLOOD = 2;

常量枚举模式的不足(五宗罪)

  • 它在类型安全性和便利性方面没有任何帮助**。如果你将apple传到想要orange的方法中,编译器也不会出现警告**,还会用==操作符将apple与orange进行对比,甚至更糟糕。apple和orange是完全不一样的东西,但是使用常量表示后可能变成了同样的东西(如都是int常量),编译器无法辨别,这是排查问题的一大障碍。


  • 遍历所有枚举常量,并没有很便利的方法。


  • int枚举模式的程序十分脆弱。因为int枚举是编译时常量,被编译到使用到它们的客户端代码中去(与JVM优化相关,用常量字面量代替引用)。如果枚举常量关系到的int发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以运行,但是它们的行为就是不确定的。这就是API兼容,而二进制不兼容。

**

  • 将int枚举常量翻译成包含有用信息的字符串,并没有很便利的方法。如果将这种常量打印出来,或者在调试器中显示出来,你所看到的就是个数字,并没有多大用处。


  • 如果是String枚举模式,虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作。还有的是,字符串可能会有书写错误,但是编译时不会检测出来,运行时才会报错。

解常量枚举模式的忧

  1. public enum Apple {
  2. APPLE_FUJI,
  3. APPLE_PIPPIN,
  4. APPLE_GRANNY_SMITH;
  5. }
  6. public enum Orange {
  7. ORANGE_NAVEL,
  8. ORANGE_TEMPLE,
  9. ORANGE_BLOOD;
  10. }

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);// 编译错误

  1. - **避免了编译器对常量的优化带来的困扰。**增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码,因为避免了编译器对常量的优化,常量值并没有被编译到客户端代码中。
  2. <br />
  3. - **可以遍历所有枚举常量。如**`Apple.values()`
  4. **
  5. - **每个枚举实例都对应一个toString方法,可以将枚举实例转换成可打印的字符串。**
  6. ```java
  7. enum Apple{
  8. FUJI(0,"富士苹果");
  9. private final int code;
  10. private final String name;
  11. Apple(int code, String name) {
  12. this.code = code;
  13. this.name = name;
  14. }
  15. @Override
  16. public String toString() {
  17. return "Apple{" + "code=" + code +
  18. ", name='" + name + '\'' +
  19. '}';
  20. }
  21. }

enum实战

枚举类的数据域和方法。

将数据与常量关联、添加方法:

  1. public enum Planet {
  2. MERCURY(3.302e+23, 2.439e6),
  3. VENUS (4.869e+24, 6.052e6),
  4. EARTH (5.975e+24, 6.378e6),
  5. MARS (6.419e+23, 3.393e6),
  6. JUPITER(1.899e+27, 7.149e7),
  7. SATURN (5.685e+26, 6.027e7),
  8. URANUS (8.683e+25, 2.556e7),
  9. NEPTUNE(1.024e+26, 2.477e7);
  10. private final double mass; // In kilograms
  11. private final double radius; // In meters
  12. private final double surfaceGravity; // In m / s^2
  13. // Universal gravitational constant in m^3 / kg s^2
  14. private static final double G = 6.67300E-11;
  15. // Constructor
  16. Planet(double mass, double radius) {
  17. this.mass = mass;
  18. this.radius = radius;
  19. surfaceGravity = G * mass / (radius * radius);
  20. }
  21. public double mass() { return mass; }
  22. public double radius() { return radius; }
  23. public double surfaceGravity() {
  24. return surfaceGravity;
  25. }
  26. public double surfaceWeight(double mass) {
  27. return mass * surfaceGravity; // F = ma
  28. }
  29. }
  1. public class PlanetDemo {
  2. public static void main(String[] args) {
  3. double earthWeight = Double.parseDouble(args[0]);
  4. double mass = earthWeight / Planet.EARTH.surfaceGravity();
  5. for (Planet p : Planet.values()) {
  6. System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
  7. }
  8. //args[0]=30输出结果
  9. //Weight on MERCURY is 11.337201
  10. //Weight on VENUS is 27.151530
  11. //Weight on EARTH is 30.000000
  12. //Weight on MARS is 11.388120
  13. //Weight on JUPITER is 75.890383
  14. //Weight on SATURN is 31.965423
  15. //Weight on URANUS is 27.145664
  16. //Weight on NEPTUNE is 34.087906
  17. }
  18. }

行为关联枚举常量

有时候会使用枚举类实例来作为逻辑条件来分发至不同的方法,如采用枚举类来写加、减、乘、除的运算。代码如下:

  1. public enum Operation {
  2. PLUS, MINUS, TIMES, DIVIDE;
  3. double apply(double x, double y) {
  4. switch(this) {
  5. case PLUS: return x + y;
  6. case MINUS: return x - y;
  7. case TIMES: return x * y;
  8. case DIVIDE: return x / y;
  9. }
  10. throw new AssertionError("Unknown op: " + this);
  11. }
  12. }

字段代码可行,但是不太好看。如果没有throw语句,他就不能通过遍历(可能没有返回值),虽然从技术角度来看代码的结束部分是可以执行到的,只有一种情况就是:添加了新的枚举常量,却忘记给switch添加相应条件。这时候,throw语句就成了检测bug的语句了。

为了减少这种失误,enum提供了更加强大的解决方案,就是将枚举常量和行为关联起来:

  1. public enum Operation {
  2. PLUS {
  3. @Override
  4. double apply(double x, double y) {
  5. return x + y;
  6. }
  7. },
  8. MINUS {
  9. @Override
  10. double apply(double x, double y) {
  11. return x - y;
  12. }
  13. },
  14. TIMES {...},
  15. DIVIDE {...};
  16. abstract double apply(double x, double y);
  17. }

甚至同时关联数据和行为:

  1. public enum Operation {
  2. PLUS("+") {
  3. @Override
  4. double apply(double x, double y) {
  5. return x + y;
  6. }
  7. },
  8. MINUS("-") {...},
  9. TIMES("*") {...},
  10. DIVIDE("/") {...};
  11. private String symbol;
  12. Operation(String symbol) {
  13. this.symbol = symbol;
  14. }
  15. @Override
  16. public String toString() {
  17. return symbol;
  18. }
  19. abstract double apply(double x, double y);
  20. }
  1. public class OperationDemo {
  2. public static void main(String[] args) {
  3. double x = Double.parseDouble(args[0]);
  4. double y = Double.parseDouble(args[1]);
  5. for (Operation op : Operation.values()) {
  6. System.out.println(String.format("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
  7. }
  8. //输入2 4
  9. //2.000000 + 4.000000 = 6.000000
  10. //2.000000 - 4.000000 = -2.000000
  11. //2.000000 * 4.000000 = 8.000000
  12. //2.000000 / 4.000000 = 0.500000
  13. }
  14. }

如果添加新的常量,你就不可能会忘记提供apply方法,因为如果你真的忘记了,编译器也会提醒你,因为枚举类型中的抽象方法必须被他所有常量中的具体方法所覆盖。

另外,贴心的enum有一个自动产生的valueOf(String)方法,他将常量(不是数据)的名字转变成枚举实例。但是,toString()和valueOf是相反的一对操作。valueOf是通过名称获取枚举常量对象。而toString()是通过枚举常量获取枚举常量的名称。如果覆盖了toString,需要考虑编写一个fromString方法。我们通常需要在enum中新增个静态常量来获取。如:

  1. public enum Operation {
  2. ...
  3. private String symbol;
  4. public static final Map<String, Operation> OPERS_MAP = Maps.newHashMap();
  5. static {
  6. for (Operation op : Operation.values()) {
  7. OPERS_MAP.put(op.toString(), op);
  8. }
  9. }
  10. Operation(String symbol) {
  11. this.symbol = symbol;
  12. }
  13. @Override
  14. public String toString() {
  15. return symbol;
  16. }
  17. public Operation fromString(String name){
  18. return OPERS_MAP.get(name);
  19. }
  20. }

这里可能会有这样的疑惑:每个枚举实例都在调用构造函数时将自身添加到OPERS_MAP中可以吗?根据以往的经验好像没问题,毕竟static语句会比构造器先执行啊。实际上,这样会导致编译时错误。因为enum类的构造器运行的时候,这些静态域还没有被实例化。不过,编译时常量域除外。注意 public static final Map<String, Operation>不是编译时常量域(字面量)。
**

策略枚举(一定要看)

行为关联常量固然在很多时候都好用,但是它也有一个不可忽视的缺点,即如果每个枚举常量都有公共的部分处理该怎么办,如果每个枚举常量关联的方法里都有公共的部分,那不仅不美观,还违反了DRY原则。

如何同时克服行为关联常量和数据关联常量的缺点呢?抽象,抽象行为,再包装一层,这就是策略模式,策略枚举啦。
**

  1. public enum PayRoll {
  2. MONDY(PayType.WEEKDAY),
  3. TUESDAY(PayType.WEEKDAY),
  4. WEDNESDAY(PayType.WEEKDAY),
  5. THURSDAY(PayType.WEEKDAY),
  6. FRIDAY(PayType.WEEKDAY),
  7. SATURDAY(PayType.WEEKEND),
  8. SUNDAY(PayType.WEEKEND);
  9. private final PayType payType;
  10. PayRoll(PayType payType) {
  11. this.payType = payType;
  12. }
  13. double pay(double hoursWorked, double payRate) {
  14. return payType.pay(hoursWorked, payRate);
  15. }
  16. private enum PayType {
  17. WEEKDAY {
  18. @Override
  19. double overtimePay(double hoursWorked, double payRate) {
  20. double overtime = hoursWorked - HOURS_PER_SHIFT;
  21. return overtime <= 0 ? 0 : overtime * payRate / 2;
  22. }
  23. },
  24. WEEKEND {
  25. @Override
  26. double overtimePay(double hoursWorked, double payRate) {
  27. return hoursWorked * payRate / 2;
  28. }
  29. };
  30. private static final int HOURS_PER_SHIFT = 8;
  31. abstract double overtimePay(double hoursWorked, double payRate);
  32. double pay(double hoursWorked, double payRate) {
  33. double basePay = hoursWorked * payRate;
  34. return basePay + overtimePay(hoursWorked, payRate);
  35. }
  36. }
  37. }

真是高明,大人真的是又高又硬啊。

鸡蛋里挑骨头

枚举类有一个小小的缺点,即装载和初始化枚举时,会有时间上和空间上的成本。除了受资源约束的设备,如手机和面包机之外,在实践中不必太在意这个问题。