【Effective Java】条30:使用枚举代替int常量

在枚举类型出现之前,一般都常常使用int常量或者String常量表示列举相关事物。如:

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常量以下不足:
1. 在类型安全方面,如果你想使用的是ORANGE_NAVEL,但是传递的是APPLE_FUJI,编译器并不能检测出错误;
2. 因为int常量是编译时常量,被编译到使用它们的客户端中。若与枚举常量关联的int发生了变化,客户端需重新编译,否则它们的行为就不确定;
3. 没有便利方法将int常量翻译成可打印的字符串。这里的意思应该是比如你想调用的是ORANGE_NAVEL,debug的时候显示的是0,但你不能确定是APPLE_FUJI还是ORANGE_NAVEL

如果你想使用String常量,虽然提供了可打印的字符串,但是性能会有影响。特殊是对于有些新手开发,有可能会直接将String常量硬编码到代码中,导致以后修改很困难。

所有这一切,enum都给出了具体的解决。唯一的缺点只是需要增加enum加载和实例化的时间。

enum使用方法

enum默认构建

以上面的APPLEORANGE为例,用enum重写:

public enum Apple {
    APPLE_FUJI,
    APPLE_PIPPIN,
    APPLE_GRANNY_SMITH;
}

public enum Orange {
    ORANGE_NAVEL,
    ORANGE_TEMPLE,
    ORANGE_BLOOD;
}

在调用的时候,直接使用enum类型,在编译的时候可以直接指定类型,否则编译不通过;并且debug的时候,显示的是enum中的常量(APPLE_FUJI这样的),可以一眼看出是否用错;最后由于枚举导出的常量域(APPLE_FUJI等)与客户端之间是通过枚举来引用的,再增加或者重排序枚举类型中的常量后,并不需要重新编译客户端代码

enum枚举常量与数据关联

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

enum枚举常量与行为关联

有些时候将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);
    }
}

大家一开始都会这样写的。实际开发中,有很多开发者也这样写。但是有个不足:如果需要新增加运算,譬如模运算,不仅仅需要添加枚举类型常量,还需要修改apply方法。万一忘记修改了,那就是运行时错误。将代码修改如下:

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 {
    @Override
    double apply(double x, double y) {
      return x * y;
    }
  },

  DIVIDE {
    @Override
    double apply(double x, double y) {
      return x / y;
    }
  };

  abstract double apply(double x, double y);
}

每次新增加运算种类,都需要重写apply方法,这样就不会有遗漏修改。

你可以写的更详细些:

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("*") {
    @Override
    double apply(double x, double y) {
      return x * y;
    }
  },

  DIVIDE("/") {
    @Override
    double apply(double x, double y) {
      return x / y;
    }
  };

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

一般,enum中重写了toString方法之后,enum中自生成的valueOf(String)方法不能根据枚举常量的字符串(toString生成)来获取枚举常量。我们通常需要在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("*") {
    @Override
    double apply(double x, double y) {
      return x * y;
    }
  },

  DIVIDE("/") {
    @Override
    double apply(double x, double y) {
      return x / y;
    }
  };

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

  abstract double apply(double x, double y);
}

可以通过调用Operation.OPERS_MAP.get(op.toString())来获取对应的枚举常量。

在有些特定的情况下,此写法有个缺点,即如果每个枚举常量都有公共的部分处理该怎么办,如果每个枚举常量关联的方法里都有公共的部分,那不仅不美观,还违反了DRY原则。这就是下面的枚举策略模式。

枚举策略模式

直接上例子来分析:

enum PayrollDay {
    MONDAY, 
    TUESDAY, 
    WEDNESDAY, 
    THURSDAY, 
    FRIDAY,
    SATURDAY, 
    SUNDAY;

    private static final int HOURS_PER_SHIFT = 8;

    double pay(double hoursWorked, double payRate) {
        double basePay = hoursWorked * payRate;

        double overtimePay; // Calculate overtime pay
        switch(this) {
            case SATURDAY: case SUNDAY:
                overtimePay = hoursWorked * payRate / 2;
                break;
            default: // Weekdays
                overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
        }

        return basePay + overtimePay;
    }
}

以上代码是计算工人工资。平时工作8小时,超过8小时,以加班工资方式另外计算;如果是双休日,都按照加班方式处理工资。

上面代码的写法和上一小节给出的差不多,通过switch来分拆计算。还是一样的问题,如果此时新增加一种工资的计算方式,枚举常量需要改,pay方法也需要改。按上一小节的介绍继续修改:

enum PayrollDay {
    MONDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    TUESDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    WEDNESDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    THURSDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    FRIDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    },
    SATURDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = overtimePay = hoursWorked * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    SUNDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = overtimePay = hoursWorked * payRate / 2;
            return basePay + overtimePay;
        }
    }, ;

    private static final int HOURS_PER_SHIFT = 8;

    abstract double pay(double hoursWorked, double payRate);
}

看了上面的代码,我觉得大家都不会这样写吧。其实细想一下,最主要的不同就是计算加班时间的工资方式不同,也就是分工作日和双休日的。继续修改:

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

虽然看起来代码不够简洁,但是修改起来确实比较安全,不怕有遗漏。

补充一点

从上面可以看出,不推荐在enum中使用switch...case...来判断不同的行为。那什么时候可以使用呢?主要是适用于给外部的枚举类型增加特定于常量的行为。如,假设Operation枚举不受开发者自己控制,但是希望它有一个实例方法来返回每个运算的反运算,则可以:

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;
        default: throw new AssertionError("Unknown op: " + op);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值