请考虑以下代码段:
class Test1 {
private static Test1 instance;
@NonNull private final Date date1;
@NonNull private final Date date2;
Test1() throws Exception {
this.date1 = new Date();
Test1.instance = this;
if (true) {
throw new Exception();
}
this.date2 = new Date();
}
public void dump() {
System.out.println("date1: " + date1);
System.out.println("date2: " + date2);
}
static void test() {
Test1 t1 = null;
try {
t1 = new Test1();
} catch (Exception e) {
e.printStackTrace();
}
Test1.instance.dump();
assert t1 == null;
}
}
Test1的构造函数在将自身分配给静态字段后总是抛出异常。该字段保留对部分初始化对象的引用:date2
字段为null
的对象,即使它已声明为@NonNull
和final
。
test()
函数无法直接获取对生成的t1的引用。在catch
块之后,t1为空。
然而,Test1.instance很好,调用它的dump()
函数显示date1
已初始化但date2
为null
。
这里发生了什么?为什么我可以保持对真正处于非法状态的对象的引用?
修改
t1
为空的事实是显而易见的(与In Java what happens when an object fails to be instantiated?不同)。这个问题是关于设法存储在静态字段中的对象的状态。
答案 0 :(得分:8)
考虑以下类的字节码:
class Foo {
public static void main(String[] args) {
new Foo();
}
}
字节码:
class Foo {
Foo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class Foo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: pop
8: return
}
您可以从中看到,新实例的创建和构造函数的调用是分开的(main
中的第0行和第4行)。
因此,即使它没有完全初始化,实例也存在;并且您可以将该实例的引用分配给另一个引用。
在完全初始化之前将实例分配给静态字段是 unsafe publication 的一个示例,您应该(显然)避免它。