您的浏览器过于古老 & 陈旧。为了更好的访问体验, 请 升级你的浏览器
Ready 发布于2019年12月07日 22:58 最近更新于 2019年12月07日 23:04

原创 通过一个诡异的字段初始化循环赋值案例 探究字段的初始化顺序细节

2515 次浏览 读完需要≈ 8 分钟 Java

内容目录

我们先来看一段示例代码:

public class Test {

	public interface A {
		public static final int X = 2 * B.Y;
	}

	public interface B {
		public static final int Y = C.Z + 1;
	}

	public interface C {
		public static final int Z = A.X + 1;
	}

	@Test
	public void test() {
		System.out.println(A.X + B.Y + C.Z); // 输出多少 ?
	}
}

大家估算一下,上面的代码会在控制台输出多少?

得出正确答案了吗?先不公布答案,我们再看看,假如将最后的单元测试方法test()改为如下代码呢:

	@Test
	public void test() {
		System.out.println( C.Z + B.Y + A.X ); // 输出多少 ?
	}

我们再颠倒一下运算顺序,将最后的单元测试代码改为如下呢:

	@Test
	public void test() {
		System.out.println( B.Y + A.X + C.Z ); // 输出多少 ?
	}
	@Test
	public void test() {
		System.out.println( A.X + C.Z + B.Y ); // 输出多少 ?
	}

大家准备好上面几个例子的答案了么?如果需要查看答案,请参见本文的最后一个段落。

通过上面几个例子的输出,我们惊讶地发现,它们的输出结果显得有点「诡异」,输出结果居然并不一样!

这是为什么呢?

我们以上面第一个示例作为参考来加以说明。

虽然 A.XB.YC.Z这三个字段都是同时被 static final 修饰的常量,并且都是原始数据类型,但是它们之间存在循环引用,因此这三个字段的值是无法在编译时就确定的,只能在运行时 当这几个字段被首次初始化时通过计算得出结果。

这几个字段的首次初始化是由于我们在代码中执行了 A.X + B.Y + C.Z 这样的加法运算所引起的。大家都知道,加法运算的顺序是从左往右的。

因此首先被加载字节码文件定义的是A。由于A.X的初始化依赖于B.Y,于是又开始加载B的字节码定义;B.Y的初始化又依赖于C.Z,于是又加载C的字节码定义;C.Z又依赖于A.X。由于此时 A.X 尚未完成初始化,因此C.Z的值表达式中的A.X被直接初始化为int类型的默认值 0 请注意:只是C.Z的值表达式(A.X + 1)中的A.X被初始化为默认值 0,并不是A中的字段X本身被初始化为 0,此时A.X尚未完成初始化】。

然后结果就出来了:

C.Z = A.X + 1 = 0 + 1 = 1;
B.Y = C.Z + 1 = 1 + 1 = 2;
A.X = 2 * B.Y = 2 * 2 = 4
A.X + B.Y + C.Z = 7;

同理,后面的几个例子也不难理解了。

在我们的实例代码中,由于字段的表达式存在循环引用依赖,因此在test()方法中,加法运算表达式里靠后的两个字段的顺序并不会影响字段的初始化,因为在加载并初始化第一个字段时,由于循环引用的关系,它所依赖的其他字段都被初始化完毕了(这有点像递归的感觉?)。

接着,我们再延伸发散一下。如果将例一中字段的类型全部由int类型改为Integer,又会有什么样的结果呢?

public class Test {

	public interface A {
		public static final Integer X = 2 * B.Y;
	}

	public interface B {
		public static final Integer Y = C.Z + 1;
	}

	public interface C {
		public static final Integer Z = A.X + 1;
	}

	@Test
	public void test() {
		System.out.println( A.X + B.Y + C.Z ); // 输出多少?
	}
}

在这个例子中,我们得到的结果将会是:程序报错!

里面的逻辑和我们上面说的实际上是一致的,只不过由于C.Z依赖于A.X,但此时A.X尚未初始化,因此此处的A.X就被初始化为默认值null。由于进行的是加法求和运算,因此值为nullA.X将被自动拆箱,自动拆箱是通过A.X.intValue()来隐式完成的,由于A.Xnull,因此将出现java.lang.NullPointerException空指针异常。

由于是在静态字段初始化的过程中抛出该异常,从而引发java.lang.ExceptionInInitializerError错误(空指针是根异常)。


上述四个例子的输出分别为:7、6、3、7。

  • CodePlayer技术交流群1
  • CodePlayer技术交流群2

1 条评论

二周 · 4年前

吊诡

0 0 0

撰写评论