Java 序列化与反序列化 - chuwuwang/ReadingNote GitHub Wiki

序列化、反序列化概念及其使用场景

序列化和反序列化

序列化(Serialization)是将对象的状态信息转化为可以存储或者传输的形式的过程,一般将一个对象存储到一个储存媒介,例如档案或记忆体缓冲等。在网络传输过程中,可以是字节或者XML等格式。而字节或者XML格式的可以还原成完全相等的对象,这个相反的过程又称为反序列化。

Java对象的序列化和反序列化

在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用此对象。但是,我们创建出来的这些对象都存在于JVM中的堆(heap)内存中,只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止,这些对象也就随之消失。

但是在真实的应用场景中,我们需要将这些对象持久化下来,并且在需要的时候将对象重新读取出来,Java的序列化可以帮助我们实现该功能。

对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以将对象的状态信息保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式转换成对象,对象的序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。

使用场景

所有可在网络上传输的对象都必须是可序列化的,比如RMI(remote method invoke 即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错。所有需要保存到磁盘的Java对象都必须是可序列化的。

实现序列化的方式

实现Serializable接口

当试图对一个对象进行序列化时,如果遇到一个没有实现java.io.Serialization接口的对象时,将抛出NotSerializationException异常。

java.io.NotSerializableException: ex.serializable.User
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at ex.serializable.Main.serialize(Main.java:41)
    at ex.serializable.Main.main(Main.java:33)

追踪ObjectOutputStream的writeObject()方法

public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        // 最终调用writeObject0()方法
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}
    
private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        // handle previously written and non-replaceable objects
        int h;
        if ((obj = subs.lookup(obj)) == null) {
            writeNull();
            return;
        } else if (!unshared && (h = handles.lookup(obj)) != -1) {
            writeHandle(h);
            return;
        } else if (obj instanceof Class) {
            writeClass((Class) obj, unshared);
            return;
        } else if (obj instanceof ObjectStreamClass) {
            writeClassDesc((ObjectStreamClass) obj, unshared);
            return;
        }

        // check for replacement object
        Object orig = obj;
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;
        for (;;) {
            // REMIND: skip this check for strings/arrays?
            Class<?> repCl;
            desc = ObjectStreamClass.lookup(cl, true);
            if (!desc.hasWriteReplaceMethod() ||
                (obj = desc.invokeWriteReplace(obj)) == null ||
                (repCl = obj.getClass()) == cl)
            {
                break;
            }
            cl = repCl;
        }
        if (enableReplace) {
            Object rep = replaceObject(obj);
            if (rep != obj && rep != null) {
                cl = rep.getClass();
                desc = ObjectStreamClass.lookup(cl, true);
            }
            obj = rep;
        }

        // if object replaced, run through original checks a second time
        if (obj != orig) {
            subs.assign(orig, obj);
            if (obj == null) {
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                writeHandle(h);
                return;
            } else if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof ObjectStreamClass) {
                writeClassDesc((ObjectStreamClass) obj, unshared);
                return;
            }
        }
        // 判断各种数据类型,如果不是String,数组,枚举,Serializable类型,就会抛出NotSerializableException异常
        // remaining cases
        if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

当然,ObjectInputStream对象也是类似,只不过writeObject()方法变成了readObject()方法。

注意:

  • 反序列化并不会调用对象的构造方法。反序列的对象是由JVM自己生成的对象,不通过构造方法生成。
  • 如果一个可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的。否则,会导致此类不能序列化。

实现Externalizable接口

  • Externalizable接口继承自Serializable接口
  • Externalizable接口允许我们自定义对象属性的序列化
  • 实现Externalizable接口必须重写writeExternal()和readExternal()方法

注意:

  • Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的。
  • 必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。

特殊方法/字段

transient关键字

使用transient修饰的属性,Java在序列化时,会忽略掉此字段。所以反序列化出的对象,被transient修饰的属性是默认值。

对于引用类型,值是null。基本类型,int值是0,boolean值是false。

static成员变量

static修饰的成员变量属于类全局属性,其值在JDK1.8存储于元数据区(1.8以前叫方法区),不属于对象范围,所以不会被序列化。

writeObject()和readObject()

private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;

通过重写writeObject()与readObject()方法,可以自己选择哪些属性需要序列化,哪些属性不需要。如果writeObject()使用某种规则序列化,则相应的readObject()需要相反的规则反序列化,以便能正确反序列化出对象。

这样我们可以进行控制序列化的方式,或者对序列化数据进行编码加密等。

当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。例如,使用不同类接收反序列化对象,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。

writeReplace()和readResolve()

private Object writeReplace() throws ObjectStreamException;
private Object readResolve() throws ObjectStreamException;

writeReplace():在序列化时,会先调用此方法,再调用writeObject()方法。此方法可将任意对象代替目标序列化对象。

readResolve():反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject()后调用。

readResolve常用来反序列单例类,保证单例类的唯一性。

注意:

readResolve()与writeReplace()的访问修饰符可以是private、protected、public,如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。通常建议对于final修饰的类重写readResolve方法没有问题。否则,重写readResolve()使用private修饰。

序列化版本号serialVersionUID

private static final long serialVersionUID = 2313214123L;

Java序列化提供了一个private static final long serialVersionUID的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

当我们新增一个类实现Serializable接口时,建议我们为其新增一个serialVersionUID。因为假设我们没有声明serialVersionUID,那么后面假设我们修改了该类的接口(新增字段)时,当我们再次反序列化时,就会报错。因为Java会拿编译器根据类信息自动生成一个id1和反序列化得到的id2进行比较,如果类有改动,id2和id1肯定不一致,就会会报InvalidClassException异常。

什么情况下需要修改serialVersionUID呢?分三种情况。

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号。
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号。
  • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值。如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

QA

https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf

同一对象序列化多次,会将这个对象序列化多次吗?

Java序列化同一对象,并不会将此对象序列化多次得到多个对象。

  • Java序列化算法
    • 所有保存到磁盘的对象都有一个序列化编码号
    • 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
    • 如果此对象已经序列化过,则直接输出编号即可。

Java序列化算法潜在的问题

由于Java序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。

序列化注意点

  • 反序列化时必须有序列化对象的class文件。
  • 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  • 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。

Code

// 序列化方法
public static <T extends Serializable> void serialize(String path, T object) {
    try (
            FileOutputStream fos = new FileOutputStream(path);
            ObjectOutputStream oos = new ObjectOutputStream(fos)
    ) {
        oos.writeObject(object);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 反序列化方法
public static <T extends Serializable> T deserialize(String path) {
    try (
            FileInputStream fis = new FileInputStream(path);
            ObjectInputStream ois = new ObjectInputStream(fis)
    ) {
        return (T) ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

// 序列化实现深度克隆
public static <T extends Serializable> T cloneObject(T object) {
    byte[] bytes = null;
    try (
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos)
    ) {
        oos.writeObject(object);
        bytes = bos.toByteArray();
    } catch (Exception e) {
        e.printStackTrace();
    }
    if (bytes != null) {
        try (
                ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
                ObjectInputStream ois = new ObjectInputStream(bis)
        ) {
            return (T) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return null;
}