为什么需要枚举?
在 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
枚举与 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
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);
}
}
|
- 类是 final 的,且构造函数是 private 的,所以除了在类内部定义的常量外,没有任何办法产生新的对象。而且枚举不能被继承,但是可以通过实现接口来扩展行为。
- RED是 public static final的。静态变量是在类加载(Class Loading)阶段进行初始化的,而 JVM 保证了一个类在整个生命周期中只会被加载一次。
抽象方法与策略模式
枚举可以定义抽象方法,让每个枚举常量提供不同的实现。这本质上是策略模式的实现 :
|
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 自动保证) |
| 代码简洁度 |
较复杂 |
极其简洁 |
笔试面试题
枚举能否被继承?能否继承其他类??
答案:
- 枚举不能被继承(隐式 final)
- 枚举不能显式继承其他类(因为已经隐式继承 java.lang.Enum)
- 但枚举可以实现接口
|
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() { }
}
|
枚举的构造函数可以用 public 修饰吗??
答案:不可以。枚举的构造函数只能是 private(可省略,默认就是 private)。
|
1
2
3
4
5
|
public enum Color {
RED("红色");
// public Color(String name) {} // 编译错误
Color(String name) { } // 正确,默认 private
}
|
原理:枚举实例由 JVM 在类加载时自动创建,不允许外部通过 new 实例化。
ordinal() 方法返回什么?能否在 switch 中使用?
答案:返回枚举常量在声明中的位置索引(从 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
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("构造函数执行");
}
}
|
- 枚举实例是静态常量属性(Static Final),它在类加载时初始化构造函数只执行一次
- 执行 Singleton s1 = Singleton.INSTANCE; 时,触发了上述加载过程,对象创建。
- 执行 Singleton s2 = Singleton.INSTANCE; 时,由于 INSTANCE 是静态常量,它已经存在于内存中了。此时 s2 只是直接指向了那个已经创建好的对象。
所以 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]);
}
}
|
解析:
- MON.ordinal() 为 0(第一个)
- FRI.ordinal() 为 4(第五个,索引从 0 开始)
- values()[2] 返回索引 2 的常量,即 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
|
为什么说枚举实现单例是最佳实践?
- 线程安全:JVM 保证枚举实例的初始化是线程安全的
- 防止反射攻击:Constructor.newInstance() 对枚举会抛出 IllegalArgumentException
- 防止序列化破坏:枚举的序列化机制保证反序列化后仍是同一实例
- 代码简洁:两行代码搞定
|
1
2
3
4
5
6
7
|
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
|
对比其他实现:
- 懒汉式需要 volatile + 双重检查锁
- 静态内部类虽然好,但仍无法防止反射攻击
- 只有枚举能天然防御所有破坏方式
EnumSet 和 HashSet 的区别?什么场景用 EnumSet?
| 特性 |
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);
}
}
|
枚举在 JVM 层面是如何保证线程安全的?
- 枚举的实例化在类加载的初始化阶段完成
- JVM 规范保证 <clinit> 方法在多线程环境下是线程安全的(会加锁)
- 枚举实例的创建由 JVM 调用,外部无法通过 new 创建新实例
枚举序列化为什么能防止创建新实例?
- 枚举类默认实现了 Serializable 接口
- 序列化时只保存枚举的 name 属性,不保存成员变量
- 反序列化时通过 Enum.valueOf(Class, String) 获取已有的枚举实例
- 因此反序列化后获得的是同一个实例,而非新对象
|
1
2
|
// 反序列化本质调用
Enum.valueOf(MyEnum.class, name);
|
values() 方法来自哪里??
- values() 不是继承自 Enum 类的方法
- 它是编译器自动生成的静态方法
- 在编译时被添加到枚举类中
用枚举实现一个简单的状态机(开关状态)?
|
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();
}
|