深入了解序列化writeObject、readObject、readResolve

说到Java的序列化,大多数人都知道使用ObjectOutputStream将对象写成二进制流,再使用ObjectInputStream将二进制流写成文件实现序列化和反序列化。今天这篇文章将深入分析一下序列化。

1.Serializable

通常我们序列化一个对象的目的是为了可以再序列化回来,使用场景有很多,比如说:
- 把对象保存到文件中,然后可以再恢复
- 使用网络IO传递一个对象
- 因为memcache不支持存储对象,把对象序列化后存到memcache中,用的时候再序列化回来

总之序列化的使用场景有很多。

首先,只有实现了Serializable和Externalizable接口的类的对象才能被序列化。

1.1 不实现Serializable接口序列化报错

可能很多人都知道Serializable接口而不知道Externalizable接口,所以这里先来介绍一下Serializable接口,Externalizable接口最后再介绍。

我们通常一个类的对象需要被序列化,我们会实现Serializable接口,如果不实现Serializable接口则会抛出异常:

java.io.NotSerializableException

1.2 序列化反序列化的注意事项

当我们将一个二进制流反序列化的时候有一些是需要注意的,否则反序列化会失败
- 1.反序列化后对象的全类名(包名+类名)需要和序列化之前对象的全类名一致
- 2.反序列化后对象的serialVersionUID需要和和序列化之前对象的serialVersionUID一致

先来解释一下第一点:序列化后的二进制流中会存储对象的全类名,如果反序列化的时候目标对象的全类名和二进制流中的全类名信息不匹配的话会抛出异常:

java.lang.ClassCastException
示例
@Test
public void testDiffPackage() throws Exception {
    //序列化User对象到file
    File file = new File("E:/User.txt");
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file));
    outputStream.writeObject(new User("lebron","123456"));

    //反序列化文件到另一个包里的User
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
    com.lebron.serializable.otherpackage.User user = (com.lebron.serializable.otherpackage.User)inputStream.readObject();
    //由于篇幅原因,没有关闭流
}
抛出异常
java.lang.ClassCastException

再来解释一下第二点:当我们的类实现Serializable接口的时候,会提示我们添加一个成员变量serialVersionUID,我们可以给serialVersionUID设置一个默认值

private static final long serialVersionUID = 1L;

这个serialVersionUID用来标识这个类的版本,和全类名一样这个serialVersionUID在序列化的时候也会被添加到二进制流中,反序列化的时候如果目标对象的serialVersionUID和二进制流中的serialVersionUID不匹配的话会抛出异常:

java.io.InvalidClassException

我们可以将User对象序列化到文件之后,再修改User类的serialVersionUID,然后反序列化User对象就会出现上面的异常。

1.3 serialVersionUID

实现了Serializable接口的类如果我们不显示的指定serialVersionUID的话,那么会基于类的属性方法等参数生成一个默认的serialVersionUID,如果类的属性或方法有所改动,那么这个默认的serialVersionUID也会随之改动。

所以如果我们不显示的指定serialVersionUID的话,只要类有所改动serialVersionUID就会变化,从而导致反序列化失败。

对于serialVersionUID的显示赋值,一般情况下直接设置成1L就行了,当然了也可以使用IDE帮我们自动生成的serialVersionUID作为默认值。

2.transient修饰符

transient的作用是标记目标类的属性,使得该属性不参与序列化和反序列化的过程。

下面一段代码演示一下transient修饰符的作用

//目标类
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    private String name;
    private transient String password;
    ...
}
@Test
public void testTransient() throws Exception {
    File file = new File("e:/user.txt");
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file));
    outputStream.writeObject(new User("lebron", "123"));

    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
    User user = (User) inputStream.readObject();
    System.out.println(user);
    //由于篇幅原因,没有关闭流
}

User类的password属性被transient修饰时的返回值

User [name=lebron, password=null]

User类的password属性没有被transient修饰时的返回值

User [name=lebron, password=123]

通过上面一段测试代码可以得出结论:序列化反序列化会忽略transient修饰的属性

3.static修饰符

static修饰符修饰的属性也不会参与序列化和反序列化。

有时候我们反序列化后生成的对象中的静态成员变量的值和序列化之前是一样的,但是这并不是通过反序列化得到的,而是因为静态成员变量时类的属性,反序列化后的对象也是这个目标类,所以这个静态成员变量会和序列化之前的值一样。

3.默认方法writeObject和readObject

 * private void writeObject(java.io.ObjectOutputStream out)
 *     throws IOException
 * private void readObject(java.io.ObjectInputStream in)
 *     throws IOException, ClassNotFoundException;

通过翻看Serializable接口的注释,我们可以看到这两个方法,这两个方法下面有大量的注释,这里就不贴出来了,有兴趣的可以自己去看一下,这里解释一下这两个方法。

先来看一下ObjectStreamClass类的源码:

if (externalizable) {
    cons = getExternalizableConstructor(cl);
} else {
    cons = getSerializableConstructor(cl);
    writeObjectMethod = getPrivateMethod(cl, "writeObject",
        new Class<?>[] { ObjectOutputStream.class },
        Void.TYPE);
    readObjectMethod = getPrivateMethod(cl, "readObject",
        new Class<?>[] { ObjectInputStream.class },
        Void.TYPE);
    readObjectNoDataMethod = getPrivateMethod(
        cl, "readObjectNoData", null, Void.TYPE);
    hasWriteObjectData = (writeObjectMethod != null);
}

从上面这段源码中可以看出,在序列化(反序列化)的时候,ObjectOutputStream(ObjectInputStream)会寻找目标类中的私有的writeObject(readObject)方法,赋值给变量writeObjectMethod(readObjectMethod)。

下面再来看两段源码:

ObjectStreamClass类中的一个判断方法

boolean hasWriteObjectMethod() {
    requireInitialized();
    return (writeObjectMethod != null);
}

ObjectOutputStream中的最终序列化对象的方法

private void writeSerialData(Object obj, ObjectStreamClass desc)
    throws IOException
{
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        if (slotDesc.hasWriteObjectMethod()) {
            ...
            slotDesc.invokeWriteObject(obj, this);
            ...
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}

通过上面这两段代码可以知道,如果writeObjectMethod != null(目标类中定义了私有的writeObject方法),那么将调用目标类中的writeObject方法,如果如果writeObjectMethod == null,那么将调用默认的defaultWriteFields方法来读取目标类中的属性。

readObject的调用逻辑和writeObject一样。

总结一下,如果目标类中没有定义私有的writeObject或readObject方法,那么序列化和反序列化的时候将调用默认的方法来根据目标类中的属性来进行序列化和反序列化,而如果目标类中定义了私有的writeObject或readObject方法,那么序列化和反序列化的时候将调用目标类指定的writeObject或readObject方法来实现。

4.默认方法readResolve

readResolve方法和writeObject、readObject方法的实现过程不太一样,但是原理类似,也是可以在目标类中定义一个私有的readResolve方法,然后再反序列化的时候会被调用到。

通过下面的案例我们来看一下readObject方法和readResolve方法被调用的顺序:

目标类
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    private String name;
    private String password;

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        System.out.println("readObject");
    }

    private Object readResolve() {
        System.out.println("readResolve");
        return new User(name, password);
    }
    ...
}
测试类
@Test
public void testReadObject() throws Exception {
    File file = new File("e:/user.txt");
   ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
    User user = (User) inputStream.readObject();
}
控制台输出
readObject
readResolve

可以看出readResolve方法会在readObject之后调用,所以反序列化的时候readResolve方法会覆盖掉readObject方法的修改。

关于readResolve方法的使用场景,在我前几天写的关于readResolve方法的使用场景,在我前几天写的单例模式文章中有过分享,这里就不赘述了。文章中有过分享,这里就不赘述了。

文章中有过分享,这里就不赘述了。

5.Externalizable接口

这里先来看一下Externalizable接口的源码

public interface Externalizable extends java.io.Serializable {
    /**
     * by calling the methods of DataOutput for its primitive values or
     * calling the writeObject method of ObjectOutput for objects, strings, and arrays.
     */
    void writeExternal(ObjectOutput out) throws IOException;

    /**
     * The object implements the readExternal method to restore its
     * contents by calling the methods of DataInput for primitive
     * types and readObject for objects, strings and arrays.  The
     * readExternal method must read the values in the same sequence
     * and with the same types as were written by writeExternal.
     */
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

通过源码我们可以得出结论:
- Externalizable接口继承了Serializable接口,所以实现Externalizable接口也能实现序列化和反序列化。
- Externalizable接口中定义了writeExternal和readExternal两个抽象方法,通过注释,可以看出这两个方法其实对应Serializable接口的writeObject和readObject方法。

所以Externalizable接口被设计出来的目的就是为了抽象出writeObject和readObject这两个方法,但是目前这个接口使用的并不多。

6.总结

最后再总结一下java的序列化
- 必须实现Serializable接口或Externalizable接口的类才能进行序列化
- transient和static修饰符修饰的成员变量不会参与序列化和反序列化
- 反序列化对象和序列化前的对象的全类名和serialVersionUID必须一致
- 在目标类中添加私有的writeObject和readObject方法可以覆盖默认的序列化和反序列化方法
- 在目标类中添加私有的readResolve可以最终修改反序列化回来的对象,可用于单例模式防止序列化导致生成第二个对象的问题


喜欢这篇文章的朋友,欢迎扫描下图关注公众号lebronchen,第一时间收到更新内容。
这里写图片描述

猜你喜欢

转载自blog.csdn.net/Leon_cx/article/details/81517603
今日推荐