- 0
- 447 words
假设一个场景:你写了一个用户管理系统,用户有个「角色」字段。你可能会写:
String role = "admin"; // 管理员
String role2 = "user"; // 普通用户
这看起来挺合理对吧?直到某天你发现数据库里出现了一个"Admin",一个"ADMIN",还有一个"超级管理员"……血压上来了没有?
字符串的灵活性既是优点也是灾难——谁都可以拼一个字符串传进来,编译阶段根本检查不出来。这就要请出我们今天的主角:枚举(Enum)。
一、枚举是什么?一句话
枚举就是一组固定常量的类型安全集合。
简单说:你告诉 Java「系统里只有这三种角色:管理员、普通用户、游客」——Java 记住了,然后谁想传个"ADMIN"或者"超级管理员"进来,编译器直接报错,门都没有。
二、基本语法:enum 关键字
Java 5 引入了enum关键字,定义一个枚举就像定义一个类:
public enum Role {
ADMIN,
USER,
GUEST
}
就这么简单。三个枚举常量之间用逗号分隔,末尾可以加分号(也可以不加,但如果枚举体里还有别的东西就必须加)。
用的时候:
Role myRole = Role.ADMIN;
// 对比一下字符串版本的问题
// String role = "admin";
// 下面的代码编译期根本不知道你在玩火
// if ("ADMIN".equals(role)) // 狗头保命
// 枚举版本——安全
if (myRole == Role.ADMIN) {
System.out.println("欢迎管理员");
}
==可以直接比较枚举常量,因为枚举常量是单例的,每个常量只有一个实例。不用写.equals(),看着就清爽。
三、枚举比你想的强大多了
很多新手以为枚举就是个高级常量列表。错了。枚举其实是类——对,你没看错,enum在 Java 里面本质上是一个类,继承自 java.lang.Enum。这就意味着枚举可以有:
- 字段(成员变量)
- 构造方法
- 方法
- 甚至可以有自己的抽象方法
来,给Role加点料:
public enum Role {
ADMIN("管理员", 1),
USER("普通用户", 2),
GUEST("游客", 3);
private final String displayName;
private final int level;
// 构造方法必须是 private 的
Role(String displayName, int level) {
this.displayName = displayName;
this.level = level;
}
public String getDisplayName() {
return displayName;
}
public int getLevel() {
return level;
}
}
每个枚举常量后面括号里的值就是传给构造方法的参数:
Role admin = Role.ADMIN;
System.out.println(admin.getDisplayName()); // 管理员
System.out.println(admin.getLevel()); // 1
这就有意思了——枚举不再是干巴巴的常量名字,它跟具体的数据绑定了。你定义好什么角色什么权限等级,代码里随便取。
四、枚举常用的内置方法
所有枚举都继承了 java.lang.Enum,所以有几个现成的方法可以直接用:
| 方法 | 作用 |
|---|---|
name() | 返回枚举常量的名字(字符串),比如 "ADMIN" |
ordinal() | 返回枚举常量的序号,从 0 开始 |
values() | 返回该枚举类型的所有常量数组 |
valueOf(String) | 根据名字字符串返回对应的枚举常量 |
// values() 遍历所有枚举
for (Role role : Role.values()) {
System.out.println(role.name() + " -> " + role.getDisplayName());
}
// valueOf() 字符串转枚举
Role role = Role.valueOf("ADMIN");
System.out.println(role.getDisplayName()); // 管理员
// 小心!valueOf("admin") 会抛 IllegalArgumentException
// 因为大小写必须精确匹配
五、枚举中的抽象方法与行为定制
这招是我最喜欢枚举的地方——每个枚举常量可以有自己的行为。看个例子:
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;
}
};
// 抽象方法,每个枚举常量必须实现
public abstract int apply(int x, int y);
}
用起来:
int result = Operation.ADD.apply(3, 5); // 8
int result2 = Operation.MULTIPLY.apply(3, 5); // 15
是不是有点像匿名内部类?每个枚举常量在花括号里重写了抽象方法。以前你写计算器逻辑可能是if...else if...else一长串,现在直接Operation.ADD.apply(),既安全又优雅。
六、枚举实战:状态机
枚举最经典的应用场景就是状态机。拿订单举例:
public enum OrderStatus {
PENDING_PAYMENT {
@Override
public OrderStatus next() {
return PAID;
}
},
PAID {
@Override
public OrderStatus next() {
return SHIPPED;
}
},
SHIPPED {
@Override
public OrderStatus next() {
return DELIVERED;
}
},
DELIVERED {
@Override
public OrderStatus next() {
return COMPLETED;
}
},
COMPLETED {
@Override
public OrderStatus next() {
return COMPLETED; // 终态
}
};
public abstract OrderStatus next();
}
整个订单流转逻辑直接定义在枚举里面了,不用到处写switch-case,也不用担心某个状态转移忘了处理:
OrderStatus current = OrderStatus.PENDING_PAYMENT;
OrderStatus next = current.next(); // PAID
next = next.next(); // SHIPPED
这是不是比你在 Service 里写一大坨 if-else 清爽多了?
七、switch 与枚举的黄金搭档
Java 7 之前 switch 只能支持 int、char 这些,但枚举从 Java 5 开始就完美支持了:
public void handleRole(Role role) {
switch (role) { // 不用写 Role.ADMIN,编译器可以推断
case ADMIN:
System.out.println("全权限");
break;
case USER:
System.out.println("有限权限");
break;
case GUEST:
System.out.println("只读权限");
break;
}
}
注意两点:
case后面直接写常量名,不用写Role.ADMIN- 如果增加了新的枚举常量但没有加
case,编译器不会报错(这是 switch 的老毛病,只能靠你自己留个心眼加default兜底)
八、switch 表达式与枚举的现代玩法
从 Java 14 开始,switch 有了表达式的写法(预览特性在 Java 12 就引入了):
String permission = switch (role) {
case ADMIN -> "ALL";
case USER -> "READ_WRITE";
case GUEST -> "READ_ONLY";
};
// 也可以用 yield 返回值
String permission2 = switch (role) {
case ADMIN:
yield "ALL";
case USER:
yield "READ_WRITE";
case GUEST:
yield "READ_ONLY";
};
箭头语法配合枚举,简直强迫症福音。而且编译器会检查你的 switch 表达式是否覆盖了所有的枚举常量——漏一个就编译报错!
这一点很重要:switch表达式要求穷尽所有case。一旦你给枚举新增了一个常量但没加case,编译器直接撂挑子。
这是switch语句没有的安全保障。
九、枚举的地雷与小技巧
1. 枚举不能继承别的类,但可以实现接口
因为所有枚举都隐式继承了 java.lang.Enum,Java 不允许一个类同时继承两个父类。但枚举可以实现接口:
public interface Describable {
String getDescription();
}
public enum Role implements Describable {
ADMIN("超级管理员"),
USER("普通用户"),
GUEST("游客");
private final String description;
Role(String description) {
this.description = description;
}
@Override
public String getDescription() {
return description;
}
}
2. 用枚举消除魔法值
很多项目里充斥着这种代码:
if (status == 1) { ... } // 谁知道 1 是啥?
改成枚举:
public enum Status {
ACTIVE(1),
INACTIVE(0),
DELETED(-1);
private final int code;
Status(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static Status fromCode(int code) {
for (Status s : values()) {
if (s.code == code) return s;
}
throw new IllegalArgumentException("未知状态码: " + code);
}
}
数据库里存的 1/0/-1 读到 Java 里就变成 Status.ACTIVE,可读性和安全性都拉满了。
3. 枚举的 instance 字段和构造方法必须是 private
这是强制规定。枚举的构造方法只能是 private 的(不写默认也是 private),因为枚举常量是在类加载时创建的,不允许外部 new 一个枚举实例。
4. ordinal() 和 name() 慎用
ordinal() 返回枚举常量的声明顺序,但如果你在开发过程中调整了枚举的顺序(比如把 GUEST 从索引 2 移到了 1),那之前序列化的数据就全崩了。持久化枚举到数据库时,最好存自定义的 code 或 name,而不是 ordinal。
十、结语
枚举是个看起来简单、实际上能玩出花的东西。
- 基础用法:类型安全的常量集合,替换
public static final int - 进阶用法:带字段和构造方法,甚至每个常量有自己的行为
- 实战用法:状态机、消除魔法值、switch 表达式枚举穷尽
很多人一开始觉得枚举就是「花哨的常量定义」,直到被字符串 Bug 搞崩过一次之后,才明白枚举的香。你能早用就早用,尤其是那些只取有限值的场景——状态、角色、类型、配置项,能用枚举的地方绝不写字符串。
下一篇我们聊包装类(Wrapper Class)——Java 的 int 为什么有对象版本?自动装箱拆箱是怎么实现的?Integer 缓存池又是什么坑?这些内容不算多但坑不少,值得单开一篇好好讲。