在 Java 开发中,经常需要定义一组固定的常量,比如订单状态、用户角色、星期、季节等。早期的做法是使用 public static final 常量:
|
1 2 3 4 5 6 |
public class OrderStatus { public static final int PENDING = 0; public static final int PROCESSING = 1; public static final int COMPLETED = 2; public static final int CANCELLED = 3; } |
这种方式存在明显缺陷:类型不安全(可以传入任意 int 值)、可读性差、无法遍历所有常量。Java 5 引入的枚举(enum)正是为解决这些问题而生 。
枚举是一种特殊的类,它表示一组固定的常量,提供了类型安全、更好的可读性和丰富的扩展能力 。
使用 enum 关键字定义枚举,常量之间用逗号分隔,以分号结尾:
|
1 2 3 |
public enum Season { SPRING, SUMMER, AUTUMN, WINTER; } |
使用方式非常简单直接:
|
1 2 |
Season current = Season.SPRING; System.out.println(current); // 输出: SPRING |
每个枚举类都隐式继承自 java.lang.Enum,因此拥有以下实用方法 :
| 方法 | 说明 |
|---|---|
| values() | 返回包含所有枚举常量的数组 |
| valueOf(String name) | 根据名称字符串返回对应的枚举常量 |
| name() | 返回枚举常量的确切名称 |
| ordinal() | 返回枚举常量在声明中的位置索引(从 0 开始) |
| toString() | 返回枚举常量的名称(可重写) |
使用示例:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 遍历所有枚举值 for (Season s : Season.values()) { System.out.println(s + " 序号: " + s.ordinal()); } // 输出: // SPRING 序号: 0 // SUMMER 序号: 1 // AUTUMN 序号: 2 // WINTER 序号: 3 // 字符串转枚举 Season winter = Season.valueOf("WINTER"); System.out.println(winter); // 输出: WINTER |
注意:ordinal() 的值会随枚举常量声明顺序改变而变化,如果业务中需要固定的数值标识,建议自定义字段而非依赖 ordinal() 。
枚举与 switch 能让分支逻辑更加清晰 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Season season = Season.SUMMER; switch (season) { case SPRING: System.out.println("春暖花开,万物复苏"); break; case SUMMER: System.out.println("夏日炎炎,蝉鸣阵阵"); break; case AUTUMN: System.out.println("秋高气爽,硕果累累"); break; case WINTER: System.out.println("冬雪皑皑,银装素裹"); break; } |
枚举可以像普通类一样拥有成员变量、构造函数和自定义方法,这让每个枚举常量都能携带丰富的元数据 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public enum Color { RED("红色", "#FF0000"), GREEN("绿色", "#00FF00"), BLUE("蓝色", "#0000FF"); private final String chineseName; private final String hexCode; // 枚举构造函数必须是 private(可省略不写) Color(String chineseName, String hexCode) { this.chineseName = chineseName; this.hexCode = hexCode; } public String getChineseName() { return chineseName; } public String getHexCode() { return hexCode; } } // 使用 Color red = Color.RED; System.out.println(red.getChineseName() + " 的十六进制码是 " + red.getHexCode()); // 输出: 红色 的十六进制码是 #FF0000 |
枚举(enum)的构造函数必须是 private(如果不写,编译器也会默认它是 private),这背后的核心原因在于枚举的本质是单例模式的扩展。
定义一个枚举时,Java 编译器在底层会将其转换成一个继承自 java.lang.Enum 的 final 类。
|
1 |
enum Color { RED; } |
在编译后大致等同于:
|
1 2 3 4 5 6 7 |
public final class Color extends Enum<Color> { public static final Color RED = new Color("RED", 0); // 强制私有,仅供内部静态初始化调用 private Color(String name, int ordinal) { super(name, ordinal); } } |
枚举可以定义抽象方法,让每个枚举常量提供不同的实现。这本质上是策略模式的实现 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public enum Operation { ADD { @Override public int apply(int x, int y) { return x + y; } }, SUBTRACT { @Override public int apply(int x, int y) { return x - y; } }, MULTIPLY { @Override public int apply(int x, int y) { return x * y; } }, DIVIDE { @Override public int apply(int x, int y) { if (y == 0) throw new IllegalArgumentException("除数不能为零"); return x / y; } }; public abstract int apply(int x, int y); } // 使用 int result = Operation.ADD.apply(10, 5); // 15 System.out.println(Operation.MULTIPLY.apply(3, 4)); // 12 |
这种方式消除了冗长的 if-else 或 switch 分支,新增操作只需添加新的枚举常量并实现抽象方法,符合开闭原则 。
枚举可以实现接口,获得额外的抽象能力,同时保持类型安全 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public interface Describable { String getDescription(); } public enum Day implements Describable { MONDAY("周一综合征高发期"), FRIDAY("即将迎来周末的快乐"), SUNDAY("周末余额不足的忧伤"); private final String description; Day(String description) { this.description = description; } @Override public String getDescription() { return description; } } |
枚举是否可以通过反射,拿到实例对象呢?结论:不可以。枚举是 Java 中唯一能从根本上防御反射攻击的对象。
假设有一个简单的枚举:
|
1 2 3 |
public enum Singleton { INSTANCE; } |
如果尝试像对待普通类那样,通过反射获取构造器并创建实例:
|
1 2 3 |
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton newInstance = constructor.newInstance(); |
会得到一个 NoSuchMethodException。是不是因为没传参数?(因为枚举编译后构造函数默认有两个参数:String name 和 int ordinal)。
好,加上参数再试:
|
1 2 3 |
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); Singleton newInstance = constructor.newInstance("HACKER", 1); |
这时,得到一个更绝望的异常:
IllegalArgumentException: Cannot reflectively create enum objects
为什么反射失效了?
反射之所以失败,不是因为找不到方法,而是因为 Constructor.newInstance() 的源代码里写了死命令。
|
1 2 3 4 5 6 7 |
public T newInstance(Object ... initargs) { // ... 前面的一些检查 ... // 【关键点】这里直接判断了修饰符 if ((this.modifiers & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); // ... 后续逻辑 ... } |
Java 之父们在设计反射 API 时,就已经预料到了有人会尝试用反射破坏枚举的单例性。于是他们在 newInstance 方法里加了一个硬编码检查:只要发现这个类是 ENUM 类型,直接抛异常
在所有的单例模式实现方案中,枚举(Enum)被公认为是最安全的。
| 攻击方式 | 普通单例(DCL、静态内部类) | 枚举单例 |
|---|---|---|
| 反射攻击 | 可以通过 setAccessible(true) 破解 | 彻底免疫(JVM 底层拦截) |
| 序列化攻击 | 需要重写 readResolve() 才能保证唯一 | 原生支持(JVM 自动保证) |
| 代码简洁度 | 较复杂 | 极其简洁 |
答案:
|
1 2 3 4 5 6 7 8 9 |
// 错误示例 // public enum MyEnum extends SomeClass { } // 编译错误 // public class SubEnum extends MyEnum { } // 编译错误 // 正确示例 public enum MyEnum implements Runnable { INSTANCE; @Override public void run() { } } |
答案:不可以。枚举的构造函数只能是 private(可省略,默认就是 private)。
|
1 2 3 4 5 |
public enum Color { RED("红色"); // public Color(String name) {} // 编译错误 Color(String name) { } // 正确,默认 private } |
原理:枚举实例由 JVM 在类加载时自动创建,不允许外部通过 new 实例化。
答案:返回枚举常量在声明中的位置索引(从 0 开始)。不建议在 switch 中使用 ordinal(),直接用枚举常量即可。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public enum Status { PENDING, PROCESSING, COMPLETED; } // 错误用法 switch (status.ordinal()) { case 0: // PENDING case 1: // PROCESSING } // 正确用法 switch (status) { case PENDING: case PROCESSING: } |
|
1 2 3 4 5 6 7 8 9 10 11 |
public enum Singleton { INSTANCE; private Singleton() { System.out.println("构造函数执行"); } public static void main(String[] args) { Singleton s1 = Singleton.INSTANCE; Singleton s2 = Singleton.INSTANCE; System.out.println(s1 == s2); } } |
|
1 2 |
构造函数执行 true |
|
1 2 3 4 5 6 |
public enum Singleton { INSTANCE; private Singleton() { System.out.println("构造函数执行"); } } |
相当于
|
1 2 3 4 5 6 7 |
public final class Singleton { // 静态常量,类加载时初始化 public static final Singleton INSTANCE = new Singleton(); private Singleton() { System.out.println("构造函数执行"); } } |
所以 s1 == s2 结果为 true,因为它们指向的是内存中同一个地址。
|
1 2 3 4 5 6 7 8 |
public enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN; public static void main(String[] args) { System.out.println(Weekday.MON.ordinal()); System.out.println(Weekday.valueOf("FRI").ordinal()); System.out.println(Weekday.values()[2]); } } |
|
1 2 3 |
0 4 WED |
解析:
参考答案:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public enum Calculator { ADD("+") { @Override public double calculate(double a, double b) { return a + b; } }, SUBTRACT("-") { @Override public double calculate(double a, double b) { return a - b; } }, MULTIPLY("*") { @Override public double calculate(double a, double b) { return a * b; } }, DIVIDE("/") { @Override public double calculate(double a, double b) { if (b == 0) throw new ArithmeticException("除数不能为零"); return a / b; } }; private final String symbol; Calculator(String symbol) { this.symbol = symbol; } public abstract double calculate(double a, double b); public String getSymbol() { return symbol; } } // 测试 double result = Calculator.ADD.calculate(10, 5); // 15.0 |
|
1 2 3 4 5 6 7 |
public enum Singleton { INSTANCE;
public void doSomething() { // 业务方法 } } |
对比其他实现:
| 特性 | EnumSet | HashSet |
|---|---|---|
| 底层实现 | 位向量(bit vector) | HashMap |
| 元素类型 | 只能是枚举 | 任意对象 |
| 性能 | 极快(位运算) | 相对较慢 |
| 内存占用 | 极小 | 较大 |
| null 支持 | 不支持 | 支持 |
使用场景:需要存储枚举类型的集合时,优先使用 EnumSet。
|
1 2 3 4 5 |
// 示例:批量处理周末 EnumSet<Weekday> weekend = EnumSet.of(Weekday.SAT, Weekday.SUN); if (weekend.contains(today)) { System.out.println("今天是周末"); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public enum HttpStatus { OK(200, "成功"), NOT_FOUND(404, "未找到"), INTERNAL_ERROR(500, "服务器内部错误"); private final int code; private final String message; HttpStatus(int code, String message) { this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } // 根据 code 查找对应的枚举 public static HttpStatus fromCode(int code) { for (HttpStatus status : values()) { if (status.code == code) { return status; } } throw new IllegalArgumentException("未知状态码: " + code); } } |
|
1 2 |
// 反序列化本质调用 Enum.valueOf(MyEnum.class, name); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public enum ToggleSwitch { ON { @Override public ToggleSwitch toggle() { return OFF; } }, OFF { @Override public ToggleSwitch toggle() { return ON; } }; public abstract ToggleSwitch toggle(); } // 使用 ToggleSwitch current = ToggleSwitch.OFF; current = current.toggle(); // 切换到 ON current = current.toggle(); // 切换回 OFF |
题目:设计订单状态(待支付 → 已支付 → 已发货 → 已完成),每个状态只能按顺序流转,不能跳转或回退。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public enum OrderStatus { PENDING { @Override public OrderStatus next() { return PAID; } @Override public boolean canCancel() { return true; } }, PAID { @Override public OrderStatus next() { return SHIPPED; } @Override public boolean canCancel() { return false; } }, SHIPPED { @Override public OrderStatus next() { return COMPLETED; } @Override public boolean canCancel() { return false; } }, COMPLETED { @Override public OrderStatus next() { throw new IllegalStateException("已完成订单无法继续流转"); } @Override public boolean canCancel() { return false; } }; public abstract OrderStatus next(); public abstract boolean canCancel(); } |