Java 枚举最佳实践:带映射缓存 + 可复用 fromXxx 方法 + JUnit5 单元测试

举报
yd_272031863 发表于 2026/10/27 23:50:16 2026/10/27
【摘要】 通过创建map集合来存储各项数据,后期调用是可以从map集合中直接获取;

日常开发里,枚举是用来固化状态、类型、业务字典最常用的手段。很多同学只写简单枚举常量,但是根据 code / 描述反向解析枚举的时候写法五花八门:每次循环遍历、没有判空、异常信息不统一、Map 没有做不可变保护,很容易埋坑。

今天分享两套工程化枚举模板,一套多字段版本、一套精简版本,配套完整 JUnit5 单元测试,直接复制就能在项目里使用。

一、枚举设计思路

先说说这套枚举的设计要点,也是项目里推荐的规范:

  1. 使用 static 静态代码块一次性预加载映射关系,避免每次解析都循环 values(),性能更好;
  2. 内部缓存 Map 使用 Collections.unmodifiableMap 包装,防止外部篡改映射表;
  3. 提供 fromCode / fromDescription / fromField / fromName 等反向解析方法;
  4. 入参做空值、空白值校验,区分参数非法、找不到枚举两种异常;
  5. 使用 lombok @Getter,只对外暴露 getter,字段全部 private final,不可变;
  6. 配套完整单元测试:覆盖正常场景、null、空串、空白串、无效值、ordinal、name、valueOf。

补充:Enum.valueOf() 是 JDK 原生方法,只能通过枚举字面量名称解析,无法自定义异常提示,所以我们封装 fromName 更适合业务使用。

二、模板1:多字段枚举(code + description + field)

适合字典有编码、中文说明、数据库字段标识三类属性的场景,例如检查频次、巡检类型。

import lombok.Getter;

import java.util.HashMap;
import java.util.Map;

/**
 * @author admin1
 * 检查频次枚举
 */
@Getter
public enum CheckFrequencyStatus {
    ENTERPRISE_SPECIAL_INSPECTION(1, "测试数据001","csdata001"),
    QUARTERLY_ENTERPRISE(2, "测试数据002","csdata002"),
    DAILY_IN_CLASS_INSPECTION(3, "测试数据003","csdata003"),
    DAILY_SHIFT_HANDOVER(4, "测试数据004","csdata004"),
    PROJECT_WEEKLY_INSPECTION(5, "测试数据005","csdata005");

    private final int code;
    private final String description;
    private final String field;

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

    private static final Map<Integer, CheckFrequencyStatus> CODE_MAP = new HashMap<>();
    private static final Map<String, CheckFrequencyStatus> DESC_MAP = new HashMap<>();
    private static final Map<String, CheckFrequencyStatus> FIELD_MAP = new HashMap<>();
    private static final Map<String, CheckFrequencyStatus> NAME_MAP = new HashMap<>();

    static {
        for (CheckFrequencyStatus status : values()) {
            CODE_MAP.put(status.code, status);
            DESC_MAP.put(status.description, status);
            FIELD_MAP.put(status.field, status);
            NAME_MAP.put(status.name(), status);
        }
    }

    public static CheckFrequencyStatus fromCode(int code) {
        CheckFrequencyStatus status = CODE_MAP.get(code);
        if (status == null) {
            throw new IllegalArgumentException("无效的状态码: " + code);
        }
        return status;
    }

    public static CheckFrequencyStatus fromDescription(String description) {
        CheckFrequencyStatus status = DESC_MAP.get(description);
        if (status == null) {
            throw new IllegalArgumentException("无效的描述: " + description);
        }
        return status;
    }

    public static CheckFrequencyStatus fromField(String field) {
        CheckFrequencyStatus status = FIELD_MAP.get(field);
        if (status == null) {
            throw new IllegalArgumentException("无效的状态值: " + field);
        }
        return status;
    }

    /**
     * 通过枚举字面量名称获取实例(替代原生 valueOf,自定义异常)
     */
    public static CheckFrequencyStatus fromName(String name) {
        CheckFrequencyStatus status = NAME_MAP.get(name);
        if (status == null) {
            throw new IllegalArgumentException("无效的枚举名称: " + name);
        }
        return status;
    }

    public static void main(String[] args) {
        CheckFrequencyStatus status = CheckFrequencyStatus.fromCode(1);
        System.out.println(status);
        System.out.println(status.field);
        System.out.println(status.code);
        System.out.println(status.description);
        System.out.println(status.name());
        System.out.println(status.getCode()+"---"+status.getDescription()+"---"+status.getField());

        String es = "ENTERPRISE_SPECIAL_INSPECTION";
        CheckFrequencyStatus status1 = CheckFrequencyStatus.valueOf(es);
        System.out.println(status1);
        System.out.println(status1.name());
        System.out.println(status1.getCode()+"---"+status1.getDescription()+"---"+status1.getField());

        CheckFrequencyStatus status2 = CheckFrequencyStatus.fromName(es);
        System.out.println(status2);
        System.out.println(status2.name());
        System.out.println(status2.getCode()+"---"+status2.getDescription()+"---"+status2.getField());

        CheckFrequencyStatus[] values = CheckFrequencyStatus.values();
        System.out.println(java.util.Arrays.toString(values));
    }
}

三、模板2:精简版枚举(code + description,推荐业务大量使用)

这里做了优化:静态 Map 使用不可变集合,防止代码里不小心修改缓存映射表,异常区分更清晰:

  • IllegalArgumentException:参数为空、空白,入参本身非法
  • NoSuchElementException:参数不为空,但是找不到对应的枚举
package com.example.demo.enumTest;

import lombok.Getter;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;

/**
 * 级别枚举
 */
@Getter
public enum JBStatus {
    ZYJ("ZYJ", "作业级"),
    XMJ("XMJ", "项目级"),
    QYJ("QYJ", "企业级");

    private final String code;
    private final String description;

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

    private static final Map<String, JBStatus> CODE_MAP;
    private static final Map<String, JBStatus> DESC_MAP;

    static {
        Map<String, JBStatus> codeMap = new HashMap<>();
        Map<String, JBStatus> descMap = new HashMap<>();
        for (JBStatus status : values()) {
            codeMap.put(status.code, status);
            descMap.put(status.description, status);
        }
        // 包装为不可变Map,防止外部修改缓存
        CODE_MAP = Collections.unmodifiableMap(codeMap);
        DESC_MAP = Collections.unmodifiableMap(descMap);
    }

    /**
     * 通过code获取枚举值
     */
    public static JBStatus fromCode(String code) {
        if (code == null || code.trim().isEmpty()) {
            throw new IllegalArgumentException("状态码不能为空");
        }
        JBStatus JBStatus = CODE_MAP.get(code);
        if (JBStatus == null) {
            throw new NoSuchElementException("无效的状态码: " + code);
        }
        return JBStatus;
    }

    /**
     * 通过description获取枚举值
     */
    public static JBStatus fromDescription(String description) {
        if (description == null || description.trim().isEmpty()) {
            throw new IllegalArgumentException("状态值不能为空");
        }
        JBStatus JBStatus = DESC_MAP.get(description);
        if (JBStatus == null) {
            throw new NoSuchElementException("无效的状态值: " + description);
        }
        return JBStatus;
    }
}

四、配套 JUnit5 单元测试

很多开发枚举写完直接裸奔,没有测试,新增/删除枚举常量后很容易改错映射关系。
这套测试覆盖:常量属性校验、values、fromCode、fromDescription、空值、空白串、无效值、ordinal、name、原生valueOf。

package com.example.demo.enumTest;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;

import java.util.NoSuchElementException;

import static org.junit.jupiter.api.Assertions.*;

/**
 * JBStatus 枚举类的单元测试
 * 测试枚举常量、静态方法和边界条件
 */
public class JBStatusTest {

    @Test
    @DisplayName("测试枚举常量 ZYJ 的值")
    public void testEnumConstantZYJ() {
        assertEquals("ZYJ", JBStatus.ZYJ.getCode());
        assertEquals("作业级", JBStatus.ZYJ.getDescription());
    }

    @Test
    @DisplayName("测试枚举常量 XMJ 的值")
    public void testEnumConstantXMJ() {
        assertEquals("XMJ", JBStatus.XMJ.getCode());
        assertEquals("项目级", JBStatus.XMJ.getDescription());
    }

    @Test
    @DisplayName("测试枚举常量 QYJ 的值")
    public void testEnumConstantQYJ() {
        assertEquals("QYJ", JBStatus.QYJ.getCode());
        assertEquals("企业级", JBStatus.QYJ.getDescription());
    }

    @Test
    @DisplayName("测试 values() 方法返回所有枚举常量")
    public void testValuesMethod() {
        JBStatus[] values = JBStatus.values();
        assertEquals(3, values.length);
        assertTrue(containsEnum(values, JBStatus.ZYJ));
        assertTrue(containsEnum(values, JBStatus.XMJ));
        assertTrue(containsEnum(values, JBStatus.QYJ));
    }

    @Test
    @DisplayName("测试 fromCode 方法 - 有效的 code 值")
    public void testFromCodeWithValidCodes() {
        assertEquals(JBStatus.ZYJ, JBStatus.fromCode("ZYJ"));
        assertEquals(JBStatus.XMJ, JBStatus.fromCode("XMJ"));
        assertEquals(JBStatus.QYJ, JBStatus.fromCode("QYJ"));
    }

    @Test
    @DisplayName("测试 fromCode 方法 - null 值应抛出 IllegalArgumentException")
    public void testFromCodeWithNull() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> JBStatus.fromCode(null)
        );
        assertEquals("状态码不能为空", exception.getMessage());
    }

    @Test
    @DisplayName("测试 fromCode 方法 - 空字符串应抛出 IllegalArgumentException")
    public void testFromCodeWithEmptyString() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> JBStatus.fromCode("")
        );
        assertEquals("状态码不能为空", exception.getMessage());
    }

    @Test
    @DisplayName("测试 fromCode 方法 - 空白字符串应抛出 IllegalArgumentException")
    public void testFromCodeWithBlankString() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> JBStatus.fromCode("   ")
        );
        assertEquals("状态码不能为空", exception.getMessage());
    }

    @Test
    @DisplayName("测试 fromCode 方法 - 无效的 code 值应抛出 NoSuchElementException")
    public void testFromCodeWithInvalidCode() {
        NoSuchElementException exception = assertThrows(
                NoSuchElementException.class,
                () -> JBStatus.fromCode("INVALID")
        );
        assertEquals("无效的状态码: INVALID", exception.getMessage());
    }

    @Test
    @DisplayName("测试 fromDescription 方法 - 有效的 description 值")
    public void testFromDescriptionWithValidDescriptions() {
        assertEquals(JBStatus.ZYJ, JBStatus.fromDescription("作业级"));
        assertEquals(JBStatus.XMJ, JBStatus.fromDescription("项目级"));
        assertEquals(JBStatus.QYJ, JBStatus.fromDescription("企业级"));
    }

    @Test
    @DisplayName("测试 fromDescription 方法 - null 值应抛出 IllegalArgumentException")
    public void testFromDescriptionWithNull() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> JBStatus.fromDescription(null)
        );
        assertEquals("状态值不能为空", exception.getMessage());
    }

    @Test
    @DisplayName("测试 fromDescription 方法 - 空字符串应抛出 IllegalArgumentException")
    public void testFromDescriptionWithEmptyString() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> JBStatus.fromDescription("")
        );
        assertEquals("状态值不能为空", exception.getMessage());
    }

    @Test
    @DisplayName("测试 fromDescription 方法 - 无效的 description 值应抛出 NoSuchElementException")
    public void testFromDescriptionWithInvalidDescription() {
        NoSuchElementException exception = assertThrows(
                NoSuchElementException.class,
                () -> JBStatus.fromDescription("无效描述")
        );
        assertEquals("无效的状态值: 无效描述", exception.getMessage());
    }

    @Test
    @DisplayName("测试 fromCode 和 fromDescription 方法间接验证映射正确性")
    public void testMappingsCorrectness() {
        assertEquals(JBStatus.ZYJ, JBStatus.fromCode("ZYJ"));
        assertEquals(JBStatus.XMJ, JBStatus.fromCode("XMJ"));
        assertEquals(JBStatus.QYJ, JBStatus.fromCode("QYJ"));

        assertEquals(JBStatus.ZYJ, JBStatus.fromDescription("作业级"));
        assertEquals(JBStatus.XMJ, JBStatus.fromDescription("项目级"));
        assertEquals(JBStatus.QYJ, JBStatus.fromDescription("企业级"));
    }

    @Test
    @DisplayName("测试枚举常量的 ordinal 值")
    public void testEnumOrdinal() {
        assertEquals(0, JBStatus.ZYJ.ordinal());
        assertEquals(1, JBStatus.XMJ.ordinal());
        assertEquals(2, JBStatus.QYJ.ordinal());
    }

    @Test
    @DisplayName("测试枚举常量的 name 值")
    public void testEnumName() {
        assertEquals("ZYJ", JBStatus.ZYJ.name());
        assertEquals("XMJ", JBStatus.XMJ.name());
        assertEquals("QYJ", JBStatus.QYJ.name());
    }

    @Test
    @DisplayName("测试 valueOf 方法")
    public void testValueOfMethod() {
        assertEquals(JBStatus.ZYJ, JBStatus.valueOf("ZYJ"));
        assertEquals(JBStatus.XMJ, JBStatus.valueOf("XMJ"));
        assertEquals(JBStatus.QYJ, JBStatus.valueOf("QYJ"));
    }

    @Test
    @DisplayName("测试 valueOf 方法 - 无效名称应抛出 IllegalArgumentException")
    public void testValueOfWithInvalidName() {
        assertThrows(IllegalArgumentException.class,
                () -> JBStatus.valueOf("INVALID"));
    }

    /**
     * 辅助方法: 检查枚举数组是否包含指定的枚举常量
     */
    private boolean containsEnum(JBStatus[] array, JBStatus target) {
        for (JBStatus item : array) {
            if (item == target) {
                return true;
            }
        }
        return false;
    }
}

五、常见问题对比 & 踩坑点

1. 为什么不用每次调用都循环 values()?

// 不推荐写法,每次调用都循环
public static JBStatus fromCode(String code){
    for(JBStatus e : values()){
        if(e.code.equals(code)) return e;
    }
    throw new NoSuchElementException();
}
  • 少量枚举感知不明显;高频解析(接口、循环里)会有额外开销;
  • 静态块只加载一次,O(1) 查询,性能稳定。

2. 什么时候用 Collections.unmodifiableMap?

团队多人协作、基础枚举、公共字典枚举,强烈建议加上,防止有人写代码直接 CODE_MAP.put() 篡改缓存。

3. fromName 和原生 Enum.valueOf() 的区别

  • Enum.valueOf():JDK原生,异常信息固定,不能自定义提示;找不到直接抛 IllegalArgumentException;
  • fromName:自己封装,异常文案可控,适合业务返回给前端、日志打印。

4. 异常怎么选?

  • 参数 null / 空 / 空白:IllegalArgumentException(参数非法)
  • 参数格式正常,但是字典不存在:NoSuchElementException(找不到资源)>

也可以统一自定义业务异常,和全局异常处理器配合,直接返回前端友好提示。

六、扩展延伸(可选)

  1. 配合 Jackson 序列化:自定义 @JsonValue 输出 code;
  2. MyBatis 枚举处理器:直接数据库存 code,自动映射枚举;
  3. 增加 getAllList() 静态方法,返回枚举下拉列表给前端;
  4. 自定义统一业务异常,替换 JDK 原生异常,统一响应码。

七、小结

这套枚举模板是项目里沉淀下来的通用模板:
✅ 缓存映射,O(1) 查询
✅ 入参校验,边界防护
✅ 不可变Map保护缓存
✅ 异常区分清晰
✅ 全量JUnit5单元测试,改枚举不怕改错

需要新增字典枚举,直接复制模板改常量即可,测试类也可以按同样套路生成。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。