Fork me on GitHub
0%

Java 中枚举让代码更优雅

对于枚举,不知道你对它的使用了解多少,说实话在刚开始使用它的时候,我对它的印象仅次于帮助我们在代码中定义一个对象的字段特定的几种状态,便于我们理解代码中该字段有哪些值,又分别代表哪些含义。

也就是说当一个对象的某个字段只有特定的几种含义的时候,与其直接通过 Integer 类型来定义该字段,然后采取 1,2,3,4 每个数字来代表其中一种含义的方式来标记该字段的含义,倒不如使用枚举来标记该字段所代表的含义,这样能够更好地理解,同时也将该字段的含义包裹在你所定义的枚举类中。

这样如果这个字段跟你的业务关联非常紧密的时候,就不会出现一个工程项目中到处都充满了这个字段和数字类型的比较的代码,而是只和你所定义的枚举进行比较就可以了。这样做的好处一个是提高了代码的可阅读性,同时也避免了今后如果关于这个字段的业务逻辑需要更新而满工程的去找使用到这个字段的地方,有时还可能会落下一两个地方没有找到导致新的业务出现问题。

举个例子,非常典型的订单表中订单状态字段,假设在某电商业务中有一张订单表,其中有一个字段 status 表示该订单的状态,我们姑且先为订单的状态定义三种状态好了,分别是待支付,已支付,已退款,然后在数据库中分别对应于 0,1,2 这三个数字存储,而在代码中我们先用 Integer 类型来定义该字段,然后加上注释写上 0 表示待支付,1 表示已支付,2 表示已退款。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Order {

private Long id;

private Long userId;

private Integer amount;

/// private OrderStatus status;

/**
* 0: 待支付,1:已支付,2:已退款.
*/
private Integer status;
}

然后在业务中只有待支付(0)的订单才能发起付款操作,已支付(1)的订单才能发起发货和退款操作,已退款(2)的订单便不能再发起任何操作了。

1
2
3
4
5
6
7
8
private boolean initiatePay(Order order){
if(order.getStatus() != 0){
// 订单状态异常
}
//发起付款
........
return true;
}

像上面这种代码加上了注释还稍微能明白代码的业务逻辑,不然的话就需要去猜数字 0 表示什么状态了。但其实最好的是我们通过看代码就能明白代码所表达出来的含义,这也是我们经常说到的代码中的命名最好是能够规范一些,尽量做到见名知意,这一点在业务代码中显得尤为重要。

下面我们再看通过枚举的方式定义订单状态字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum OrderStatus {

TO_PAY(0),

PAID(1),

REFUNDED(2);

private int code;

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

相应的业务逻辑代码:

1
2
3
4
5
6
7
8
private boolean initiatePay(Order order){
if(order.getStatus() != OrderStatus.TO_PAY){
// 订单状态异常
}
//发起付款
........
return true;
}

两者相比是不是觉得下面使用枚举来定义的方式更优雅一些而且从枚举的名字一下就能明白这段代码的含义。但是你以为枚举的作用就只是这吗,其实枚举能够帮我们进一步的书写出更优雅的代码,下面就一个一个来介绍下枚举还有哪些特性。

枚举的比较

关于枚举的比较,其实枚举类型可确保 JVM 中仅存在所定义的常量的一个实例,所以我们可以直接使用 == 来比较两个枚举是否相等,而不需要通过 equals 方法来比较,当然用 equals 比较也可以,但是使用 equals 方法稍不注意可能会引起 NPE,下面我们举一个简单的例子来说明。

在敏捷开发中,我们常用 Issue 对象来表示 Epic(史诗),Story(故事),Task(任务),Bug(缺陷),这几个类型在 Issue 对象中会用一个类型字段来表示,然后与之对应的会有一个状态字段来标记当前 Issue 所处的状态,这里我们暂且使用 Task 类型的 Issue,然后通过该 Issue 中的状态字段来举例说明。不是很熟悉敏捷开发流程的建议可以了解下,应该算是目前比较流行的开发流程,快速迭代,将一个需求通过讲故事的方式进行开发迭代,将故事拆成很多的任务分配到开发人员来完成。具体可能需要经历过这样的过程才能更好地理解,这里就先暂时把 Issue 当成一个任务好了,任务肯定有它的状态,这里只是简单的定义成三种状态,TODO,DOING,DONE,分别代表待完成,正在进行中,已完成。

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
@Data
@ToString
@NoArgsConstructor
public class Issue {

private Long id;

private IssueType type;

private StatusEnum status;

public Issue(StatusEnum status){
this.status = status;
}

@Getter
public enum StatusEnum {

TODO(0),

DOING(1),

DONE(2);

private int code;

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

上面代码中就定义了一个 Issue 类,为了简单只定义了三个字段,id(住建),type(issue 类型),status(issue 状态),其中 type 和 status 都是枚举,接下来以 status 枚举字段来测试。

1
2
3
4
5
6
7
8
9
10
11
Issue issue = new Issue();
issue.setId(1L);
// compare result false
System.out.println(issue.getStatus() == Issue.StatusEnum.DONE);

// throw NPE
/// System.out.println(issue.getStatus().equals(Issue.StatusEnum.DONE));
issue.setStatus(Issue.StatusEnum.DONE);

// compare result true
System.out.println(issue.getStatus() == Issue.StatusEnum.DONE);

上面测试代码中从测试结果可以看到就算 issue 对象的 status 字段为 null,使用 == 比较是否相等也不会产生异常,比较的结果为 false,而如果使用一个 null 去调用 equals 方法这时就会产生 NPE,然后我们给 status 字段赋值 DONE,再使用 == 比较,比较的结果为 true,这就是推荐使用 == 比较两个枚举值是否相等的其中一个原因,可以避免运行时产生 NPE。

虽然使用 == 比较就算为 null 也没关系,但是值得注意的是当在 switch 中还是要注意 NPE 问题的,比如下面的代码当 issue 中的 status 值为 null 时就可能产生 NPE:

1
2
3
4
5
6
7
8
9
10
11
12
public int getCode(){
switch (getStatus()){
case TODO:
return 0;
case DOING:
return 1;
case DONE:
return 2;
default:
return -1;
}
}

我们还可以从另外一点看出 Java 不希望我们使用 equals 来比较,首先 enum 类型继承自 java.lang.Enum 类,如果我们是通过调用 equals 方法来比较的话,调用的 equals 方法也就是 java.lang.Enum 类的中 equals 方法,而该类中 equals 方法被 final 修饰,并且方法实现就是使用 == 来比较的,我们都知道被 final 修饰的方法不能被重写,意味着我们自己定义的枚举类的 equals 方法都是使用 == 比较的,从这里可以看出可能是因为枚举所定义的枚举常量实例在 JVM 中是唯一的,所以 Java 就不希望我们重写 equals 方法来比较,直接使用 == 比较即可。

1
2
3
4
// java.lang.Enum.java 类中 equals 方法的内部实现就是 ==,比较的是引用的地址是否相等,同时 final 修饰
public final boolean equals(Object other) {
return this==other;
}

推荐使用 == 比较的另外一个原因是可以在编译时就帮你进行了合法性检查,当使用两个不同类型的枚举进行比较是在编译时就报错提示不能使用 == 比较两个不同的枚举类型,但是使用 equals 却是可以的,但使用 equals 比较两个不同枚举类型是没有意义的,因为比较的结果总是为 false。

1
2
3
4
// compare result false
System.out.println(issue.getStatus().equals(TestFlowEnum.TODO));
// compile error
/// System.out.println(issue.getStatus() == TestFlowEnum.TODO);

在枚举中定义方法

文章开始我们提到使用枚举来定义字段,将枚举中的含义包裹在枚举类中,这里我们可以通过在枚举中定义一些方法来使一些基本的业务判断代码也包裹在枚举类中。请看下面的示例代码:

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
public enum StatusEnum {

TODO(0),

DOING(1){
@Override
public int getInnerCode() {
return this.getCode();
}

@Override
public boolean isDoing() {
return true;
}
},

DONE(2){
@Override
public int getInnerCode() {
return this.getCode();
}

@Override
public boolean isDone() {
return true;
}
};

public int getInnerCode(){
// default return -1
return 0;
}

public boolean isDoing(){
// default return false
return false;
}

public boolean isDone(){
// default return false
return false;
}

private int code;

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

可以看到上面的枚举类中定义了三个方法,方法带有默认实现,同时在各个枚举常量实例中分别重写对应的方法,覆盖默认的实现。这样在外围代码中都可以不需要使用 == 来作为业务判断了。

1
2
3
4
5
6
7
8
9
10
issue.setStatus(Issue.StatusEnum.DOING);
// 定义方法前的业务判断
if(issue.getStatus == Issue.StatusEnum.DOING){
// execute
}
// 定义方法后的业务判断
if(issue.isDoing()){
// execute
}
System.out.println(issue.isDone());

这样一来代码看起来是不是更优雅一点了呢。

EnumSet 和 EnumMap 的使用

关于枚举还有两个集合类也会用到,分别是 EnumSet 和 EnumMap,这两个集合类在什么时候会用到呢。继续拿上面的 Issue 类来说,这时如果我们需要筛选出当前还未完成的任务,我们可以在 Issue 类中加一个方法,方法的实现如下:

1
2
3
4
5
private static EnumSet<StatusEnum> undoneSet = EnumSet.of(StatusEnum.TODO, StatusEnum.DOING);

public static List<Issue> getUndoneIssue(List<Issue> issues){
return issues.stream().filter(issue -> undoneSet.contains(issue.getStatus())).collect(Collectors.toList());
}

测试代码:

1
2
3
4
5
6
7
8
9
// EnumSet
Issue issue1 = new Issue(Issue.StatusEnum.TODO);
Issue issue2 = new Issue(Issue.StatusEnum.DOING);
Issue issue3 = new Issue(Issue.StatusEnum.DOING);
Issue issue4 = new Issue(Issue.StatusEnum.DONE);
List<Issue> issues = Arrays.asList(issue1, issue2, issue3, issue4);
List<Issue> undoneIssues = Issue.getUndoneIssue(issues);
System.out.println(undoneIssues.size());
undoneIssues.forEach(item -> System.out.println(item.toString()));

从测试代码中可以看到通过引入 EnumSet,可以做到很便利的筛选符合条件的状态的 Issue。

而当我们需要对 Issue 进行按状态分类时,就可以利用到 EnumMap 集合,在 Issue 类中加上 groupByStatus 方法:

1
2
3
public static EnumMap<StatusEnum, List<Issue>> groupByStatus(List<Issue> issues){
return issues.stream().collect(Collectors.groupingBy(Issue::getStatus, () -> new EnumMap<>(StatusEnum.class), Collectors.toList()));
}

测试代码:

1
2
3
// todo status issue size is 1, doing status issue size is 2, done status issue size is 1
EnumMap<Issue.StatusEnum, List<Issue>> map = Issue.groupByStatus(issues);
System.out.println(String.format("todo status issue size is %s, doing status issue size is %s, done status issue size is %s", map.get(Issue.StatusEnum.TODO).size(), map.get(Issue.StatusEnum.DOING).size(), map.get(Issue.StatusEnum.DONE).size()));

同样从测试代码中可以看到通过使用 EnumMap 集合我们可以很快的做到按状态分类。

上面是我们在日常使用枚举的过程中可以稍微进行改进的地方,自我感觉合理的使用好枚举会让我们的代码看起来更加优雅,同时也会减少出错的可能性。当然这些只是关于枚举的一些小技巧,但其实通过枚举我们还可以实现简单的设计模式,接下来就来看看通过枚举怎么实现单例模式和策略模式。

完整测试代码地址:Java basic sample

参考:https://www.baeldung.com/a-guide-to-java-enums

 wechat
扫描上面图中二维码关注微信公众号