Java与单例模式一文中提到了,Java可以通过反序列化来破坏单例,其底层就是利用反射,通过一个代表无参构造方法的Constructor
对象,使用其newInstance()
方法来创建对象。
但是,在后续的测试代码中发现,其实目标类的无参构造方法并没有执行!所以,对于这个对象的创建过程并不是我一开始想的那样。请看下面的例子:
// 目标类
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
public Elvis() {
System.out.println("no parameters constructor invoked!");
}
}
// 测试
@Test
public void testSerialization() throws Exception {
Elvis elvis1 = Elvis.INSTANCE;
FileOutputStream fos = new FileOutputStream("a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(elvis1);
oos.flush();
oos.close();
Elvis elvis2 = null;
FileInputStream fis = new FileInputStream("a.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
elvis2 = (Elvis) ois.readObject();
System.out.println("elvis1与elvis2相等吗? ===> " + (elvis1 == elvis2));
}
结果:
no parameters constructor invoked!
elvis1与elvis2相等吗? ===> false
public static final Elvis INSTANCE = new Elvis();
是对象第一次实例化,所以第一行的打印no parameters constructor invoked
是这个时候执行的。Elvis
的构造方法只执行了一次,所以反序列化的时候,Java没有去调用目标类的构造方法来创建对象。
回顾反序列化过程的Java代码,在ObjectInputStream
类中的Object readOrdinaryObject(boolean)
方法中。如下:
private Object readOrdinaryObject(boolean unshared)
throws IOException {
//此处省略部分代码
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//此处省略部分代码
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
重点代码块:
obj = desc.isInstantiable() ? desc.newInstance() : null;
我们知道desc.newInstance()
就是创建对象,我读了一下这个方法的Java文档:
创建目标类的一个新的实例对象。如果该类是
Externalizable
类型的,则调用它自身的无参构造方法(前提是该方法是public
)。如果该类是Serializable
类型的,则调用该类的第一个非Serializable
类型的父类的无参构造方法。
文档已经告诉我们答案了。在我们的测试代码中,Elvis
实现了Serializable
接口,所以反序列化的时候调用的是它父类的Object
的无参构造方法创建新对象的。因此,Elvis
的无参构造方法没有执行。
请看改进后的测试代码:
public class Parent {
public Parent() {
System.err.println("Parent no-arg constructor invoked!");
}
}
public class Elvis extends Parent implements Serializable {
public static final Elvis INSTANCE = new Elvis();
public Elvis() {
System.out.println("Elvis no-arg constructor invoked!");
}
}
我给Elvis
加了一个父类Parent
,看看结果如何:
Parent no-arg constructor invoked!
Elvis no-arg constructor invoked!
===开始反序列化===
Parent no-arg constructor invoked!
elvis1与elvis2相等吗? ===> false
这下结果是符合我的预期的。父类的无参构造方法被执行了2次。第2次就是desc.newInstance()
时候执行的。
通过debug我们也可以发现,我们在newInstance()
方法中打个断点。如下图:
显而易见,代码中的cons
指向的是父类的构造方法。
这下我们可以得出结论:对于实现Serializable
接口的类,并不要求该类具有一个无参的构造方法, 因为在反序列化的过程中实际上是去其继承树上找到一个没有实现Serializable
接口的父类(最终会找到Object
),然后构造该类的对象,再逐层往下的去设置各个可以反序列化的属性(也就是没有被transient
修饰的非静态属性)。
参考
分享
最近加入了上面参考博文的作者的知识星球,不得不说里面的文章干货满满。每天一篇文章,每天都有收货,觉得很适合刚工作的Java工程师。