今天遇到一个问题,就是设计了两个枚举,一个是状态枚举(EnumA)一个是动作枚举(EnumB),状态枚举定义了当前状态的可以进行的操作,操作枚举定义了执行了此操作后的下一个状态。
具体代码如下:
public enum EnumA {
STATUS_A("STATUS_A", "状态A", EnumB.OPERATION_A),
STATUS_B("STATUS_B", "状态B", null);
private String code;
private String desc;
private List<EnumB> operation;
}
EnumA(String code, String desc, List<EnumB> operation) {
this.operation = operation;
this.code = code;
this.desc = desc;
}
public enum EnumB {
OPERATION_A("OPERATION_A", "操作1", EnumA.STATUS_B);
private String code;
private String desc;
private EnumA next;
}
EnumB(String code, String desc, EnumA next) {
this.next = next;
this.code = code;
this.desc = desc;
}
当我按照这种定义去声明枚举值常量时,编译器并没有错误提示。
但是执行的时候结果如下:
public static void main(String[] args) {
System.out.println(EnumB.OPERATION_A.getNext());
System.out.println(EnumA.STATUS_A.getOperation());
}
public static void main(String[] args) {
System.out.println(EnumA.STATUS_A.getOperation());
System.out.println(EnumB.OPERATION_A.getNext());
}
可以看出来,首先被使用到的枚举值中的内容是完整的。原因简单分析一下便容易明白,当第一个枚举值(例如是EnumA)被使用的时候,虚拟机查到这个EnumA的实例还没有加载,便调用构造函数去实例化。构造函数中需要EnumB的某个实例,所以虚拟机会调用EnumB的构造函数去实例化EnumB,但是此时EnumA还没有实例化完成,拿到的指针为NULL,所以EnumB中的类型为EnumA的属性便为NULL。
解决方法
对症下药的想法就是将两个枚举的关系在初始化层面解除耦合,但是带来的问题是两个枚举又需要使用对方的实例,什么时候将对方的实例填充进去呢?初步想法是当首先被调用的枚举实例化完成后将此枚举的实例填入另外一个枚举属性中。
那第二个问题便是如何让首先使用的枚举实例化后自动的开始填充动作呢?映入脑海的应该是代码块了,它是类加载的时候会自动执行的代码逻辑。所以是否可以采用在构造函数下方添加一个代码块来让枚举实例化(调用构造函数)后执行相应的代码呢?
具体代码如下:
public enum EnumA {
STATUS_A("STATUS_A", "状态A", EnumB.OPERATION_A),
STATUS_B("STATUS_B", "状态A", null);
private String code;
private String desc;
private EnumB operation;
EnumA(String code, String desc, EnumB next) {
this.code = code;
this.desc = desc;
this.operation = next;
}
{
EnumB.OPERATION_A.setNext(STATUS_B);
}
}
但是添加了代码后,编译器明显的提示像是对我说:“小伙子想的太简单了”。
这段代码有一个警告和一个编译错误,他们的内容分别如下:
这里说明了,枚举实例是静态的,就是和静态成员变量(用static修饰的变量)是同一个级别的。但是他们的初始化又是利用构造函数才可以完成的,执行了构造函数则必然会执行代码块。因为实例化要一个一个执行,所以代码块执行的时候其他的某些枚举实例还没有创建,所以需要使用静态代码块来完成填充的动作。
所以正确的方式如下:但是并不是完美解决了,需要看文章最后一部分的说明
public enum EnumA {
STATUS_A("STATUS_A", "状态A", EnumB.OPERATION_A),
STATUS_B("STATUS_B", "状态A", null);
private String code;
private String desc;
private EnumB operation;
static {
EnumB.OPERATION_A.setNext(STATUS_B);
}
EnumA(String code, String desc, EnumB next) {
this.code = code;
this.desc = desc;
this.operation = next;
}
}
枚举实例加载顺序
这里做了一个实验,可以观察到枚举实例在实例化时的执行顺序:
public enum EnumA {
STATUS_A("STATUS_A", "状态A", EnumB.OPERATION_A),
STATUS_B("STATUS_B", "状态A", null);
private String code;
private String desc;
private EnumB operation;
static {
System.out.println("static:");
}
EnumA(String code, String desc, EnumB next) {
this.code = code;
this.desc = desc;
this.operation = next;
System.out.println("constructor:" + this);
}
{
System.out.println("init:" + this);
}
}
可见在实例化的时候,构造函数和代码块依次执行。当全部枚举实例都实例化完成后静态代码块才会执行,逻辑比较简单,毕竟枚举实例属于静态变量,当前的状态还是处于类级别初始化阶段,只不过初始化要用到具体对象的实例化逻辑,才会调用到构造函数和代码块,当全部枚举实例实例化完成后,类初始化完毕后便会执行静态代码块中的逻辑。
写在最后
这一点是在完成总结的时候想到的,因为上述方案只是在EnumA中进行了属性值的填充,但是它的前提是EnumA要优先在EnumB之前被调用,不然在EnumB实例化的时候会需要EnumA先实例化完成,但是填充逻辑是在EnumA中的,填充时又需要EnumB的实例,执行的话会报空指针。所以要保证枚举的加载顺序才行
解决方法
- 第一步:将没有填充逻辑的枚举中构造函数中的入参中去掉类型为另外一个枚举的变量,这样做才可以保证正常的初始化。
- 第二步:在没有填充逻辑的枚举中的静态代码块中调用一下有填充逻辑的枚举,这样就会触发实例化另外一个枚举的动作,并且执行填充逻辑。这样即使没有填充逻辑的枚举优先被调用,也可以获取到正常的值。
具体代码如下:
public enum EnumB {
OPERATION_A("OPERATION_A", "操作1");
private String code;
private String desc;
@Setter
private EnumA next;
static {
EnumA init = EnumA.STATUS_A;
}
EnumB(String code, String desc) {
this.code = code;
this.desc = desc;
}
}