枚举与注解
枚举和注解都是 Java 语言提供的“表达语义”的机制。
枚举用于表达一组有限、固定、类型安全的值;注解用于给代码附加元数据,让编译器、框架或运行时工具能够读取这些信息并做进一步处理。
在业务开发中,枚举和注解非常常见:
- 订单状态、支付渠道、用户类型通常适合用枚举。
@Override、@Deprecated、@Transactional、@Controller、@Test都是注解。- Spring、MyBatis、JUnit 等框架大量依赖注解和反射。
# 1. 枚举的基本用法
枚举使用 enum 定义。
public enum OrderStatus {
CREATED,
PAID,
CANCELED,
FINISHED
}
使用:
OrderStatus status = OrderStatus.CREATED;
if (status == OrderStatus.CREATED) {
System.out.println("订单已创建");
}
枚举适合表达有限集合:
订单状态
CREATED -> PAID -> FINISHED
└-> CANCELED
比起字符串常量,枚举有几个优势:
| 对比项 | 字符串常量 | 枚举 |
|---|---|---|
| 类型安全 | 弱,任意字符串都能传 | 强,只能传枚举值 |
| 编译期检查 | 较弱 | 强 |
| 可读性 | 一般 | 好 |
| 可承载行为 | 不方便 | 可以定义字段和方法 |
| 重构支持 | 弱 | IDE 支持好 |
# 2. 枚举的本质
枚举本质上是继承 java.lang.Enum 的特殊类。每个枚举常量都是该枚举类的一个固定实例。
public enum PayType {
WECHAT,
ALIPAY
}
可以概念化理解为:
public final class PayType extends Enum<PayType> {
public static final PayType WECHAT = new PayType("WECHAT", 0);
public static final PayType ALIPAY = new PayType("ALIPAY", 1);
private PayType(String name, int ordinal) {
super(name, ordinal);
}
}
结构:
PayType 类
├─ WECHAT 唯一实例
└─ ALIPAY 唯一实例
因此枚举构造器必须是私有的,不能在外部创建新枚举对象。
# 3. 枚举字段和方法
枚举可以有字段、构造器和方法。
public enum PayType {
WECHAT("微信支付"),
ALIPAY("支付宝支付");
private final String description;
PayType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
使用:
System.out.println(PayType.WECHAT.getDescription());
业务枚举建议不要只写枚举名,可以加业务 code:
public enum OrderStatus {
CREATED("CREATED", "已创建"),
PAID("PAID", "已支付");
private final String code;
private final String text;
OrderStatus(String code, String text) {
this.code = code;
this.text = text;
}
}
这样便于和数据库、前端、外部接口交互。
# 4. 枚举中的抽象方法
枚举常量可以有自己的行为。
public enum DiscountType {
NONE {
BigDecimal calculate(BigDecimal price) {
return price;
}
},
HALF {
BigDecimal calculate(BigDecimal price) {
return price.multiply(new BigDecimal("0.5"));
}
};
abstract BigDecimal calculate(BigDecimal price);
}
这种写法适合策略数量固定且逻辑较简单的场景。
结构:
DiscountType
├─ NONE -> 原价
└─ HALF -> 半价
如果策略复杂、依赖很多外部服务,更推荐使用策略类和依赖注入,而不是把复杂业务塞进枚举。
# 5. 枚举常用方法
| 方法 | 作用 |
|---|---|
name() | 返回枚举常量名 |
ordinal() | 返回声明顺序,从 0 开始 |
values() | 返回所有枚举常量 |
valueOf(String) | 按名称获取枚举 |
compareTo | 按声明顺序比较 |
示例:
for (OrderStatus status : OrderStatus.values()) {
System.out.println(status.name());
}
valueOf 找不到会抛异常:
OrderStatus.valueOf("UNKNOWN"); // IllegalArgumentException
开发中不要把 ordinal() 存入数据库。枚举顺序一旦调整,历史数据含义就会错乱。应存稳定的 code。
# 6. switch 与枚举
枚举适合配合 switch:
switch (status) {
case CREATED -> System.out.println("待支付");
case PAID -> System.out.println("已支付");
case CANCELED -> System.out.println("已取消");
case FINISHED -> System.out.println("已完成");
}
如果枚举新增值,编译器和 IDE 往往能提示 switch 未覆盖分支。
# 7. EnumSet 与 EnumMap
EnumSet 是专门为枚举优化的 Set。
EnumSet<OrderStatus> activeStatuses =
EnumSet.of(OrderStatus.CREATED, OrderStatus.PAID);
EnumMap 是专门以枚举为 key 的 Map。
EnumMap<OrderStatus, String> textMap = new EnumMap<>(OrderStatus.class);
textMap.put(OrderStatus.CREATED, "待支付");
优势:
| 类型 | 优势 |
|---|---|
EnumSet | 内部可用位向量表示,紧凑高效 |
EnumMap | 以枚举 ordinal 定位,访问高效 |
当 key 或元素明确是枚举时,优先考虑 EnumSet 和 EnumMap。
# 8. 注解的基本概念
注解是附加在代码元素上的元数据。
@Override
public String toString() {
return "User";
}
注解本身不直接改变代码逻辑。真正产生效果的是读取注解的工具:
源码上的注解
│
├─ 编译器读取:检查语法、生成警告、生成代码
├─ 注解处理器读取:编译期生成文件
└─ 框架运行时读取:反射扫描并执行逻辑
例如:
@Override被编译器读取。@Deprecated被编译器和 IDE 读取。@Transactional被 Spring 运行时读取。@Test被 JUnit 读取。
# 9. 自定义注解
定义注解:
public @interface AuditLog {
String value();
boolean enabled() default true;
}
使用:
@AuditLog(value = "创建订单")
public void createOrder() {
}
注解元素规则:
- 可以是基本类型、
String、Class、枚举、注解或这些类型的数组。 - 可以有默认值。
- 不能有普通方法体。
- 元素名为
value且只设置它时,使用时可省略value =。
@AuditLog("创建订单")
public void createOrder() {
}
# 10. 元注解
元注解是修饰注解的注解。
# 10.1 Retention
@Retention 定义注解保留到哪个阶段。
| 策略 | 保留阶段 | 典型场景 |
|---|---|---|
SOURCE | 源码 | 编译检查,如 @Override |
CLASS | 字节码 | 编译期工具、字节码增强 |
RUNTIME | 运行期 | 反射读取,如 Spring 注解 |
如果框架运行时需要读取注解,必须使用 RUNTIME。
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
}
# 10.2 Target
@Target 定义注解可以放在哪里。
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AuditLog {
}
常见目标:
| 目标 | 说明 |
|---|---|
TYPE | 类、接口、枚举 |
METHOD | 方法 |
FIELD | 字段 |
PARAMETER | 参数 |
CONSTRUCTOR | 构造器 |
ANNOTATION_TYPE | 注解类型 |
# 10.3 Documented 与 Inherited
@Documented 表示生成 Javadoc 时包含该注解。
@Inherited 表示类上的注解可以被子类继承。注意它只对类继承有效,对方法、字段无效。
# 11. 运行时读取注解
Method method = OrderService.class.getDeclaredMethod("createOrder");
AuditLog auditLog = method.getAnnotation(AuditLog.class);
if (auditLog != null && auditLog.enabled()) {
System.out.println(auditLog.value());
}
读取流程:
Class / Method / Field
│
▼
getAnnotation
│
▼
获取注解代理对象
│
▼
读取注解元素值
Spring AOP、接口文档工具、参数校验框架都大量使用这种模式。
# 12. 编译期注解处理
有些注解不是运行时读取,而是在编译期由注解处理器处理。
典型例子:
- Lombok 根据注解生成 getter、setter、构造器等代码。
- MapStruct 根据注解生成对象转换实现类。
- 一些配置处理器生成元数据。
流程:
Java 源码
│ javac
▼
注解处理器扫描注解
│
├─ 生成新源码
├─ 生成配置文件
└─ 输出编译警告或错误
编译期处理的好处是运行时开销低,但会增加编译链路复杂度。
# 13. 枚举与注解的开发建议
# 13.1 枚举建议
- 业务枚举增加稳定 code,不依赖
ordinal()。 - 对外接口传输 code,不直接暴露 Java 枚举名。
- 枚举逻辑简单时可以封装方法,复杂时使用策略类。
- 需要枚举集合时优先
EnumSet。 - 需要枚举作为 key 时优先
EnumMap。
# 13.2 注解建议
- 运行时读取的注解必须设置
@Retention(RUNTIME)。 - 注解只表达元数据,复杂逻辑放到处理器、切面或框架组件中。
- 自定义注解命名要表达明确语义。
- 不要滥用注解隐藏业务流程,影响可读性。
# Tips 快问快答
Q:枚举可以被继承吗?
A:不能。枚举本质上是特殊的 final 类,且已经继承 Enum。
Q:枚举可以实现接口吗? A:可以。枚举不能继承其他类,但可以实现接口。
Q:枚举构造器为什么不能 public? A:枚举常量必须固定,不能让外部随意创建实例。
Q:数据库里能存 ordinal() 吗?
A:不推荐。枚举声明顺序变化会导致历史数据含义错乱。
Q:枚举比较用 == 还是 equals?
A:通常用 ==,因为枚举常量是单例。
Q:注解本身会执行逻辑吗? A:不会。注解只是元数据,需要编译器、处理器或框架读取后才产生效果。
Q:为什么自定义注解运行时读不到?
A:很可能没有设置 @Retention(RetentionPolicy.RUNTIME)。
Q:@Inherited 能让方法注解被子类继承吗?
A:不能。它只对类级别注解继承有效。
Q:Lombok 注解是运行时生效吗? A:不是。Lombok 主要在编译期修改或生成代码。
Q:什么时候不要用注解? A:当注解让业务流程变得隐式、难搜索、难调试时,应考虑显式代码或配置。