枚举

ℹ️本篇定位

枚举(enum)是 JDK 5 引入的"有限实例集合"类型:一周七天、订单四种状态、HTTP 状态码分组——凡是"取值范围固定"的场景都该用它,而不是 int 常量或 String 魔法值。本篇按老规矩从问题出发:为什么不用常量?枚举底层是什么?单例为什么推荐枚举?

一、问题提出:常量方案的三大痛点

JDK 5 之前表示"季节",只能靠 int 常量或接口常量:

public class SeasonConstant {
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;
}

三个致命问题:

  1. 类型不安全int season 可以被赋成 99,编译器不吭声,运行时才炸;
  2. 没有命名空间PRINTING = 1SEASON_SPRING = 1 打架,只能靠前缀区分;
  3. 打印无意义:日志里打出 season=2,谁也不知道是夏是冬。

String 常量同样不安全(拼写错误静默通过)。枚举 = 编译器强制"取值只能在这几个里挑"

二、基本使用:比想象中能干

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER   // 分号可省略;后面还要写成员/方法时不能省
}
Season s = Season.SUMMER;
s.name();      // "SUMMER" —— 枚举常量的名字
s.ordinal();   // 1 —— 声明序号(从 0 开始)
s.toString();  // 默认同 name()
Season.valueOf("SPRING");   // 字符串 → 枚举,找不到抛 IllegalArgumentException
Season.values();            // 全部常量的数组(编译器合成方法)

枚举本质是类,所以可以有字段、构造器、方法——每个常量就是一份实例:

public enum Season {
    SPRING("春", "温暖"),
    SUMMER("夏", "炎热"),
    AUTUMN("秋", "凉爽"),
    WINTER("冬", "寒冷");        // 常量列表以分号结束

    private final String cnName;
    private final String desc;

    Season(String cnName, String desc) {   // 构造器天然 private
        this.cnName = cnName;
        this.desc = desc;
    }

    public String getCnName() { return cnName; }
    public String getDesc()   { return desc; }
}

规则:构造器只能是 private(编译器强制,反正外界也不该 new);常量必须在第一行列出;字段建议 final。

抽象方法玩法:每个常量各自实现(编译器生成匿名子类)——

public enum Operation {
    PLUS  { public int apply(int a, int b) { return a + b; } },
    MINUS { public int apply(int a, int b) { return a - b; } };

    public abstract int apply(int a, int b);
}
Operation.PLUS.apply(1, 2);   // 3

三、底层原理:枚举是"被驯化的类"

javap 看一眼 Season 的字节码:

public final class Season extends java.lang.Enum<Season> {
    public static final Season SPRING;   // 每个"常量"就是一个 static final 实例
    ...
    public static Season[] values();
    public static Season valueOf(java.lang.String);
}

三个关键事实:

  1. enum 隐式继承 java.lang.Enum,所以枚举不能再 extends 别的类(Java 单继承被用掉了);可以实现接口;
  2. 隐式 final(无抽象方法时),类被编译器锁死,外界无法继承、无法反射创建——Constructor.newInstance() 遇到枚举会直接抛 IllegalArgumentException
  3. values() / valueOf()编译器合成的静态方法,在 Enum 父类里并不存在——这就是"在父类里找不到 values() 却能调用"的原因;每次调用 values() 都会克隆新数组,高频调用应缓存。

Enum 自带的 name()/ordinal()/compareTo()(按 ordinal 比较)/equals()(final,就是 ==)都是围绕"实例有限且唯一"展开的。

四、枚举实现单例:天然防反射、防序列化

public enum Singleton {
    INSTANCE;

    private int counter;
    public void doSomething() { counter++; }
}

调用:Singleton.INSTANCE.doSomething()

为什么《Effective Java》力荐枚举单例:

攻击面 普通单例(DCL/静态内部类) 枚举单例
反射创建 需要构造器抛异常防御 JVM 层面直接禁止,反射抛异常
序列化破坏 需重写 readResolve() 防止反序列化出新实例 JVM 保证同一枚举常量反序列化后仍是同一实例
写法复杂度 双重检查锁一坨模板代码 一行

枚举单例唯一的"缺点":不能懒加载(类加载即初始化)——但对绝大多数场景这根本不是问题。

五、与集合的配合:EnumMap / EnumSet

当枚举做 key / 元素时,JDK 提供了专为枚举优化的集合——内部就是数组+位图,无哈希计算,性能碾压 HashMap/HashSet:

// EnumMap:内部是 Season.values().length 长度的数组,key.ordinal() 就是下标
Map<Season, Integer> count = new EnumMap<>(Season.class);
count.put(Season.SPRING, 3);

// EnumSet:long 位图实现(常量 ≤64 个时),add/contains 都是 O(1) 位运算
EnumSet<Season> warm = EnumSet.of(Season.SPRING, Season.SUMMER);
warm.contains(Season.SUMMER);   // true

面试一句:key 是枚举时用 EnumMap 是白捡的性能优化(无装箱、无哈希冲突、无扩容)。

六、switch 支持枚举

switch (s) {
    case SPRING -> desc = "回暖";     // JDK 14+ 箭头语法;case 直接写常量名,不加枚举类前缀
    case SUMMER -> desc = "炎热";
    default     -> desc = "其他";
}

switch 对枚举的支持从 JDK 5 就有,编译器按 ordinal 跳表;JDK 21 的 switch 模式匹配进一步支持枚举与模式组合(新特性速览篇展开)。

七、实战与坑

1. 用枚举替代 if-else 分支(策略味道):状态机、权限点、支付渠道,每种取值一个行为实现,加状态只加枚举常量不改主干(开闭原则的轻量版)。

2. 数据库存取:MyBatis/JPA 都支持枚举映射——注意存 name() 还是 ordinal() 的选择:ordinal 会因调整常量顺序而错位,永远优先存 name 或 code。给枚举加个 code 字段 + 静态查找方法是最稳的落库姿势:

public enum OrderStatus {
    PAID(1), SHIPPED(2), DONE(3);
    private final int code;
    OrderStatus(int code) { this.code = code; }
    public static OrderStatus of(int code) {
        for (OrderStatus s : values()) if (s.code == code) return s;
        throw new IllegalArgumentException("未知状态: " + code);
    }
}

3. values() 高频调用坑:每次返回克隆数组,写在超热循环里是无谓开销,缓存成 private static final List 即可。

4. 枚举能常量化配置吗:枚举常量编译期固化,改取值要重新编译发版——频繁变化的配置别用枚举,那是数据库/配置中心的活。枚举表达的是"业务语义上就固定"的集合。

八、高频面试题速答

  1. 枚举和常量类的区别? 类型安全(非法值编译不过)、有命名空间、可带行为、天然单例;常量类三条全无。
  2. 枚举能继承类吗? 不能(已隐式继承 Enum);能实现接口,抽象方法各常量自行实现。
  3. 枚举单例为什么安全? JVM 禁止反射创建枚举实例 + 序列化由 JVM 保证唯一(对照第四节表格)。
  4. values() 是父类方法吗? 不是,编译器合成;且每次调用克隆新数组。
  5. EnumMap 为什么快? 数组下标 = ordinal,省掉哈希计算与冲突处理。

勾连


⬅️ 05-动态代理 🏠 00-Java ➡️ 07-Lambda与函数式接口