Java:Effective java学习笔记之 用enum代替int常量

用enum代替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常量以下不足:

  • 在类型安全方面,如果你想使用的是ORANGE_NAVEL,但是传递的是APPLE_FUJI,编译器并不能检测出错误;
  • 因为int常量是编译时常量,被编译到使用它们的客户端中。若与枚举常量关联的int发生了变化,客户端需重新编译,否则它们的行为就不确定;
    • 因为这些常量被声明为final的,也就是说在编译阶段是被直接替换掉的,这样带来的后果是如果“常量”被修改了,那么使用了个值的客户端都需要重新被编译。
  • 没有便利方法将int常量翻译成可打印的字符串。这里的意思应该是比如你想调用的是ORANGE_NAVELdebug的时候显示的是0,但你不能确定是APPLE_FUJI还是ORANGE_NAVEL

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

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

1、用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等)与客户端之间是通过枚举来引用的,再增加或者重排序枚举类型中的常量后,并不需要重新编译客户端代码

  • java枚举类型背后的基本想法非常简单:Java的枚举和其他语言的枚举不同,Java的枚举实际上是一个完整的类,他们就是通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。因为客户端既不能创建枚举类型的实例,也不能对他进行扩展,因此很可能没有实例,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的。他们是单例的泛型化。本质上是单元素的枚举。枚举类型为类型安全的枚举模式。
  • 枚举提供了编译时的类型安全。如果声明一个参数的类型为Apple,就可以保证,被传到该参数上的任何非null的对象一定属于三个有效的Apple值之一。试图传递类型错误的值时,会导致编译时错误,就像试图将某种枚举类型的表达式赋给另一种枚举类型的变量,或者试图利用==操作符比较不同枚举类型的值一样。
  • 包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。你可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码,因为导出常量的域在枚举类型和他的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在int枚举模式中。最终,可以通过调用toString方法,将枚举转换成可打印的字符串。
  • 枚举是一个没有构造方法,不可继承,不可创建实例的类,并且除了声明的枚举常量外,不能再有任何实例。相当于单元素的枚举,这也是为什么枚举是声明单例类最好的方式。

2、enum枚举常量与数据关联

枚举类型还允许添加任意的方法和域,并实现任意的接口。他们提供了所有Object方法的高级实现,实现了Comparable和Serializable接口,并针对权举类型的可任意改变性设计了序列化方式。

将方法或者域添加到枚举类型中的作用:

  • 1)将数据与他的常量关联起来。
  • 2)可以利用任何适当的方法来增强枚举类型。枚举类型可以先作为枚举常量的一个简单集合,随着时间的推移再演变成为全功能的抽象。

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


为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域都应该为final的。它们可以是公有的,但最好将他们做成是私有的,并提供公有的访问方法。

Planet功能十分强大,下面我们举个例子:

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

注意Planet就像所有的枚举一样,它有一个静态的values方法,按照声明顺序返回他的值数组。注意没有覆写的toString方法返回每个枚举值的声明名称。

3、枚举常量与行为关联

与枚举常量关联的有些行为,可能只需要用在定义了枚举的类或者包中。这种行为最好被实现成私有的或者包级私有的方法。于是,每个枚举常量都带有一组隐蔽的行为,这使得包含该枚举的类或者包在遇到这种常量时都可以做出适当的反应。

  • 就像其他的类一样,除非迫不得已要将枚举方法导出至它的客户端,否则都应该将它声明为私有的,如有必要,则声明为包级私有的。

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

枚举类型有一个自动产生的valueOf(String)方法,它将常量的名字转变成常量本身。如果在枚举类型中覆盖toString,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举。下列代码(适当的改变了类型名称)可以为任何枚举完成这一技巧,只要每个常量都有一个独特的字符串表示法。

还是引用上文的的OperationImporve2类作为例子

public enum OperationImporve2 {

    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;

    OperationImporve2(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

    abstract double apply(double x, double y);

//Implementing a  fromString method on an enum type
    private static final Map<String, OperationImporve2> stringToEnum = new HashMap<>();

    //Initialize map from constant name to enum constant
    //初始化从常量名称到enum常量的映射
    static {
        for (OperationImporve2 value : values()) {
            stringToEnum.put(value.toString(), value);
        }
    }

    //Return OperationImporve2 for string , or null if string is invalid
    public static OperationImporve2 fromString(String symbol) {
        return stringToEnum.get(symbol);
    }
}
public class Test {
    public static void main(String[] args) {
        //相当于调用OperationImporve2.toString
        System.out.println(OperationImporve2.PLUS);
        OperationImporve2 op = OperationImporve2.fromString("+");
        System.out.println(op.name());
        //相当于调用OperationImporve2.toString
        System.out.println(op);
        double apply = op.apply(1, 2);
        System.out.println(apply);
    }
}

输出:

+
PLUS
+
3.0

在常量被创建之后,OperationImporve2常量从静态代码块中被放入到stringToEnummap中。试图使每个常量都从自己的构造器将自身放入到map中,会导致编译时错误。这是好事,因为如果这是合法的,就会抛出NullPointException异常。枚举构造器不可以访问枚举的静态域,除了编译时常量域之外。这一限制是很有必要的,因为构造器运行的时候,这些静态域还没有被初始化。

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

4、枚举策略模式

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

5、总结

枚举的使用场景:每当需要一组固定常量的时候。

  • 1)天然的枚举类型:行星,一周的天数以及棋子的数目等等。
  • 2)在编译时就知道其所有可能值的其他集合:菜单的选项,操作代码以及命令行标记等。

使用枚举的好处:

  • 修改常量不必重写客户端
  • 可以通过toString()打印属性的具体名称
  • 枚举有类型限制,也有安全检查
  • 枚举可以根据需要实现其他的接口

参考

1、使用enum代替int常量
2、Effective Java笔记第五章枚举和注解第一节用enum代替int常量
3、《Effective Java》读书笔记(三十四)使用枚举类型代替整型常量

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值