如下所示代码:
public class Example049 { private final int overtime; public static final Example049 INSTANCE = new Example049();//1 private static final int CURRENT_YEAR = Calendar.getInstance().get( Calendar.YEAR);//2 private Example049() { overtime = CURRENT_YEAR - 1970; } public int getOverTime() { return overtime; } public static void main(String[] args) { System.out.println(INSTANCE.getOverTime()); } }
结果说明:
输出结果是-1970,如果将1和2调换位置,则输出结果为当前年减去1970的差--45。
代码分析:
该程序所遇到的问题是由类初始化顺序中的循环而引起的。类的初始化是由虚拟机对其 main 方法的调用而触发的。 首先,其静态域被设置为缺省值,其中 INSTANCE 域被设置为 null,CURRENT_YEAR 被设置为 0,overtime被设置为0。接下来,静态域初始器按照其出现的顺序执行初始化。 静态域INSTANCE的值是通过调用构造器而计算出来的。这个构造器会用一个涉及静态域 CURRENT_YEAR 的表达式来初始化 overtime。通常,读取一个静态域是会引起一个类被初始化的事件之一,但是我们已经在初始化类了,递归的初始化尝试会直接被忽略掉。因此,CURRENT_YEAR 的值仍旧是其缺省值 0,这就是为什么结果是-1930了。将2和1调换位置,静态域CURRENT_YEAR会先被初始化,再初始化INSTANCE的时候,CURRENT_YEAR已经有正确的值了,所以输出结果正确。
该程序表明,在 final 类型的静态域被初始化之前,存在着读取它的值的可能,而此时该静态域包含的还只是其所属类型的缺省值。这是与直觉相违背的,因为我们通常会将 final 类型的域看作是常量。final 类型的域只有在其初始化表达式是常量表达式时才是常量。
由类初始化中的循环所引发的问题是难以诊断的,但是一旦被诊断到,通常是很容易订正的。要想订正一个类初始化循环,需要重新对静态域的初始器进行排序,使得每一个初始器都出现在任何依赖于它的初始器之前。
某些通用的设计模式本质上就是初始化循环的, 特别是本题展示的单例模式( Singleton)和服务提供者框架( Service Provider Framework)。类型安全的枚举模式(Typesafe Enum pattern)也会引起类初始化的循环。
总之,要当心类初始化循环。最简单的循环只涉及到一个单一的类,但是它们也可能涉及多个类。 类初始化循环也并非总是坏事,但是它们可能会导致在静态域被初始化之前就调用构造器。静态域, 甚至是 final 类型的静态域,可能会在它们被初始化之前,被读走其缺省值。
注:本【java解惑】系列均是博主阅读《java解惑》原书后将原书上的讲解和例子部分改编然后写成博文进行发布的。所有例子均亲自测试通过并共享在github上。通过这些例子激励自己惠及他人。同时本系列所有博文会同步发布在博主个人微信公众号搜索“爱题猿”或者“ape_it”方便大家阅读。如果文中有任何侵犯原作者权利的内容请及时告知博主以便及时删除如果读者对文中的内容有异议或者问题欢迎通过博客留言或者微信公众号留言等方式共同探讨。
源代码地址https://github.com/rocwinger/java-disabuse