4Effective Java(第3版)

枚举和注解

枚举与注解的最佳用法

枚举类型注解定义元注解常量特定类

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 常量模式的问题

java
// 反模式: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 枚举类型的优势

java
// 正确做法:枚举类型
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)

java
// 反模式:依赖 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)

java
// 反模式:位域
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)

java
// 反模式:用序数索引数组
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)

java
// 定义操作接口
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)

枚举类型的一个限制是不能被扩展。但通过接口,可以实现可扩展的枚举:

java
// 基础接口
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 状态机实战:订单状态管理

java
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 注解实战:自定义测试框架

java
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 注解,用于标记需要校验的方法,编写注解处理器在运行时执行校验逻辑。


4/10枚举和注
0%