枚举的高级用法——用枚举实现策略模式和状态机

引言

在 Java 中,枚举(enum)常常被当作"常量列表"使用,比如表示星期、状态、错误码等。

但其实,枚举远比常量强大:它可以有构造函数、字段、方法,甚至可以实现接口

这意味着我们可以将行为直接绑定到每个枚举常量上,从而消除大量的 if-elseswitch 语句,让代码更优雅、更安全、更易维护。

一、枚举基础回顾

在深入之前,先简单回顾一下枚举的基本特性:

java 复制代码
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}

枚举可以拥有字段、构造方法和普通方法:

java 复制代码
public enum Status {
    PENDING(0, "待处理"),
    PROCESSING(1, "处理中"),
    COMPLETED(2, "已完成"),
    CANCELLED(3, "已取消");

    private final int code;
    private final String description;

    Status(int code, String description) {
        this.code = code;
        this.description = description;
    }

    public int getCode() { return code; }
    public String getDescription() { return description; }

    public static Status fromCode(int code) {
        for (Status s : values()) {
            if (s.code == code) return s;
        }
        throw new IllegalArgumentException("未知状态码: " + code);
    }
}

这些基础用法大家都很熟悉,但枚举的潜力远不止于此。

二、枚举实现接口:为每个常量定制行为

假设我们有一个计算器,需要支持加、减、乘、除四种操作。传统做法是使用 if-elseswitch 根据操作符执行不同逻辑:

java 复制代码
public int calculate(int a, int b, String op) {
    switch (op) {
        case "ADD": return a + b;
        case "SUBTRACT": return a - b;
        case "MULTIPLY": return a * b;
        case "DIVIDE": return a / b;
        default: throw new IllegalArgumentException();
    }
}

这样写有几个问题:

  • 每次新增操作都要修改 switch 语句,容易遗漏。
  • 字符串参数容易写错,类型不安全。
  • 行为与调用方耦合,不易扩展。

用枚举重构:让枚举实现一个操作接口

java 复制代码
public interface Operation {
    int apply(int a, int b);
}

public enum Calculator implements Operation {
    ADD {
        @Override
        public int apply(int a, int b) {
            return a + b;
        }
    },
    SUBTRACT {
        @Override
        public int apply(int a, int b) {
            return a - b;
        }
    },
    MULTIPLY {
        @Override
        public int apply(int a, int b) {
            return a * b;
        }
    },
    DIVIDE {
        @Override
        public int apply(int a, int b) {
            return a / b;
        }
    };
}

使用:

java 复制代码
int result = Calculator.ADD.apply(5, 3); // 8

这样,每个枚举常量都自带了行为,调用时无需任何条件判断。新增操作只需要添加一个新的枚举常量,实现接口方法即可,符合开闭原则。

三、枚举与 Lambda 结合:更简洁的策略模式

上面的写法虽然清晰,但每个常量都要写一个匿名内部类,有点啰嗦。从 Java 8 开始,我们可以利用 Lambda 表达式来简化:

java 复制代码
public enum Calculator {
    ADD((a, b) -> a + b),
    SUBTRACT((a, b) -> a - b),
    MULTIPLY((a, b) -> a * b),
    DIVIDE((a, b) -> a / b);

    private final Operation operation;

    Calculator(Operation operation) {
        this.operation = operation;
    }

    public int apply(int a, int b) {
        return operation.apply(a, b);
    }

    @FunctionalInterface
    public interface Operation {
        int apply(int a, int b);
    }
}

这种方式将策略实现作为构造函数参数传入,代码更紧凑,而且每个枚举常量仍然持有自己的行为。

四、枚举单例 ------ 最安全的单例实现

《Effective Java》中极力推荐使用枚举实现单例,因为它简洁、线程安全、且能防止反射攻击和序列化问题。

java 复制代码
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        // 业务逻辑
    }
}

使用:

java 复制代码
Singleton.INSTANCE.doSomething();
  • 线程安全:枚举的实例创建由 JVM 保证,无需同步。
  • 防止反射:反射不能创建枚举实例。
  • 防止序列化破坏:枚举默认的序列化机制保证实例唯一。

这是实现单例的最佳实践

五、枚举实现有限状态机

枚举天然适合表示有限状态机中的状态。每个状态可以定义进入、退出、转移等行为。下面以订单状态为例:

java 复制代码
public enum OrderState {
    PENDING {
        @Override
        public OrderState next() {
            return PAID;
        }
    },
    PAID {
        @Override
        public OrderState next() {
            return SHIPPED;
        }
    },
    SHIPPED {
        @Override
        public OrderState next() {
            return DELIVERED;
        }
    },
    DELIVERED {
        @Override
        public OrderState next() {
            return DELIVERED; // 最终状态
        }
    },
    CANCELLED {
        @Override
        public OrderState next() {
            return CANCELLED; // 取消后不可变
        }
    };

    public abstract OrderState next();

    public boolean canTransitionTo(OrderState target) {
        // 可以定义更复杂的转移规则
        return this.next() == target;
    }
}

使用:

java 复制代码
OrderState state = OrderState.PENDING;
state = state.next(); // PAID

如果需要更复杂的状态机(如带事件、动作),可以在枚举中定义更多方法,甚至结合接口。

六、EnumMap 和 EnumSet:高性能、类型安全的集合

当我们需要以枚举为键进行映射时,EnumMap 是比 HashMap 更好的选择:

  • 内部使用数组存储,访问速度极快。
  • 无需计算哈希码,直接使用枚举的 ordinal 作为索引。
  • 类型安全,键只能是该枚举类型。
java 复制代码
Map<Day, String> schedule = new EnumMap<>(Day.class);
schedule.put(Day.MONDAY, "开会");
schedule.put(Day.FRIDAY, "总结");

类似地,EnumSet 用于存储枚举值的集合,内部也是位向量实现,非常高效:

java 复制代码
Set<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
Set<Day> workdays = EnumSet.range(Day.MONDAY, Day.FRIDAY);

在需要枚举集合或映射的场景,可以优先使用之。

七、注意事项

  1. 枚举的序列化:枚举默认序列化是安全的,但如果你在枚举中存储了外部状态(如数据库连接),序列化可能不会恢复这些状态,需要谨慎。
  2. 避免过度复杂:如果枚举的行为逻辑过于复杂,考虑将复杂逻辑拆分到普通类中,枚举只做策略分发。
  3. 枚举的 ordinal 依赖 :尽量不要依赖 ordinal() 作为业务逻辑,因为枚举常量的顺序改变会导致 ordinal 变化,容易引入 bug。使用自定义的 code 字段更可靠。

八、总结

  • 枚举 + 接口:为每个常量定制行为,消除条件判断。
  • 枚举 + Lambda:更简洁的策略实现。
  • 枚举单例:最优雅、最安全的单例模式。
  • 枚举 + 状态机:清晰表达状态流转。
  • EnumMap / EnumSet:性能与安全的双重保障。

九、代码在哪?

本篇涉及到的代码已上传至 GitHub:

https://github.com/iweidujiang/java-tricks-lab

欢迎 star & fork!

相关推荐
%Leo2 小时前
macos idea 插件搜索不到
java·intellij-idea
鱼鳞_2 小时前
Java学习笔记_Day19
java·笔记·学习
曹牧2 小时前
Java:驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立连接
java·开发语言·ssl
cch89182 小时前
PHP vs C++:10倍性能差距的编程语言对决
android·java·开发语言
房开民10 小时前
c++总结
java·开发语言·c++
好大哥呀10 小时前
C++ 多态
java·jvm·c++
毕设源码-赖学姐10 小时前
【开题答辩全过程】以 基于Java的医院器材管理系统的设计与实现为例,包含答辩的问题和答案
java·开发语言
float_com10 小时前
【java常用API】----- Arrays
java·开发语言
LuckyTHP11 小时前
迁移shibboleth java获取shibboleth用户信息
java·开发语言