EffectiveJava(30) -- 全面解析enum类型

—-在大多数项目中,我们会经常使用int类型来声明final类型的常量,它在不考虑安全的情况下确实能满足我们绝大多数的需求.但是在JDK1.5版本发布之后,声明一组固定的常量组成合法值的类型就建议使用enum(枚举)类型代替.原因有三:

– - -1.int类型对安全性和使用方便性没有任何帮助.就像你可以用==/=将不同类型进行对比甚至赋值,而这违背了它的final特性
- - - 2.如果与枚举常量关联的int发生了变化,客户端就必须重新编译.如果没有重新编译,它就会带着不确定的行为运行,你无法预测程序的运行结果
- - - 3.并没有方便的方式将int枚举常量翻译成可打印的字符串,而在实际操作中我们经常要这么做.

那么什么是枚举呢?

枚举就是通过公有的静态final域为每个枚举常量导出实例的类.它很像一个特殊的class,实际上enum声明定义的类型就是一个类。
而这些类都是类库中Enum类的子类(java.lang.Enum<E>)。它们继承了这个Enum中的许多有用的方法。
我们对代码编译之后发现,编译器将enum类型单独编译成了一个字节码文件:类名.class。
它们是单例的泛型化,本质上还是int类型

.

枚举的工作模式又是怎么样的呢?

包含多个同名常量的多个枚举可以在同一个系统中和平共处,因为每个枚举都有自己的命名空间.
你可以增加或者重新排列枚举类型的常量并无需重新编译它的客户端代码,因为常量值并没有被编译到客户端代码中,而是在int枚举模式中.
导出常量的域在枚举类型和他的客户端之间提供了一个隔离层,

接下来是我实现一个枚举类型的范例:通过行星的质量和半径计算它的表面重力

    public enum Planet {
      //枚举行星名称
      MERCURY(3.302e+23, 2.439e6), VENUS(4.869e+24, 6.052e6), EARTH(5.975e+24, 6.378e6);

      private final double mass;
      private final double radius;
      //表面重力
      private final double surfaceGravity;

      private static final double G = 6.6730E-11;

      //为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器
      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;
      }
    }

在这个示例中,我们再enum类型里添加了mass,radius,surfaceGravity域和其对应的构造方法,enum是允许我们这么做的,它还可以实现任意接口.
我们可以用任何适当的方法来增强枚举类型,它可以从一个枚举常量的简单集合演变成全功能的抽象类!
这是它的测试方法,他会以表格形式显示出2kg的物体在所有行星上的重量–

    double earthWeigth = 2;
    double mass = earthWeigth/Planet.EARTH.surfaceGravity();
    for(Planet p:Planet.values()){
      System.out.printf("Weight on %s is %f%n",p,p.surfaceWeight(mass));
    }

接下来我们讲讲enum类型使用的注意事项:
1.除非非要将枚举方法导出至它的客户端,否则都应该将它声明为私有的.
2.如果一个枚举具有普遍适用性,他应该称为一个顶层类.如果它被用在一个特定的顶层类中,他就应该成为该顶层类的一个成员类.

enum还可以为我们实现更加强大的功能
当你需要将本质上不同的行为与每个常量关联起来时,上面的例子明显有些不够用了.
我们用枚举提供一个方法来执行计算器的四大基本操作:

  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);
    }
  }

每当你添加了新的计算操作,你都要添加switch条件,否则就会运行失败.这种代码是很脆弱的,因为你不能寄希望于程序员时刻记住去编写一个容易忽略的方法,你应该强制程序员去实现它.
强制实现一个方法我们可以通过实现一个接口或者实现一个为实现的抽象方法,这里我们选后一种.
在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法,如果你添加新的常量却没有实现抽象方法,编译器会提醒你.

    public enum Operation {
      PLUS("+"){
        double apply(double x,double y){
          return x + y;
        }
      },
      MINUS("-"){
        double apply(double x,double y){
          return x - y;
        }
      },
      TIMES("*"){
        double apply(double x,double y){
          return x * y;
        }
      },
      DIVIDE("/"){
        double apply(double x,double y){
          return x/y;
        }
      };
      private final String symbol;
      Operation(String symbol){
        this.symbol = symbol;
      }
      @Override public String toString(){
        return symbol;
      }
      abstract double apply(double x,double y);
    }

测试 double x = 2;
double y = 4;
for(Operation op:Operation.values()){
System.out.printf(“%f %s %f = %f%n”,x,op,y,op.apply(x, y));
}可以使用switch呢? – 枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为.

  public static Operation inverse(Operation op){
    switch(op){
      case PLUS: return Operation.MINUS;
      case MINUS:return Operation.PLUS;
      case TIMES: return Operation.DIVIDE;
      case DIVIDE:return Operation.TIMES;
    }
  }

values():枚举类都要继承的Enum的方法,同时还有以下方法可以使用:
(1) ordinal()方法: 返回枚举值在枚举类种的顺序。这个顺序根据枚举值声明的顺序而定。
- - - Color.RED.ordinal(); //返回结果:0
- - - Color.BLUE.ordinal(); //返回结果:1
(2) compareTo()方法: Enum实现了java.lang.Comparable接口,因此可以比较象与指定对象的顺序。Enum中的compareTo返回的是两个枚举值的顺序之差。当然,前提是两个枚举值必须属于同一个枚举类,否则会抛出ClassCastException()异常。(具体可见源代码)
- - - Color.RED.compareTo(Color.BLUE); //返回结果 -1
(3) values()方法: 静态方法,返回一个包含全部枚举值的数组。
- - - Color[] colors=Color.values();
- - - for(Color c:colors){
- - - System.out.print(c+”,”);
- - - }//返回结果:RED,BLUE,BLACK YELLOW,GREEN,
(4) toString()方法: 返回枚举常量的名称。
- - - Color c=Color.RED;
- - - System.out.println(c);//返回结果: RED
(5) valueOf()方法: 这个方法和toString方法是相对应的,返回带指定名称的指定枚举类型的枚举常量。
- - - - Color.valueOf(“BLUE”); //返回结果: Color.BLUE
(6) equals()方法: 比较两个枚举类对象的引用。

上面我们提到过,enum可以很轻松的实现将enum常量以字符串的方式打印出来,下面我们就利用enum继承的values()方法的来实现这个方法

  private static final Map<String,Operation> stringToEnum = new HashMap<String,Operation>();
  static{
    for(Operation op :values()){
      stringToEnum.put(op.toString(),op);
    }
  }
  public static Operation fromString(String symbol){
    return stringToEnum.get(symbol);
  }

一大堆赞美过后,往往也要有个但是.虽然Enum类型好像什么时候都可以用,那么为什么使用它的程序员好像依然不多呢?
1.Enum类型相对来说比int要消耗更多的系统资源
2.从上面的举例中我们也看到,通过Enum实现某个功能是比较繁杂的,普通程序员并不能很好的理解并知道什么时候使用它.
3.它使得在枚举常量中共享代码变得更加困难
例如:给公司员工算总工资(总工资=基本工资+加班工资)
注:在有趣的JAVA中,我们说明了浮点数并不适合于计算工资等类似的计算.
具体请转传送门:
http://blog.csdn.net/jacxuan/article/details/62238406

  enum PayrolDay{
    MONDAY,TUERDAY,WEDENSDAY,THURSDAY,FRIDAY,STAURDAY,SUNDAY;
    //基本工时
    private static final int HOURS_PER_SHIFT = 8;
    double base(double hoursWorked,double payRate){
      double basePay = hoursWorked * payRate;
      double overtimePay;
      switch(this){
        case STAURDAY : case SUNDAY : overtimePay = hoursWorked * payRate*1.5;
        default:
         overtimePay = hoursWorked<=HOURS_PER_SHIFT?0:(hoursWorked - HOURS_PER_SHIFT)*payRate*1.5;
         break;
      }
      return basePay + overtimePay;
    }
  }

这段代码是十分危险的,因为如果你在国庆小长假的时候被公司强制要求加班的时候,你只能双休日才能领到加班工资.换句话说,这段代码扩展性非常差,它很难增加新的’工资方案’.那么我们如何解决这个问题呢?
必须用工作日加班的具体方法代替PayrollDay中抽象的overtimePay方法.每当你添加一个枚举常量时,就必须强制选择一种加班报仇策略.

  public enum PayrollDay {
  //声明enum常量并添加工作报酬策略
  MONDAY(PayType.WEEKDAY),TUESDAY(PayType.WEEKDAY),WEDNESDAY(PayType.WEEKDAY),THURSDAY(PayType.WEEKDAY),FRIDAY(PayType.WEEKDAY),SATUSDAY(PayType.WEEKEND),SUNDAY(PayType.WEEKEND);

  private final PayType payType;
  //添加构造方法使其能够选择报酬策略
  PayrollDay(PayType payType) {
    this.payType = payType;
  }
  double pay(double hoursWorked,double payRate){
    return payType.pay(hoursWorked, payRate);
  }
  /将加班报仇策略添加到PayType方法中
  private enum PayType{
    WEEKDAY{
      double overtimePay(double hours,double payRate){
        return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)*payRate *1.5;
      }
    },
    WEEKEND{
      double overtimePay(double hours,double payRate){
        return hours*payRate*1.5;
      }
    };
    private static final int HOURS_PER_SHIFT = 8;
    abstract double overtimePay(double hrs,double payRate);
    double pay(double hoursWorked,double payRate){
      double basePay = hoursWorked * payRate;
      return basePay + overtimePay(hoursWorked,payRate);
    }
  }
}

枚举类型这么强大,我们应该在何时选择它呢?
每当需要一组固定常量的时候.这包括”天然的枚举类型”,如行星,一周的天数,棋子的数目等.他也包括你在编译时就知道其所有可能值的其他集合,如菜单选项,操作代码,命令标记等.最后,记住一点.枚举类型中的常量集并不一定要始终保持不变.

总结:与int类型相比,枚举类型的优势是不言而喻的.枚举类型要易读的多,也要更加安全,功能更加强大.许多枚举类型都不需要显式的构造器或者成员名单许多其他枚举则受益于”每个常量于属性的关联”以及”提供行为受这个属性影响的方法”.只有极少数的枚举受益于将多种行为与单个方法关联.在这种相对少见的情况下,特定于常量的方法要优于启用自有值的枚举.如果多个枚举常量同时共享相同的行为,则考虑枚举.

原文地址:https://www.cnblogs.com/qwop/p/6637273.html