概述

在这个教程中,我们将探讨如何在Java反序列化API中使用readObject()readResolve()方法。同时,我们会比较这两个方法的区别。

2. 序列化

Java序列化更深入地介绍了序列化和反序列化的原理。本文将专注于readResolve()readObject()方法,它们在使用反序列化时经常引起疑问。

3. readObject()的使用

在序列化过程中,Java对象会被转换成字节流保存在文件或网络传输。通过ObjectInputStreamreadObject()方法,反序列化过程会将字节流恢复成原始对象,这个过程内部会调用defaultReadObject()进行默认的反序列化。

如果我们的类中定义了readObject()方法,那么ObjectInputStreamreadObject()方法会在从流中读取对象时使用我们类的readObject()方法。

例如,在某些情况下,我们可以在类中实现readObject()来以特定方式反序列化字段。

在展示实际案例之前,让我们看看在类中实现readObject()方法的语法:

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;

假设我们有一个User类,包含两个字段:

public class User implements Serializable {

    private static final long serialVersionUID = 3659932210257138726L;
    private String userName;
    private String password;

    // standard setters, getters, constructor(s) and toString()
}

我们不想以明文形式序列化password字段,那么Java的readObject()方法能帮到我们。

3.1. 在序列化时添加自定义更改

首先,我们可以在writeObject()方法中对对象字段进行特定的序列化修改,比如对password字段进行编码。

对于User类,我们在writeObject()方法中添加一个额外的字符串前缀到密码字段:

private void writeObject(ObjectOutputStream oos) throws IOException {
    this.password = "xyz" + password;
    oos.defaultWriteObject();
}

3.2. 不使用readObject()的测试

现在,我们测试User类,但不实现readObject()。在这种情况下,ObjectInputStream类的readObject()方法会被调用:

@Test
public void testDeserializeObj_withDefaultReadObject() throws ClassNotFoundException, IOException {
    // Serialization
    FileOutputStream fos = new FileOutputStream("user.ser");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    User acutalObject = new User("Sachin", "Kumar");
    oos.writeObject(acutalObject);

    // Deserialization
    User deserializedUser = null;
    FileInputStream fis = new FileInputStream("user.ser");
    ObjectInputStream ois = new ObjectInputStream(fis);
    deserializedUser = (User) ois.readObject();
    assertNotEquals(deserializedUser.hashCode(), acutalObject.hashCode());
    assertEquals(deserializedUser.getUserName(), "Sachin");
    assertEquals(deserializedUser.getPassword(), "xyzKumar");
}

可以看到,密码是xyzKumar,因为我们类中还没有readObject()方法来获取原始字段并进行自定义更改。

3.3. 在反序列化时添加自定义更改

接下来,我们可以在readObject()方法中对对象字段进行特定的反序列化修改,如解码password字段。

User类中实现readObject()方法,并移除我们在序列化时添加到密码字段的额外字符串前缀:

private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
    ois.defaultReadObject();
    this.password = password.substring(3);
}

3.4. 使用readObject()的测试

再次测试User类,这次readObject()方法会在反序列化时被调用:

@Test
public void testDeserializeObj_withOverriddenReadObject() throws ClassNotFoundException, IOException {
    // Serialization
    FileOutputStream fos = new FileOutputStream("user.ser");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    User acutalObject = new User("Sachin", "Kumar");
    oos.writeObject(acutalObject);

    // Deserialization
    User deserializedUser = null;
    FileInputStream fis = new FileInputStream("user.ser");
    ObjectInputStream ois = new ObjectInputStream(fis);
    deserializedUser = (User) ois.readObject();
    assertNotEquals(deserializedUser.hashCode(), acutalObject.hashCode());
    assertEquals(deserializedUser.getUserName(), "Sachin");
    assertEquals(deserializedUser.getPassword(), "Kumar");
}

我们可以注意到,对象不同,并且自定义的readObject()方法被调用,密码字段被正确转换。

4. readResolve()的使用

在Java反序列化中,readResolve()方法用于在反序列化期间替换创建的对象,以使用不同的对象。这在需要确保应用程序中特定类只有一个实例或者希望替换已经在内存中存在的不同实例时非常有用。

让我们回顾一下在类中添加readResolve()的方法语法:

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

readObject()示例中,值得注意的是对象的hashCode不同。这是因为在反序列化过程中,新的对象是从流化的对象创建的。

我们通常会在创建单例实例时使用readResolve(),通过它确保反序列化后的对象与现有实例相同。

让我们举个创建单例对象的例子:

public class Singleton implements Serializable {

    private static final long serialVersionUID = 1L;
    private static Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

4.1. 不使用readResolve()的测试

到目前为止,我们还没有添加readResolve()方法。现在测试我们的Singleton类:

@Test
public void testSingletonObj_withNoReadResolve() throws ClassNotFoundException, IOException {
    // Serialization
    FileOutputStream fos = new FileOutputStream("singleton.ser");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    Singleton actualSingletonObject = Singleton.getInstance();
    oos.writeObject(actualSingletonObject);

    // Deserialization
    Singleton deserializedSingletonObject = null;
    FileInputStream fis = new FileInputStream("singleton.ser");
    ObjectInputStream ois = new ObjectInputStream(fis);
    deserializedSingletonObject = (Singleton) ois.readObject();
    assertNotEquals(actualSingletonObject.hashCode(), deserializedSingletonObject.hashCode());
}

这里我们可以看到,两个对象不同,这与单例类的目标相悖。

4.2. 使用readResolve()的测试

为了修复这个问题,我们在Singleton类中添加readResolve()方法:

private Object readResolve() throws ObjectStreamException {
    return INSTANCE;
}

现在,我们再次测试,带有readResolve()方法的Singleton类:

@Test
public void testSingletonObj_withCustomReadResolve() throws ClassNotFoundException, IOException {
    // Serialization
    FileOutputStream fos = new FileOutputStream("singleton.ser");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    Singleton actualSingletonObject = Singleton.getInstance();
    oos.writeObject(actualSingletonObject);

    // Deserialization
    Singleton deserializedSingletonObject = null;
    FileInputStream fis = new FileInputStream("singleton.ser");
    ObjectInputStream ois = new ObjectInputStream(fis);
    deserializedSingletonObject = (Singleton) ois.readObject();
    assertEquals(actualSingletonObject.hashCode(), deserializedSingletonObject.hashCode());
}

可以看到,两个对象的hashCode相同。

5. readObject()readResolve()的对比

以下是这两个方法的主要区别:

readResolve()

readObject()

返回类型:Object

返回类型:void

参数:无

参数:ObjectInputStream作为参数

通常用于实现单例模式,需要在反序列化后返回同一个对象。

用于设置对象的非持久字段值,这些字段未被序列化,如由其他字段派生的字段或动态初始化的字段。

可能抛出异常:ClassNotFoundException, ObjectStreamException

可能抛出异常:ClassNotFoundException, IOException

readObject()更快,因为它不需要读取整个对象图。

readResolve()慢,因为它读取整个对象图。

6. 结论

在这篇文章中,我们了解了Java序列化API中的readObject()readResolve()方法,并比较了两者。如往常一样,本文的示例代码可在GitHub上找到。