Java枚举私有final字段循环依赖中null值成因解析
枚举循环依赖导致null值的原因解析
问题场景
先看两个互相循环依赖的枚举定义:
enum Color { RED(Shape.CIRCLE), GREEN(Shape.TRIANGLE), BLUE(Shape.SQUARE); private final Shape shape; Color(Shape shape) { this.shape = shape; } Shape getShape() { return shape; } } enum Shape { CIRCLE(Color.RED), TRIANGLE(Color.GREEN), SQUARE(Color.BLUE); private final Color color; Shape(Color color) { this.color = color; } Color getColor() { return color; } }
这两个枚举的常量互相作为构造参数,Java 8编译时不会给出警告,但运行以下测试用例时,每个测试的第二行都会失败:
@Test public void testRedAndCircle() { assertThat(Color.RED.getShape()).isNotNull(); // 正常通过 assertThat(Shape.CIRCLE.getColor()).isNotNull(); // 失败,返回null } @Test public void testCircleAndRed() { assertThat(Shape.CIRCLE.getColor()).isNotNull(); // 正常通过 assertThat(Color.RED.getShape()).isNotNull(); // 失败,返回null } @Test public void testGreenAndTriangle() { assertThat(Color.GREEN.getShape()).isNotNull(); // 正常通过 assertThat(Shape.TRIANGLE.getColor()).isNotNull(); // 失败,返回null } @Test public void testBlueAndSquare() { assertThat(Color.BLUE.getShape()).isNotNull(); // 正常通过 assertThat(Shape.SQUARE.getColor()).isNotNull(); // 失败,返回null }
原因解释
这是Java枚举初始化机制和循环依赖共同作用的结果,核心逻辑如下:
- 枚举初始化时机:当首次访问枚举的静态成员(比如枚举常量)时,JVM会触发该枚举类的初始化,执行类的
<clinit>方法,按顺序创建所有枚举常量。 - 初始化的顺序敏感性:循环依赖会打断初始化流程,导致某个枚举常量在构造时拿到的是另一个枚举常量的
null引用:- 比如首次访问
Color.RED时,JVM开始初始化Color类,创建RED需要传入Shape.CIRCLE,此时Shape类未初始化,JVM会暂停Color的初始化,转而初始化Shape类。 - 在
Shape类初始化时,创建CIRCLE需要传入Color.RED,但此时Color类的<clinit>还没执行到给RED赋值的步骤(初始化被暂停),所以Color.RED还是默认的null,最终Shape.CIRCLE的color字段被赋值为null。 - 等
Shape类初始化完成后,回到Color类的初始化,RED的shape字段会被正确赋值为Shape.CIRCLE,但Shape.CIRCLE的color已经是null了。 - 反过来,如果首次访问
Shape.CIRCLE,则会出现Shape.CIRCLE.getColor()正常,但Color.RED.getShape()为null的情况。
- 比如首次访问
简单来说:先触发初始化的枚举,其常量能拿到另一个枚举的完整实例;但另一个枚举的常量在构造时,只能拿到还未被赋值的null引用。
总结
这种循环依赖属于枚举设计的反模式,Java编译器不会主动检测警告,但运行时会出现不符合预期的null值。解决方法是重构代码消除循环依赖——比如通过中间类维护关联关系,或者使用延迟加载在需要时再建立关联。
内容的提问来源于stack exchange,提问作者mjn
相关产品推荐
相关产品推荐

