《Effective Java》解读第34条:用enum代替int常量
第34条:用enum代替int常量
枚举类型是由固定常量组成合法值的类型。平时经常使用,就不再过多介绍了。
int枚举模式
在java引入枚举之前,通常是通过一组int常量来表示枚举类型,这种方法叫做int枚举模式。
例如:
// int 枚举模式 - 极度不推荐!
public class Fruit {
public static final int APPLE = 1;
public static final int ORANGE = 2;
public static final int BANANA = 3;
}
缺点:
- 类型不安全
- 你可以将任意int值传给期望“水果类型”的方法,编译器不会报错。
- 无命名空间
- 容易发生冲突,通常通过前缀区分(例如APPLE_)。
- 脆弱的
- int枚举是编译时常量,如果值发生变化,客户端代码需要重新编译,否则行为可能异常。
- 调试困难
- 打印出来是数字,难以直观知道他的含义。
- 遍历困难
- 难以遍历所有合法值,也难以判断。
int枚举模式有一种变体是String枚举模式,虽然可供打印等,但仍不是一种好的枚举方式。
当然,这种模型也不能说完全错误,在一些简单枚举、性能要求高、大量序列化场景仍然可以使用。
枚举类型
对比之下,枚举类型的优点就很突出了。
public enum Fruit {
APPLE, ORANGE, BANANA
}
Java枚举类型的基本想法非常简单,这些类通过公有的静态final域为每个枚举常量导出一个实例。枚举类型没有可以访问的构造器,所以它是真正的final类。客户端不能创建枚举类型的实例,也不能对它进行扩展,因此不存在实例,而只存在声明过的枚举常量。换句话说,枚举类型是实例受控的。它们是单例(Singleton)的泛型化,本质上是单元素的枚举。
优势:
- 类型安全
- 方法签名可以明确要求Fruit类型:void eatFruit(Fruit fruit)。
- 不能传递任意值,只能传递Fruit.APPLE等,编译器保障。
- 自带命名空间
- 常量通过枚举类名访问(Fruit.APPLE),清晰且无冲突。
- 打印友好
- toString()返回可读的名称(如"APPLE"),调试方便。
- 可遍历
- 通过Fruit.values()可以获得所有枚举常量的数组。
- 功能可扩展(这是关键!)
- 枚举不是简单的常量列表,它是一个类!可以添加字段、方法等。
简单的使用示例
行星枚举类:
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);
// 字段
private final double mass; // 千克
private final double radius; // 米
private final double surfaceGravity; // 计算得出
// 通用常量
private static final double G = 6.67300E-11;
// 构造器(默认是 private)
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
this.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
}
}
数据(质量、半径)和行为(计算表面重力、重量)被紧密地封装在枚举常量中,这是int常量绝对无法实现的。
使用:
double earthWeight = 70.0; // 千克
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values()) {
System.out.printf("Your weight on %s is %f%n",
p, p.surfaceWeight(mass));
}
枚举类型的高级模式
特定于常量的实现方法
不同枚举对象方法实现不同,其实就是通过匿名类的方式做特定实现而已。
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public 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; }
// 抽象方法,强制每个常量实现自己的逻辑
public abstract double apply(double x, double y);
}
注意:添加新操作(如EXP)时,必须实现apply方法,否则编译报错,确保了完整性。
策略枚举
虽然switch在枚举上可用,但更推荐将行为封装在枚举内部,这样更面向对象,减少因漏写case导致错误的风险。
通过私有嵌套的策略枚举PayType实例传入PayrollDay构造器中。
// 推荐:行为封装在枚举内
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), // 特定常量使用特殊策略
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
PayrollDay() { this(PayType.WEEKDAY); } // 默认构造器
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 :
(hours - HOURS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hrs, double rate);
}
public double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + payType.overtimePay(hoursWorked, payRate);
}
}
虽然没有switch简洁,但是更加安全和灵活。
总结
-
停止使用public static final int模式定义常量集合。
-
将任何固定类别的常量(状态、类型、选项、模式)定义为枚举,每当需要一组固定常量,并且在编译时就知道其成员的时候,就应该使用枚举。
-
记住枚举是一个类,充分利用其能力:
3.1 添加字段(如关联的符号、值)。
3.2 添加方法(如根据字段计算)。
3.3 实现接口(枚举可以实现一个或多个接口)。 -
优先使用“特定于常量的方法实现”或“策略枚举”,而不是在枚举外部使用switch语句,这能获得更好的类型安全性和可维护性。
-
理解枚举的天然单例特性,它也是实现单例模式的最佳方式之一
更多推荐




所有评论(0)