美文网首页
使用enum代替int常量

使用enum代替int常量

作者: 没走过的二丁目 | 来源:发表于2018-10-12 11:48 被阅读0次

    在枚举类型出现之前,一般都常常使用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构建

    以上面的APPLE、ORANGE为例,用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枚举常量与数据关联还不够,还需要将枚举常量与行为关联。

    如采用枚举来写加、减、乘、除的运算。代码如下:

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

    相关文章

      网友评论

          本文标题:使用enum代替int常量

          本文链接:https://www.haomeiwen.com/subject/hmifnftx.html