Java反序列化与对象的创建

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()方法中打个断点。如下图:
Java反序列化与对象的创建
显而易见,代码中的cons指向的是父类的构造方法。

这下我们可以得出结论:对于实现Serializable接口的类,并不要求该类具有一个无参的构造方法, 因为在反序列化的过程中实际上是去其继承树上找到一个没有实现Serializable接口的父类(最终会找到Object),然后构造该类的对象,再逐层往下的去设置各个可以反序列化的属性(也就是没有被transient修饰的非静态属性)。

参考

分享

最近加入了上面参考博文的作者的知识星球,不得不说里面的文章干货满满。每天一篇文章,每天都有收货,觉得很适合刚工作的Java工程师。
Java反序列化与对象的创建

上一篇:第89项:对于实例控制,枚举类型优先于readResolve


下一篇:leetcode刷题题解——141.环形链表