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

缺点:

  1. 类型不安全
    • 你可以将任意int值传给期望“水果类型”的方法,编译器不会报错。
  2. 无命名空间
    • 容易发生冲突,通常通过前缀区分(例如APPLE_)。
  3. 脆弱的
    • int枚举是编译时常量,如果值发生变化,客户端代码需要重新编译,否则行为可能异常。
  4. 调试困难
    • 打印出来是数字,难以直观知道他的含义。
  5. 遍历困难
    • 难以遍历所有合法值,也难以判断。

int枚举模式有一种变体是String枚举模式,虽然可供打印等,但仍不是一种好的枚举方式。
当然,这种模型也不能说完全错误,在一些简单枚举、性能要求高、大量序列化场景仍然可以使用。

枚举类型

对比之下,枚举类型的优点就很突出了。

public enum Fruit {
    APPLE, ORANGE, BANANA
}

Java枚举类型的基本想法非常简单,这些类通过公有的静态final域为每个枚举常量导出一个实例。枚举类型没有可以访问的构造器,所以它是真正的final类。客户端不能创建枚举类型的实例,也不能对它进行扩展,因此不存在实例,而只存在声明过的枚举常量。换句话说,枚举类型是实例受控的。它们是单例(Singleton)的泛型化,本质上是单元素的枚举。

优势:

  1. 类型安全
    • 方法签名可以明确要求Fruit类型:void eatFruit(Fruit fruit)。
    • 不能传递任意值,只能传递Fruit.APPLE等,编译器保障。
  2. 自带命名空间
    • 常量通过枚举类名访问(Fruit.APPLE),清晰且无冲突。
  3. 打印友好
    • toString()返回可读的名称(如"APPLE"),调试方便。
  4. 可遍历
    • 通过Fruit.values()可以获得所有枚举常量的数组。
  5. 功能可扩展(这是关键!)
    • 枚举不是简单的常量列表,它是一个类!可以添加字段、方法等。

简单的使用示例

行星枚举类:

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简洁,但是更加安全和灵活。

总结

  1. 停止使用public static final int模式定义常量集合。

  2. 将任何固定类别的常量(状态、类型、选项、模式)定义为枚举,每当需要一组固定常量,并且在编译时就知道其成员的时候,就应该使用枚举。

  3. 记住枚举是一个类,充分利用其能力:
    3.1 添加字段(如关联的符号、值)。
    3.2 添加方法(如根据字段计算)。
    3.3 实现接口(枚举可以实现一个或多个接口)。

  4. 优先使用“特定于常量的方法实现”或“策略枚举”,而不是在枚举外部使用switch语句,这能获得更好的类型安全性和可维护性。

  5. 理解枚举的天然单例特性,它也是实现单例模式的最佳方式之一

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐