Java 枚举:有效应用

Java 枚举本身的介绍本文就不多说,相关资料很多,本文将讲述些 Java 枚举使用的技巧和注意事项。

枚举属性

Java 枚举除了可以定义常量以外,还可以定义属性。比如很常见的一个星期枚举

1
2
3
public enum WeekEnum {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}

以英文定义了从周一到周日的7个枚举常量值,通常情况下大部分序列化工具都会以常量名的字符串形式对其进行序列化。WeekEnum.MONDAY 的常量名字符就是字符串"MONDAY",可以通过 MONDAY.name() 函数访问。

这样定义的枚举除了英文常量外,没有附带其它信息。但是,我们可以通过在枚举内自定义属性,为枚举添加有意义的附加信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum WeekEnum {
MONDAY(1, "周一"),
TUESDAY(2, "周二"),
WEDNESDAY(3, "周三"),
THURSDAY(4, "周四"),
FRIDAY(5, "周五"),
SATURDAY(6, "周六"),
SUNDAY(7, "周日");

WeekEnum(int value, String label) {
this.value = value;
this.label = label;
}

public final int value;
public final String label;
}

通过添加一个 int 类型的 value 属性来说明枚举的值(序列化的字段),一个 String 类型的 label 属性来说明枚举的描述。如果需要,你可以添加多个描述属性,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public enum WeekEnum {
MONDAY(1, "周一", "工作日"),
// ....
SUNDAY(7, "周日", "休息日");
WeekEnum(int value, String label, String desc) {
this.value = value;
this.label = label;
this.desc = desc;
}

public final int value;
public final String label;
public final String desc;
}
```

## 枚举常量名 `name`

枚举常量名:`WeekEnum.MONDAY` 它的常量名就是 `name`,字符串表现形式为 `"MONDAY"`。属性 `name` 是 `private`的,可以通过 `name()` 方法访问。

这里的一个 **坑** 在于,`name()` 方法带有 `final` 修饰符,意味着你不能重写它,因为 Java 的机制,你可以定义一个 `name` 属性字段来覆盖枚举默认的 `name`,比如这样:

```java
public enum SexEnum {
MALE("MAN"),
FEMALE("WOMAN");
SexEnum(String name) {
this.name = name;
}
private String name;
}

这时候可能会给你造成一个假象,以为 name() 方法将返回你定义的的 this.name 的值,如:MALE.name() 将返回 "MAN",但实际上它将返回 "MALE"。因为枚举常量名(name())是不能重写的,同时在 Enum 里定义的 nameprivate的,它将始终带初始化为常量名的字符串表形形式。

记住,这一点很重要!因为大多数序列化工具都调用枚举的 .name() 方法获得字符串来进行序列化,如:Jackson、Dubbo……

所以为了避免不必要的理解岐义,建议自定义属性时不要使用 name 作为属性名字。

枚举属性命名

  1. 用于序列化的枚举属性只应使用 intString 类型
  2. 用于序列化的枚举属性建议命名为 value
  3. 用于描述枚举的字段建议命名为:labeldesctitle
  4. 自定义枚举属性不要命名为 name,这会覆盖枚举自身的常量名。

枚举序列化

以 Jackson 为例,若恰好你的枚举使用常量名进行序列化已经满足业务要求,那你不需要作任何设置。

若你想以某个自定义属性的值来作为序列化,那你在属性名上添加 @JsonValue 注解即可,在序列化时 Jackson 将会使用它的值来进行序列化。比如前面定义的 WeekEnum#value 属性,在添加了 @JsonValue 注解后,Jackson 将会把 WeekEnum.MONDAY 序列化成数字 1

不建议使用枚举索引来序列化

Jackson 对枚举提供了 4 种序列化方式:

  1. 全局配置调用 .name() 序列化为字符串(默认)
  2. 全局配置调用 .toString() 序列化为字符串
  3. 全局配置调用 .ordinal() 序列化为数字
  4. 通过注解自定义序列化,如:@JsonValue 指定序列化的属性

通过 .ordinal() 获得枚举常量索引进行序列化是最不推荐的方式,因为它是按照常量在枚举里定义的顺序从上到下从 0 开始计数的,在代码重构及演进过程中,很可能不小心改变了顺序,这样会造成序列化值的错乱并失去兼容性!

枚举反序列化

当你使用 Jackson 的全局配置调用 .name().toString().ordinal() 序列化枚举,再想将其反序列化时会一切正常。但是,使用 @JsonValue 注解序列化,在反序列化时,Jackson 仍将使用默认配置的方式进行反序列化,也就是说并不会将使用 @JsonValue 注解指定的属性用来进行反序列化。若要通过用 @JsonValue 指定的属性来进行反序列化,需要自定义一个 Deserializer ,并通过 Jackson 的模块机制注册它。这很简单:

虽然嘴上说很简单,但代码还是不少的。这里就不贴代码了,完整代码可访问:

有关 Jackson 更多内容可以阅读我的另一篇文章:《JSON 之 Jackson》

总结

  1. 自定义属性时建议用于序列化的属性命名为 value,并只使用 intString 作为数据类型。枚举作为常量,使用 Integer 包装类型是没有意义的
  2. 用于描述的字段建议使用 labeltitledesc 等字段,可使用任意类型
  3. Jackson 的 @JsonValue 注解默认只在序列化时起效,反序列化需要自定义 Jackson Deserializer
  4. 不用使用枚举的 .ordinal() 来进行序列化!
  5. 请确保枚举常量名稳定!通常大部分序列化框架都通过 .name() 方法获取枚举常量名的字符串表现形式并用于序列化/反序列化。比如:Dubbo

对于数据库访问层怎样使用 Java 枚举,比如:Mybatis,之后的文章我将对其进行介绍。你可以先收藏我的一个系列文章 《CRUDer 的自我修养:PostgreSQL、JDBC、MyBatis、R2DBC》