一组固定的命名值
有些值只能是一个已知小列表中的一项:一周中的某一天、一副牌中的花色、订单可能处于的状态。你可以用 int 编码(0 表示待处理,1 表示已发货)或字符串("PENDING"、"SHIPPED")来表示它们,但没有什么能阻止调用者传入 7 或 "PNEDING"。enum 堵上了这个漏洞:类型本身只允许你声明过的值。
Day 是一个全新的类型。Day 类型的变量只能存放这七个常量中的一个(或者 null),别的都不行。编译器会拒绝 Day d = "WEDNESDAY"; 或 Day d = 2;。这种保证正是枚举的意义所在。
每个常量(Day.MONDAY 等)都是一个只创建一次的共享实例。因为 MONDAY 永远只有一个,所以比较枚举时用 == 而不是 equals:它们就是同一个对象。
对枚举使用 switch
枚举和 switch 天生是一对。在 switch 内部,你甚至不需要在每个常量前加上类型名:
在 switch 内部,写 case SATURDAY,而不是 case Day.SATURDAY:Java 会推断类型。和对字符串或 int 使用 switch 相比,最大的好处是:如果以后新增一个常量,IDE 和穷尽性检查可以标出每一个忘了处理它的 switch。
遍历所有常量
每个枚举都会自动获得一个静态的 values() 方法,按声明顺序返回所有常量,此外还有 ordinal(),给出每个常量从 0 开始的位置:
name() 以 String 形式给出常量的标识符,values() 非常适合用来构建菜单或遍历选项。要注意一点:不要把 ordinal() 保存到任何持久化的地方。如果以后有人调整了常量的顺序,这些数字就会变化,你保存的数据也就坏了。把 ordinal() 当作实现细节,而不是稳定的 ID。
枚举是真正的类:字段和方法
这正是 Java 枚举超越其他语言中“命名整数”式枚举的地方。每个常量都可以通过构造方法携带数据,枚举还可以拥有方法。先列出常量,给每个常量传入构造方法参数,然后在一个分号之后声明字段、构造方法和方法。
注意它的结构:常量放在最前面,以分号结束,然后是类的其余部分。构造方法隐式为 private:你永远不能写 new Planet(...),因为唯一允许的实例就是顶部声明的那些。final 字段让每个常量都不可变,这正是共享单例所需要的。
字符串与枚举互相转换
要把字符串转换成对应的常量,使用自动生成的 valueOf:
valueOf 要求与常量名完全一致,包括大小写,没有匹配时抛出 IllegalArgumentException。这是常见的陷阱:"shipped" 匹配不了 SHIPPED。解析用户输入或外部数据时,先统一大小写(input.toUpperCase()),并用 try/catch 包住这次调用,否则遇到第一个错误值程序就会崩溃。
每个常量各自的行为
有时每个常量需要的不只是存放不同的数据,还要有不同的行为。你可以给枚举定义一个抽象方法,让每个常量在一个小小的类体中提供自己的实现:
这种写法有时叫作“常量特定方法”模式。它用直接附加在每个常量上的行为,取代了针对常量的冗长 switch:新增一种运算时,编译器会强制你定义它的 apply,所以你不会漏掉任何一种情况。
什么时候使用枚举
只要一个值天然属于一组小的、固定的、在编译时就已知的集合,就用枚举:
- 工作流中的状态(
PENDING、SHIPPED、DELIVERED)。 - 类别、模式或类型(
READ、WRITE、EXECUTE)。 - 任何你想定义一组
public static final int常量的地方:枚举给你同样的命名值,还免费附带类型安全、可读的toString和switch支持。
不要把枚举用于开放式的或运行时才确定的集合(用户名、从数据库读取的国家列表)。枚举在编译时就固定下来;如果集合在程序运行期间会变化,你需要的是普通的集合类。
下一步:泛型
你已经看到 values() 返回 Planet[],前面的页面也用过 List<Shape>,这种 <...> 语法就是泛型,它是 Java 让你只写一个类或方法、就能用于多种类型,同时保持完整类型安全的方式。接下来我们会讲解 List<String> 和 Map<K, V> 到底是怎么工作的,以及如何编写你自己的泛型类型。
常见问题
Java 中的枚举是什么?
enum 是一种特殊的类型,它的值只能是一组固定的命名常量之一,比如 Day.MONDAY 或 Status.ACTIVE。每个常量都是枚举的一个预先创建好的唯一实例。枚举提供类型安全:接收 Day 的方法只能收到真正的星期值,绝不会收到拼错的字符串或超出范围的 int。
Java 枚举可以有字段和方法吗?
可以。枚举是一个完整的类。你可以给每个常量传入构造方法参数,把它们存进 private final 字段,再添加使用这些字段的方法。例如 MERCURY(3.3e23, 2.4e6) 把质量和半径传给枚举的构造方法,surfaceGravity() 方法就可以根据这些字段进行计算。
Java 枚举的 values() 和 valueOf() 有什么区别?
values() 按声明顺序返回枚举所有常量组成的数组,很适合遍历每个选项。valueOf("NAME") 则相反:它查找名字与字符串完全一致的常量,如果没有匹配的就抛出 IllegalArgumentException。两者都由编译器自动生成。