枚举的常用姿势 (JAVA 为例) | LIXI.FUN
0%

枚举的常用姿势 (JAVA 为例)

几乎没啥用的示例

初见枚举,见过最多的实例无外乎有两个 ColorWeek,拿其中一个举个例子看看

1
2
3
4
5
public enum Color {
GREEN,
YELLOW,
RED
}

然后呢?怎么用?if/else ? switch ?

这样用的话,跟常量类唯一的区别就是限定了类型,让编译器能够发力限制使用以此枚举为入参的 api 参数类型,在一定程度上是一种进步,但实际使用完全没有变化。

初级进化版本

我们用的数据通常是要持久化的,持久化一般用数字或者某些字符串来代替其实际含义,这样来看上面那个枚举几乎没啥用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用了 lombok 的注解,简化代码
@Getter
@AllArgsConstructor
public enum Color {
GREEN(1),
YELLOW(2),
RED(3);

/**
* 持久化的 code 值
*/
private Integer code;
}

现在持久化的数据和枚举建立联系了,咋个用呢?貌似还是 switch 只是多了一个 getCode() 的方法能拿到对应持久化的值,对于要通过枚举值获得持久化的值,到这里就可以做到了。

可是我们从前端拿到的值是持久化的代表值,咋个转化成枚举呢?也就是从 code -> enum 这步该怎么搞呢?

再进化版本

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
// 使用了 lombok 的注解,简化代码
@Getter
@AllArgsConstructor
public enum Color {
GREEN(1),
YELLOW(2),
RED(3);

private Integer code;

/**
* 表驱动
* CODE: 枚举实例
*
* 不要直接暴露这个 Map 出去,提供一个方法出去,限制对 Map 的操作
*/
private static final Map<Integer, Color> CODE_TO_ENUM =
Arrays.stream(Color.values())
.collect(Collectors.toMap(Color::getCode, e -> e));

/**
* 通过 code 获取枚举实例
* 这里使用了 Optional 语义上表示如果 code 不存在(非法值),起一个提示作用
*
* @param code code
* @return 枚举实例
*/
public static Optional<Color> forCode(Integer code) {
return Optional.ofNullable(CODE_TO_ENUM.get(code));
}
}

到这里,这个枚举就比较能覆盖常见使用场景了,比如,前台选中某种颜色,传到后端

后端的 xxxService.process(Color color) 只接受 Color 类型,在这一层就限制了入参的范围,让 API 更健壮。

1
2
3
4
5
Optional<Color> colorOpt = Color.forCode(colorCode);
if (colorOpt.isPresent()) {
return xxxService.process(colorOpt.get());
}
return "something else";

终极进化

一般上面的版本就够用了,但是如果后面对于不同的 Color 要做除了 getCode() 以外的操作,还是逃不脱 if/elseswitch 这两种做法,那该怎么解决呢?答案是抽象方法。

是的,在枚举里是可以定义抽象方法的,各个实例去实现这个方法就可以了,代码可以参考 JDK8 的 java.util.concurrent.TimeUnit#toXxx() 方法

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Getter
@AllArgsConstructor
public enum Color {
GREEN(1) {
@Override
public void printColor() {
System.out.println("绿色");
}
},
YELLOW(2) {
@Override
public void printColor() {
System.out.println("黄色");

}
},
RED(3) {
@Override
public void printColor() {
System.out.println("红色");
}
};

private Integer code;

/**
* CODE: 枚举实例
*/
private static final Map<Integer, Color> CODE_TO_ENUM =
Arrays.stream(Color.values())
.collect(Collectors.toMap(Color::getCode, e -> e));

/**
* 通过 code 获取枚举实例
*
* 这里使用了 Optional 语义上表示如果 code 不存在(非法值),起一个提示作用
*
* @param code code
* @return 枚举实例
*/
public static Optional<Color> forCode(Integer code) {
return Optional.ofNullable(CODE_TO_ENUM.get(code));
}

/**
* 打印颜色
*/
public abstract void printColor();
}

先来看下原始的 switch 没有用抽象方法的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Optional<Color> colorOpt = forCode(code);
if (colorOpt.isPresent()) {
Color color = colorOpt.get();
switch (color) {
case GREEN:
System.out.println("绿色");
break;
case YELLOW:
System.out.println("黄色");
break;
case RED:
System.out.println("红色");
break;
default:
break;
}
}

使用抽象方法,如果要做 printColor() 的动作

1
2
3
4
5
Optional<Color> colorOpt = Color.forCode(colorCode);
if (colorOpt.isPresent()) {
// 这里体现了多态,根据具体实例调用具体方法
colorOpt.get().printColor();
}

可以看到,逻辑上是一样的,使用抽象方法的这种 主逻辑 更清晰了,做了什么动作一目了然,没有被淹没在各种不同情况的实现细节上面,如果开始只有一个动作用 switch 也没啥,但要是对于不同的枚举值有很多不同的动作,那每做一个动作都去 switch 就真的要人命了。

要注意一点的是,代码是一行也没有少写的,该有的逻辑都有,只有部分交给了程序自己去判断。

总结

  1. 内部以 code 对应枚举实例的 Map 为表,提供通过 code 获取实例的方法;
  2. 定义抽象方法,实例去实现抽象方法,利用多态做分支判断。

预留问题:现在的 code 是一个确定值,从 Map 往外拿值有就是有,没有就是没有,那如果 code 是范围值呢?就像考试成绩的 A,B,C,D 它们是区间分段的,怎么搞呢?

见下一篇 用枚举值表示范围

  • 枚举天然单例,可以放心大胆的用 == 进行比较。
  • 和枚举配套使用的 EnumSet/EnumMap 更加简单高效。
觉得有收获就鼓励下作者吧