×

假设一个场景:你写了一个用户管理系统,用户有个「角色」字段。你可能会写:

String role = "admin";  // 管理员
String role2 = "user";  // 普通用户

这看起来挺合理对吧?直到某天你发现数据库里出现了一个"Admin",一个"ADMIN",还有一个"超级管理员"……血压上来了没有?

字符串的灵活性既是优点也是灾难——谁都可以拼一个字符串传进来,编译阶段根本检查不出来。这就要请出我们今天的主角:枚举(Enum)

一、枚举是什么?一句话

枚举就是一组固定常量的类型安全集合。

简单说:你告诉 Java「系统里只有这三种角色:管理员、普通用户、游客」——Java 记住了,然后谁想传个"ADMIN"或者"超级管理员"进来,编译器直接报错,门都没有。

二、基本语法:enum 关键字

Java 5 引入了enum关键字,定义一个枚举就像定义一个类:

public enum Role {
    ADMIN,
    USER,
    GUEST
}

就这么简单。三个枚举常量之间用逗号分隔,末尾可以加分号(也可以不加,但如果枚举体里还有别的东西就必须加)。

用的时候:

Role myRole = Role.ADMIN;

// 对比一下字符串版本的问题
// String role = "admin";
// 下面的代码编译期根本不知道你在玩火
// if ("ADMIN".equals(role)) // 狗头保命

// 枚举版本——安全
if (myRole == Role.ADMIN) {
    System.out.println("欢迎管理员");
}

== 可以直接比较枚举常量,因为枚举常量是单例的,每个常量只有一个实例。不用写.equals(),看着就清爽。

三、枚举比你想的强大多了

很多新手以为枚举就是个高级常量列表。错了。枚举其实是——对,你没看错,enum在 Java 里面本质上是一个类,继承自 java.lang.Enum。这就意味着枚举可以有:

  • 字段(成员变量)
  • 构造方法
  • 方法
  • 甚至可以有自己的抽象方法

来,给Role加点料:

public enum Role {
    ADMIN("管理员", 1),
    USER("普通用户", 2),
    GUEST("游客", 3);

    private final String displayName;
    private final int level;

    // 构造方法必须是 private 的
    Role(String displayName, int level) {
        this.displayName = displayName;
        this.level = level;
    }

    public String getDisplayName() {
        return displayName;
    }

    public int getLevel() {
        return level;
    }
}

每个枚举常量后面括号里的值就是传给构造方法的参数:

Role admin = Role.ADMIN;
System.out.println(admin.getDisplayName());  // 管理员
System.out.println(admin.getLevel());         // 1

这就有意思了——枚举不再是干巴巴的常量名字,它跟具体的数据绑定了。你定义好什么角色什么权限等级,代码里随便取。

四、枚举常用的内置方法

所有枚举都继承了 java.lang.Enum,所以有几个现成的方法可以直接用:

方法作用
name()返回枚举常量的名字(字符串),比如 "ADMIN"
ordinal()返回枚举常量的序号,从 0 开始
values()返回该枚举类型的所有常量数组
valueOf(String)根据名字字符串返回对应的枚举常量
// values() 遍历所有枚举
for (Role role : Role.values()) {
    System.out.println(role.name() + " -> " + role.getDisplayName());
}

// valueOf() 字符串转枚举
Role role = Role.valueOf("ADMIN");
System.out.println(role.getDisplayName());  // 管理员

// 小心!valueOf("admin") 会抛 IllegalArgumentException
// 因为大小写必须精确匹配

五、枚举中的抽象方法与行为定制

这招是我最喜欢枚举的地方——每个枚举常量可以有自己的行为。看个例子:

public enum Operation {
    ADD {
        @Override
        public int apply(int x, int y) {
            return x + y;
        }
    },
    SUBTRACT {
        @Override
        public int apply(int x, int y) {
            return x - y;
        }
    },
    MULTIPLY {
        @Override
        public int apply(int x, int y) {
            return x * y;
        }
    };

    // 抽象方法,每个枚举常量必须实现
    public abstract int apply(int x, int y);
}

用起来:

int result = Operation.ADD.apply(3, 5);       // 8
int result2 = Operation.MULTIPLY.apply(3, 5); // 15

是不是有点像匿名内部类?每个枚举常量在花括号里重写了抽象方法。以前你写计算器逻辑可能是if...else if...else一长串,现在直接Operation.ADD.apply(),既安全又优雅。

六、枚举实战:状态机

枚举最经典的应用场景就是状态机。拿订单举例:

public enum OrderStatus {
    PENDING_PAYMENT {
        @Override
        public OrderStatus next() {
            return PAID;
        }
    },
    PAID {
        @Override
        public OrderStatus next() {
            return SHIPPED;
        }
    },
    SHIPPED {
        @Override
        public OrderStatus next() {
            return DELIVERED;
        }
    },
    DELIVERED {
        @Override
        public OrderStatus next() {
            return COMPLETED;
        }
    },
    COMPLETED {
        @Override
        public OrderStatus next() {
            return COMPLETED; // 终态
        }
    };

    public abstract OrderStatus next();
}

整个订单流转逻辑直接定义在枚举里面了,不用到处写switch-case,也不用担心某个状态转移忘了处理:

OrderStatus current = OrderStatus.PENDING_PAYMENT;
OrderStatus next = current.next();  // PAID
next = next.next();                   // SHIPPED

这是不是比你在 Service 里写一大坨 if-else 清爽多了?

七、switch 与枚举的黄金搭档

Java 7 之前 switch 只能支持 int、char 这些,但枚举从 Java 5 开始就完美支持了:

public void handleRole(Role role) {
    switch (role) {  // 不用写 Role.ADMIN,编译器可以推断
        case ADMIN:
            System.out.println("全权限");
            break;
        case USER:
            System.out.println("有限权限");
            break;
        case GUEST:
            System.out.println("只读权限");
            break;
    }
}

注意两点:

  • case 后面直接写常量名,不用写 Role.ADMIN
  • 如果增加了新的枚举常量但没有加 case,编译器不会报错(这是 switch 的老毛病,只能靠你自己留个心眼加 default 兜底)

八、switch 表达式与枚举的现代玩法

从 Java 14 开始,switch 有了表达式的写法(预览特性在 Java 12 就引入了):

String permission = switch (role) {
    case ADMIN -> "ALL";
    case USER -> "READ_WRITE";
    case GUEST -> "READ_ONLY";
};

// 也可以用 yield 返回值
String permission2 = switch (role) {
    case ADMIN:
        yield "ALL";
    case USER:
        yield "READ_WRITE";
    case GUEST:
        yield "READ_ONLY";
};

箭头语法配合枚举,简直强迫症福音。而且编译器会检查你的 switch 表达式是否覆盖了所有的枚举常量——漏一个就编译报错!

这一点很重要:switch表达式要求穷尽所有case。一旦你给枚举新增了一个常量但没加case,编译器直接撂挑子。
这是switch语句没有的安全保障。

九、枚举的地雷与小技巧

1. 枚举不能继承别的类,但可以实现接口

因为所有枚举都隐式继承了 java.lang.Enum,Java 不允许一个类同时继承两个父类。但枚举可以实现接口:

public interface Describable {
    String getDescription();
}

public enum Role implements Describable {
    ADMIN("超级管理员"),
    USER("普通用户"),
    GUEST("游客");

    private final String description;

    Role(String description) {
        this.description = description;
    }

    @Override
    public String getDescription() {
        return description;
    }
}

2. 用枚举消除魔法值

很多项目里充斥着这种代码:

if (status == 1) { ... }  // 谁知道 1 是啥?

改成枚举:

public enum Status {
    ACTIVE(1),
    INACTIVE(0),
    DELETED(-1);

    private final int code;

    Status(int code) {
        this.code = code;
    }

    public int getCode() {
        return code;
    }

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

数据库里存的 1/0/-1 读到 Java 里就变成 Status.ACTIVE,可读性和安全性都拉满了。

3. 枚举的 instance 字段和构造方法必须是 private

这是强制规定。枚举的构造方法只能是 private 的(不写默认也是 private),因为枚举常量是在类加载时创建的,不允许外部 new 一个枚举实例。

4. ordinal() 和 name() 慎用

ordinal() 返回枚举常量的声明顺序,但如果你在开发过程中调整了枚举的顺序(比如把 GUEST 从索引 2 移到了 1),那之前序列化的数据就全崩了。持久化枚举到数据库时,最好存自定义的 code 或 name,而不是 ordinal。

十、结语

枚举是个看起来简单、实际上能玩出花的东西。

  • 基础用法:类型安全的常量集合,替换 public static final int
  • 进阶用法:带字段和构造方法,甚至每个常量有自己的行为
  • 实战用法:状态机、消除魔法值、switch 表达式枚举穷尽

很多人一开始觉得枚举就是「花哨的常量定义」,直到被字符串 Bug 搞崩过一次之后,才明白枚举的香。你能早用就早用,尤其是那些只取有限值的场景——状态、角色、类型、配置项,能用枚举的地方绝不写字符串。


下一篇我们聊包装类(Wrapper Class)——Java 的 int 为什么有对象版本?自动装箱拆箱是怎么实现的?Integer 缓存池又是什么坑?这些内容不算多但坑不少,值得单开一篇好好讲。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章

20. 接口基础:语法、特性与抽象类对比

抽象类讲完了。你知道父类可以只声明方法签名,...

读出全部

19. 多态的应用:参数多态化、工厂模式与支付系统实战

上一篇我们拆解了多态的底层机制——动态绑定、...

读出全部

18. 多态的本质:动态绑定、向上转型与向下转型

抽象类学完了,你现在应该知道 「父类只声明、...

读出全部