Java枚举双向引用问题:如何规避初始化死锁?
兄弟,你碰到的确实是枚举初始化里典型的循环依赖问题,跟死锁的表现有点像,但本质是JVM类加载阶段的顺序依赖坑——枚举实例是在类加载时就初始化的,互相引用的话,谁先加载谁就能拿到对方的完整实例,后加载的那个在构造时引用对方,可能拿到的是还没完全初始化的对象甚至null,所以调换顺序结果就反过来了。
下面给你几种靠谱的解决思路,按需选就行:
方案1:延迟初始化(懒加载)关联对象
别在枚举构造器里直接初始化双向关联,先把关联字段留空,等所有枚举类都加载完成后,再手动建立关联关系。
举个代码例子:
public enum FirstEnum { INSTANCE_A, INSTANCE_B; private SecondEnum secondEnum; // 提供设置关联的方法 public void setSecondEnum(SecondEnum secondEnum) { this.secondEnum = secondEnum; } public SecondEnum getSecondEnum() { return secondEnum; } } public enum SecondEnum { INSTANCE_X, INSTANCE_Y; private FirstEnum firstEnum; public void setFirstEnum(FirstEnum firstEnum) { this.firstEnum = firstEnum; } public FirstEnum getFirstEnum() { return firstEnum; } } // 专门的初始化类,在所有枚举加载完成后建立关联 public class EnumInitializer { static { FirstEnum.INSTANCE_A.setSecondEnum(SecondEnum.INSTANCE_X); SecondEnum.INSTANCE_X.setFirstEnum(FirstEnum.INSTANCE_A); FirstEnum.INSTANCE_B.setSecondEnum(SecondEnum.INSTANCE_Y); SecondEnum.INSTANCE_Y.setFirstEnum(FirstEnum.INSTANCE_B); } }
这种方式完全避开了类加载时的循环依赖,因为关联是在所有枚举类都准备好之后才建立的。
方案2:通过名称延迟获取关联实例
如果不想手动写初始化代码,可以在构造器里只保存对方枚举的名称,第一次调用获取方法时,再通过valueOf()去拿到实例——这时候两个枚举类肯定已经加载完成了。
代码示例:
public enum FirstEnum { INSTANCE_A(SecondEnum.INSTANCE_X.name()), INSTANCE_B(SecondEnum.INSTANCE_Y.name()); private final String secondEnumName; private SecondEnum secondEnum; FirstEnum(String secondEnumName) { this.secondEnumName = secondEnumName; } public SecondEnum getSecondEnum() { // 懒加载:第一次调用时才初始化关联 if (secondEnum == null) { secondEnum = SecondEnum.valueOf(secondEnumName); } return secondEnum; } } public enum SecondEnum { INSTANCE_X(FirstEnum.INSTANCE_A.name()), INSTANCE_Y(FirstEnum.INSTANCE_B.name()); private final String firstEnumName; private FirstEnum firstEnum; SecondEnum(String firstEnumName) { this.firstEnumName = firstEnumName; } public FirstEnum getFirstEnum() { if (firstEnum == null) { firstEnum = FirstEnum.valueOf(firstEnumName); } return firstEnum; } }
方案3:引入中间映射类维护关联
如果你的枚举实例比较多,或者不想给枚举加额外的方法,可以把双向关联逻辑剥离到一个单独的映射类里,让枚举本身保持简洁。
代码示例:
public enum FirstEnum { INSTANCE_A, INSTANCE_B; } public enum SecondEnum { INSTANCE_X, INSTANCE_Y; } public class EnumMapping { // 维护两个方向的映射关系 private static final Map<FirstEnum, SecondEnum> FIRST_TO_SECOND = new HashMap<>(); private static final Map<SecondEnum, FirstEnum> SECOND_TO_FIRST = new HashMap<>(); static { FIRST_TO_SECOND.put(FirstEnum.INSTANCE_A, SecondEnum.INSTANCE_X); SECOND_TO_FIRST.put(SecondEnum.INSTANCE_X, FirstEnum.INSTANCE_A); FIRST_TO_SECOND.put(FirstEnum.INSTANCE_B, SecondEnum.INSTANCE_Y); SECOND_TO_FIRST.put(SecondEnum.INSTANCE_Y, FirstEnum.INSTANCE_B); } // 提供获取关联的方法 public static SecondEnum getSecondEnum(FirstEnum firstEnum) { return FIRST_TO_SECOND.get(firstEnum); } public static FirstEnum getFirstEnum(SecondEnum secondEnum) { return SECOND_TO_FIRST.get(secondEnum); } }
补充说明:为什么初始化顺序会影响结果?
JVM加载枚举类时,会严格按照枚举实例的声明顺序依次初始化。当第一个枚举的构造器引用第二个枚举时,如果第二个枚举还没完成初始化,就会得到一个未完全初始化的实例(甚至null,取决于JVM具体实现);调换顺序后,第二个枚举先完成加载,所以能拿到第一个枚举的完整实例,结果自然相反。这本质是类加载阶段的循环依赖问题,不是传统意义上的死锁,但表现类似——初始化被阻塞或者得到不完整的对象。
内容的提问来源于stack exchange,提问作者Adeptius
相关产品推荐
相关产品推荐

