第4章:枚举和注解(Enums and Annotations)
一、导读
1.1 本章学习目标
第4章探讨 Java 中两个强大但常被低估的特性:枚举类型和注解。枚举不仅仅是"int 常量的替代",它是一种完整的类,支持方法、字段、构造器和接口实现。注解则提供了元数据的标准化方式,使得框架和工具能够以声明式的方式工作。
本章的核心学习目标包括:
- 理解枚举类型的完整能力:掌握枚举的构造器、方法、字段,以及枚举的 switch 语句。
- 掌握枚举的实例域技术:学会用枚举的实例域实现常量特定行为。
- 理解注解的类型和使用:掌握标记注解、单值注解和多值注解的区别。
- 学会定义和使用自定义注解:理解元注解的作用,掌握注解处理器的基本原理。
- 理解枚举的序列化安全性:为什么枚举是单例的最佳实现方式。
1.2 为什么第四章如此重要
枚举和注解在 Java 生态中无处不在。Spring、Hibernate、JUnit 等主流框架都大量使用注解来简化配置。枚举则广泛应用于状态机、策略模式、常量定义等场景。正确使用这两个特性,能够显著提高代码的可读性、安全性和可维护性。
1.3 核心问题引导
枚举与 int 常量或 String 常量相比有什么优势?
如何实现枚举的常量特定行为(constant-specific behavior)?
注解有哪些类型?它们分别在什么场景下使用?
如何定义一个可以附加在方法上的自定义注解?
枚举的序列化安全性是如何保证的?
二、核心概念详解
4.1 用枚举类型代替 int 常量(Item 34)
4.1.1 int 常量模式的问题
// 反模式:int 枚举模式
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
// 问题:类型不安全,没有命名空间
int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN; // 编译通过,但毫无意义4.1.2 枚举类型的优势
// 正确做法:枚举类型
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
// 类型安全
Apple apple = Apple.FUJI;
// Orange orange = apple; // 编译错误!类型不匹配枚举的优势:
类型安全:不能将一种枚举类型赋给另一种。
命名空间:每种枚举类型有自己的命名空间。
可添加数据和方法:枚举可以包含字段和方法。
可实现接口:枚举可以实现一个或多个接口。
不可变性:枚举实例是 final 的。
序列化安全:枚举的序列化机制保证了实例的唯一性。
4.2 用实例域代替序数(Item 35)
// 反模式:依赖 ordinal()
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal() + 1; // 脆弱!如果顺序改变就会出错
}
}
// 正确做法:使用实例域
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), NONET(9), DECTET(10);
private final int numberOfMusicians;
Ensemble(int numberOfMusicians) {
this.numberOfMusicians = numberOfMusicians;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}4.3 用 EnumSet 代替位域(Item 36)
// 反模式:位域
public class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
public static final int STYLE_STRIKETHROUGH = 1 << 3;
public void applyStyles(int styles) {
// styles 是位域
}
}
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
// 正确做法:EnumSet
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
public void applyStyles(Set<Style> styles) {
// styles 是类型安全的集合
}
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));EnumSet 的优势:
类型安全:编译时检查类型。
可读性:代码意图清晰。
性能:内部使用位向量实现,性能与位域相当。
丰富的操作:支持 complementOf、union、intersection 等集合操作。
4.4 用 EnumMap 代替序数索引(Item 37)
// 反模式:用序数索引数组
public enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
class Plant {
String name;
LifeCycle lifeCycle;
}
// 按生命周期分组
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
// 正确做法:EnumMap
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (LifeCycle lc : LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
// 使用 Stream
Map<LifeCycle, List<Plant>> grouped = garden.stream()
.collect(Collectors.groupingBy(
p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class),
Collectors.toList()
));4.5 用接口模拟可多重继承的枚举(Item 38)
// 定义操作接口
public interface Operation {
double apply(double x, double y);
}
// 枚举实现接口
public enum BasicOperation implements Operation {
PLUS("+") {
@Override
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
@Override
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
@Override
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
@Override
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() { return symbol; }
}
// 使用接口类型,可以接受任何实现 Operation 的枚举
public static <T extends Enum<T> & Operation> void test(
Class<T> opSet, double x, double y) {
for (Operation op : opSet.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}4.6 考虑用接口模拟枚举的可扩展性(Item 39)
枚举类型的一个限制是不能被扩展。但通过接口,可以实现可扩展的枚举:
// 基础接口
public interface Operation {
double apply(double x, double y);
String symbol();
}
// 基础枚举实现
public enum BasicOperation implements Operation {
PLUS("+") { public double apply(double x, double y) { return x + y; } },
MINUS("-") { public double apply(double x, double y) { return x - y; } };
private final String symbol;
BasicOperation(String symbol) { this.symbol = symbol; }
public String symbol() { return symbol; }
}
// 扩展枚举
public enum ExtendedOperation implements Operation {
EXP("^") { public double apply(double x, double y) { return Math.pow(x, y); } },
REMAINDER("%") { public double apply(double x, double y) { return x % y; } };
private final String symbol;
ExtendedOperation(String symbol) { this.symbol = symbol; }
public String symbol() { return symbol; }
}
// 使用
public static void main(String[] args) {
double x = 2.0, y = 3.0;
test(ExtendedOperation.class, x, y);
}
public static <T extends Enum<T> & Operation> void test(
Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op.symbol(), y, op.apply(x, y));
}
}三、实战案例
5.1 状态机实战:订单状态管理
public enum OrderStatus {
CREATED {
@Override
public OrderStatus nextAction(Order order) {
order.confirm();
return CONFIRMED;
}
},
CONFIRMED {
@Override
public OrderStatus nextAction(Order order) {
order.ship();
return SHIPPED;
}
},
SHIPPED {
@Override
public OrderStatus nextAction(Order order) {
order.deliver();
return DELIVERED;
}
},
DELIVERED {
@Override
public OrderStatus nextAction(Order order) {
if (order.isReturnRequested()) {
return RETURNED;
}
return COMPLETED;
}
},
RETURNED {
@Override
public OrderStatus nextAction(Order order) {
order.refund();
return COMPLETED;
}
},
COMPLETED {
@Override
public OrderStatus nextAction(Order order) {
throw new IllegalStateException("Order is already completed");
}
};
public abstract OrderStatus nextAction(Order order);
public boolean isTerminal() {
return this == COMPLETED;
}
}
public class Order {
private String id;
private OrderStatus status;
private boolean returnRequested;
public Order(String id) {
this.id = id;
this.status = OrderStatus.CREATED;
}
public void advance() {
if (status.isTerminal()) {
throw new IllegalStateException("Cannot advance completed order");
}
status = status.nextAction(this);
}
public void confirm() { System.out.println("Order " + id + " confirmed"); }
public void ship() { System.out.println("Order " + id + " shipped"); }
public void deliver() { System.out.println("Order " + id + " delivered"); }
public void refund() { System.out.println("Order " + id + " refunded"); }
public boolean isReturnRequested() { return returnRequested; }
}5.2 注解实战:自定义测试框架
import java.lang.annotation.*;
import java.lang.reflect.*;
// 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
Class<? extends Throwable> expected() default None.class;
enum None { INSTANCE }
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Timeout {
int value() default 1000; // 毫秒
}
// 测试类
public class CalculatorTest {
@Test
public void testAdd() {
assert 2 + 2 == 4;
}
@Test(expected = ArithmeticException.class)
public void testDivideByZero() {
int result = 1 / 0;
}
@Test
@Timeout(500)
public void testSlowOperation() throws InterruptedException {
Thread.sleep(100);
}
}
// 测试运行器
public class TestRunner {
public static void main(String[] args) throws Exception {
Class<?> testClass = CalculatorTest.class;
Object instance = testClass.getDeclaredConstructor().newInstance();
int passed = 0, failed = 0;
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(Test.class)) {
Test test = method.getAnnotation(Test.class);
Class<? extends Throwable> expected = test.expected();
try {
method.invoke(instance);
if (expected != Test.None.class) {
System.out.println("FAIL: " + method.getName() + " - expected " + expected.getSimpleName());
failed++;
} else {
System.out.println("PASS: " + method.getName());
passed++;
}
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (expected != Test.None.class && expected.isInstance(cause)) {
System.out.println("PASS: " + method.getName() + " (expected " + expected.getSimpleName() + ")");
passed++;
} else {
System.out.println("FAIL: " + method.getName() + " - " + cause);
failed++;
}
} catch (Exception e) {
System.out.println("FAIL: " + method.getName() + " - " + e);
failed++;
}
}
}
System.out.println("\nResults: " + passed + " passed, " + failed + " failed");
}
}四、本章小结
第4章系统讲解了枚举和注解的核心知识:
枚举类型是完整的类,支持构造器、方法、字段和接口实现。
实例域优于 ordinal(),避免依赖枚举常量的顺序。
EnumSet 和 EnumMap 是位域和序数索引的类型安全替代。
接口可以模拟枚举的可扩展性。
注解提供了标准化的元数据机制,支持编译时和运行时处理。
枚举和注解是 Java 中两个被低估但极其强大的特性。正确使用它们,能够显著提高代码的类型安全性、可读性和可维护性。
五、思考与练习
思考题
枚举的序列化安全性是如何保证的?为什么普通类做不到这一点?
EnumSet 的内部实现原理是什么?为什么它的性能与位域相当?
如何通过接口实现可扩展的枚举?这种方式的局限性是什么?
注解的 RetentionPolicy 有哪些类型?分别在什么场景下使用?
如何编写一个注解处理器来处理自定义注解?
编程练习
实现一个计算器:使用枚举定义基本运算(加减乘除),支持通过接口扩展高级运算(幂、对数、三角函数)。
实现一个状态机框架:使用枚举定义状态和转换规则,支持状态进入和退出时的回调。
实现一个自定义注解:定义 @Validate 注解,用于标记需要校验的方法,编写注解处理器在运行时执行校验逻辑。