使用枚举实现状态转换限制

本质上,JVM 并不支持语法糖,语法糖只存在于编译期。

当编译器将 .java 源文件编译成 .class 字节码文件时,会进行解语法糖的操作,来还原最原始的基础语法结构。

编程语言中几乎都会包含语法糖,当然 JAVA 也不例外。

JAVA 中的语法糖包含条件编译、断言、switch 支持 String 与枚举、可变参数、自动装箱/拆箱、枚举、内部类、泛型擦除、增强for循环、lambda表达式、try-with-resources等等。


枚举类

JDK5 提供了一种新的特殊的类——枚举类,一般在类对象有限且固定的场景下使用,用来替代类中定义常量的方式。枚举相较于常量更加直观且类型安全。

枚举类的使用非常简单,用 enum 关键字来定义,多个枚举变量直接用逗号隔开。先来定义一个简单的枚举类 OrderStatus.java

1
2
3
4
public enum OrderStatus {
//未支付、已支付、退款中、退款成功、退款失败;
NO_PAY, PAY, REFUNDING, REFUNDED, FAIL_REFUNDED, ;
}

在其他类中使用 enum 变量的时候,只需要【类名.变量名】就可以了,和使用静态变量一样。另外,枚举类型可以确保 JVM 中仅存在一个常量实例,所以可以放心的使用“ ==”来比较两个变量。

注意事项:

  1. 枚举类的第一行必须是枚举项,最后一个枚举项后的分号是可以省略的,但是如果枚举类有其它的东西,这个分号就不能省略。建议不要省略!
  2. 枚举变量最好大写,多个单词之间使用“_”隔开(比如:NO_PAY)。

反编译

可以先通过 javac 命令或者 IDEA 的编译功能将OrderStatus.java 编译为OrderStatus.class 字节码文件,然后用DJ Java Decompiler 反编译器对 .class 文件进行反编译。

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
public final class OrderStatus extends Enum
{

//该方法会返回包括所有枚举变量的数组,可以方便的用来做循环。
public static OrderStatus[] values()
{
return (OrderStatus[])$VALUES.clone();
}

//根据传入的字符串,转变为对应的枚举变量。
//前提是传的字符串和定义枚举变量的字符串一抹一样,区分大小写。
//如果传了一个不存在的字符串,那么会抛出异常。
public static OrderStatus valueOf(String name)
{
return (OrderStatus)Enum.valueOf(com/itcast/java/enumpack/OrderStatus, name);
}

private OrderStatus(String s, int i)
{
super(s, i);
}

public static final OrderStatus NO_PAY;
public static final OrderStatus PAY;
public static final OrderStatus REFUNDING;
public static final OrderStatus REFUNDED;
public static final OrderStatus FAIL_REFUNDED;
private static final OrderStatus $VALUES[];

static
{
NO_PAY = new OrderStatus("NO_PAY", 0);
PAY = new OrderStatus("PAY", 1);
REFUNDING = new OrderStatus("REFUNDING", 2);
REFUNDED = new OrderStatus("REFUNDED", 3);
FAIL_REFUNDED = new OrderStatus("FAIL_REFUNDED", 4);
$VALUES = (new OrderStatus[] {
NO_PAY, PAY, REFUNDING, REFUNDED, FAIL_REFUNDED
});
}
}

如源码所示:

  • 编译器会自动创建一个 final 类型的类继承 Enum 类,所以枚举类不能被继承。
  • 会自动生成私有构造方法,当然也可以定义构造方法,但必须是私有的,这样就不能在别处声明此类的对象了。
  • 枚举项会被自动添加 public static final 修饰,并定义为 OrderStatus 类型,并在静态代码块中被初始化。
  • 并提供了 values()valueOf(String name) 的静态方法。

定义的枚举变量实际上是编译器自动生成了构造函数。

所有枚举类都是 Enum 的子类,枚举类可以实现一个或多个接口。

Enum

Enum 是所有 Java 语言枚举类型的公共基类,实现了 Comparable 和 Serializable 接口。它包含 final 类型的 name 和 ordinal (此枚举常量的序号,从0开始)属性,下面来了解下它的方法

  • protected Enum(String name, int ordinal);——构造方法;
  • public String toString();——返回 name 字段,即枚举定义枚举变量的字符串;
  • protected final Object clone();——抛出 CloneNotSupportedException 异常,保证枚举类永远不会被克隆;
  • public final ClassgetDeclaringClass();——返回与此枚举常量的枚举类型对应的类对象;
  • protected final void finalize();—— 枚举类不能有 finalize 方法;
  • readObject(ObjectInputStream in);&readObjectNoData();—— 抛出InvalidObjectException 异常,防止默认反序列化;

扩展

  1. 枚举类中可以自定义属性自定义的属性值最好用 private final 修饰,防止生成的 set 方法在使用时修改属性值,使代码更加安全。
  2. 枚举类中可以自定义构造函数构造函数必须为 private 修饰,防止在别处声明此类对象。
  3. 枚举类可以自定义方法,枚举项可以选择性覆盖自定义的方法。
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
public enum OrderStatus{
NO_PAY("未支付",0),
PAY("已支付",1){
@Override
public void printOrderStatus() {
System.out.println("已支付");
}
},
REFUNDING("退款中",2),
REFUNDED("退款成功",3),
FAIL_REFUNDED("退款失败",4),
;

private final String name;
private final int status;

private OrderStatus(String name,int status){
this.name = name;
this.status = status;
}

public void printOrderStatus(){
System.out.println("打印订单状态");
}
}


public class EnumTest {
public static void main(String[] args) {
OrderStatus.PAY.printOrderStatus();
OrderStatus.NO_PAY.printOrderStatus();
}
}

img

枚举类也可以有抽象方法,但是枚举项必须重写该方法。

  1. 枚举类实现接口与普通类一样,实现接口的时候需要实现接口的抽象方法,也可以让枚举类的不同对象实现不同的行为。

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
//定义一个接口
public interface Order {
void printOrderStatus();
}

//枚举类实现该接口
public enum OrderStatus implements Order{
NO_PAY("未支付",0){
@Override
public void printOrderStatus() {
System.out.println("未支付");
}
},
PAY("已支付",1){
@Override
public void printOrderStatus() {
System.out.println("已支付");
}
},
REFUNDING("退款中",2){
@Override
public void printOrderStatus() {
System.out.println("退款中");
}
},
REFUNDED("退款成功",3){
@Override
public void printOrderStatus() {
System.out.println("退款成功");
}
},
FAIL_REFUNDED("退款失败",4){
@Override
public void printOrderStatus() {
System.out.println("退款失败");
}
},
;

private final String name;
private final int status;

private OrderStatus(String name,int status){
this.name = name;
this.status = status;
}
}

此时查看编译后的文件,会发现除了生成 OrderStatus.class 文件之外,还生成了多个 .class 文件:

img

它们是 OrderStatus.class 中生成的匿名内部类的文件。

状态转换

需求

订单是电商项目中不可缺少的组成部分,而订单状态的转换也是经常讨论的问题。都知道订单状态的转换是有一定的逻辑性的,不可以随意转换。

例:购买某个商品,只是把它加入了购物车,此时应该是未支付状态。如果来个请求想把它转换为退款状态,那么系统应该抛出提示信息“状态转换失败,请先完成购买!”

接下来就用枚举来完成一下订单状态转换的限制。

实现

枚举类定义:

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
public enum OrderStatus{
NO_PAY("未支付",0){
@Override
public Boolean canChange(OrderStatus orderStatus) {
switch (orderStatus){
case PAY:
return true;
default:
return false;
}
}
},
PAY("已支付",1){
@Override
public Boolean canChange(OrderStatus orderStatus) {
//因为退款接口一般都会有延迟,所以会先转化为“退款中”状态
switch (orderStatus){
case REFUNDING:
return true;
default:
return false;
}
}
},
REFUNDING("退款中",2){
@Override
public Boolean canChange(OrderStatus orderStatus) {
switch (orderStatus){
case REFUNDED:
case FAIL_REFUNDED:
return true;
default:
return false;
}
}
},
REFUNDED("退款成功",3),
FAIL_REFUNDED("退款失败",4),
;

private final String name;
private final int status;

private OrderStatus(String name,int status){
this.name = name;
this.status = status;
}

//自定义转换方法
public Boolean canChange(OrderStatus orderStatus){
return false;
}
}

调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
public class EnumTest {

public static void main(String[] args) {
Boolean aBoolean = OrderStatus.NO_PAY.canChange(OrderStatus.PAY);
String statusStr = aBoolean?"可以":"不可以";
System.out.println("是否可以完成状态转换:"+ statusStr);

Boolean flag = OrderStatus.REFUNDED.canChange(OrderStatus.FAIL_REFUNDED);
String flagStr = flag?"可以":"不可以";
System.out.println("是否可以完成状态转换:"+ flagStr);
}
}

返回结果:

img

这样就用枚举类实现了订单状态转换的限制。此例子只是为状态转换提供一种思路,具体的流程还需要根据自己系统中的业务来具体处理。

1
2
3
4
5
6
7
/**
* prevent default deserialization
*/
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}

知道Enum类源码这个阻止默认反序列化是干什么的吗,我试了一下反序列化他,可是他并没有抛出异常,结果也是正确的。

枚举常量的序列化不同于普通的可序列化或可外部化的对象。枚举常量的序列化形式仅由其名称组成;常量的字段值不存在于表单中。为了序列化枚举常量,ObjectOutputStream 写入枚举常量的 name 方法返回的值。为了反序列化一个枚举常量,ObjectInputStream 从流中读取常量名;然后通过调用 java.lang.Enum.valueOf 方法获得反序列化的常量,将常量的枚举类型与接收到的常量名称一起作为参数传递。与其他可序列化或可外部化的对象一样,枚举常量可以充当随后出现在序列化流中的反向引用的目标。

枚举常量的序列化过程无法自定义:在序列化和反序列化过程中,枚举类型定义的任何类特定的 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 方法都将被忽略。类似地,任何 serialPersistentFields 或 serialVersionUID 字段声明也被忽略——所有枚举类型都有一个固定的 serialVersionUID 0L。没有必要为枚举类型记录可序列化字段和数据,因为发送的数据类型没有变化。

枚举类类本来就是数据不变的常量,序列化只记录名字,反序列化时jvm直接根据名字找到本地的就行了,不需要也没必要加载类里面的数据。

本文标题:使用枚举实现状态转换限制

文章作者:LiJing

发布时间:2022年05月05日 - 17:29:23

最后更新:2023年06月03日 - 09:59:46

原始链接:https://blog-next.xiaojingge.com/posts/870230163.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------------本文结束 感谢您的阅读-------------------