通常我们使用Java的序列化与反序列化时,只需要将类实现 Serializable
接口即可,剩下的事情就交给了jdk。今天我们就来探究一下,Java序列化是怎么实现的,然后探讨一下几个常见的集合类,他们是如何处理序列化带来的问题的。
Serializable
接口就可以了。 final
成员变量 serialVersionUID
。 transient
关键字的, static变量也不会被序列化。 接下来我们就带着问题,在源码中找寻答案吧。
先看 Serializable
接口,源码很简单,一个空的接口,没有方法也没有成员变量。但是注释非常详细,很清楚的描述了 Serializable
怎么用、能做什么,很值得一看,我们捡几个重点的翻译一下,
/** * Serializability of a class is enabled by the class implementing the * java.io.Serializable interface. Classes that do not implement this * interface will not have any of their state serialized or * deserialized. All subtypes of a serializable class are themselves * serializable. The serialization interface has no methods or fields * and serves only to identify the semantics of being serializable. */ 复制代码
类的可序列化性通过实现 java.io.Serializable
接口开启。未实现序列化接口的类不能序列化,所有实现了序列化的子类都可以被序列化。 Serializable
接口没有方法和属性,只是一个识别类可被序列化的标志。
/** * Classes that require special handling during the serialization and * deserialization process must implement special methods with these exact * signatures: * * <PRE> * private void writeObject(java.io.ObjectOutputStream out) * throws IOException * private void readObject(java.io.ObjectInputStream in) * throws IOException, ClassNotFoundException; * private void readObjectNoData() * throws ObjectStreamException; * </PRE> */ 复制代码
在序列化过程中,如果类想要做一些特殊处理,可以通过实现以下方法 writeObject()
, readObject()
, readObjectNoData()
,其中,
readObject() readObject() writeReplace() readResolve()
//我们要序列化对象的方法实现一般都是在这个函数中 public final void writeObject(Object obj) throws IOException { ... try { //写入的具体实现方法 writeObject0(obj, false); } catch (IOException ex) { ... throw ex; } } private void writeObject0(Object obj, boolean unshared) throws IOException { ...省略 Object orig = obj; Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { // REMIND: skip this check for strings/arrays? Class<?> repCl; //获取到ObjectStreamClass,这个类很重要 //在它的构造函数初始化时会调用获取类属性的函数 //最终会调用getDefaultSerialFields这个方法 //在其中通过flag过滤掉类的某一个为transient或static的属性(解释了问题3) desc = ObjectStreamClass.lookup(cl, true); if (!desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { break; } cl = repCl; } //其中主要的写入逻辑如下 //String, Array, Enum本身处理了序列化 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); //重点在这里,通过`instanceof`判断对象是否为`Serializable` //这也就是普通自己定义的类如果没有实现`Serializable` //在序列化的时候会抛出异常的原因(解释了问题1) } 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()); } } ... } private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { ... try { desc.checkSerialize(); //写入二进制文件,普通对象开头的魔数0x73 bout.writeByte(TC_OBJECT); //写入对应的类的描述符,见底下源码 writeClassDesc(desc, false); handles.assign(unshared ? null : obj); if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } private void writeClassDesc(ObjectStreamClass desc, boolean unshared) throws IOException { //句柄 int handle; //null描述 if (desc == null) { writeNull(); //类对象引用句柄 //如果流中已经存在句柄,则直接拿来用,提高序列化效率 } else if (!unshared && (handle = handles.lookup(desc)) != -1) { writeHandle(handle); //动态代理类描述符 } else if (desc.isProxy()) { writeProxyDesc(desc, unshared); //普通类描述符 } else { //该方法会调用desc.writeNonProxy(this)如下 writeNonProxyDesc(desc, unshared); } } void writeNonProxy(ObjectOutputStream out) throws IOException { out.writeUTF(name); //写入serialVersionUID out.writeLong(getSerialVersionUID()); ... } public long getSerialVersionUID() { // 如果没有定义serialVersionUID // 序列化机制就会调用一个函数根据类内部的属性等计算出一个hash值 // 这也是为什么不推荐序列化的时候不自己定义serialVersionUID的原因 // 因为这个hash值是根据类的变化而变化的 // 如果你新增了一个属性,那么之前那些被序列化后的二进制文件将不能反序列化回来,Java会抛出异常 // (解释了问题2) if (suid == null) { suid = AccessController.doPrivileged( new PrivilegedAction<Long>() { public Long run() { return computeDefaultSUID(cl); } } ); } //已经定义了SerialVersionUID,直接获取 return suid.longValue(); } //分析到这里,要插一个我对序列化后二进制文件的一点个人见解,见下面 复制代码
如果我们要序列化一个 List<PhoneItem>
, 其中 PhoneItem
如下,
class PhoneItem implements Serializable { String phoneNumber; } 复制代码
构造List的代码省略,假设我们序列化了一个 size
为5的 List
,查看二进制文件大概如下所示,
7372 xxxx xxxx 7371 xxxx xxxx 7371 xxxx xxxx 7371 xxxx xxxx 7371 xxxx xxxx 复制代码
通过刚才的源码解读,开头的魔数0x73表示普通对象,72表示类的描述符号,71表示类描述符为引用类型。管中窥豹可知一点薄见,在解析二进制文件的时候,就是通过 匹配魔数 (magic number) 开头方式,从而转换成Java对象的。当在序列化过程中,如果流中已经有同样的对象,那么之后的序列化可以直接获取该类对象句柄,变为引用类型,从而提高序列化效率。
//通过writeSerialData调用走到真正解析类的方法中,有没有复写writeObject处理的逻辑不太一样 //这里以默认没有复写writeObject为例,最后会调用defaultWriteFields方法 private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException { ... int primDataSize = desc.getPrimDataSize(); if (primVals == null || primVals.length < primDataSize) { primVals = new byte[primDataSize]; } desc.getPrimFieldValues(obj, primVals); //写入属性大小 bout.write(primVals, 0, primDataSize, false); ObjectStreamField[] fields = desc.getFields(false); Object[] objVals = new Object[desc.getNumObjFields()]; int numPrimFields = fields.length - objVals.length; desc.getObjFieldValues(obj, objVals); for (int i = 0; i < objVals.length; i++) { ... try { //遍历写入属性类型和属性大小 writeObject0(objVals[i], fields[numPrimFields + i].isUnshared()); } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } } 复制代码
由于反序列化过程和序列化过程类似,这里不再赘述。
Java要求被反序列化后的对象要与被序列化之前的对象保持一致,但因为hashmap的key是通过hash计算的。反序列化后计算得到的值可能不一致(反序列化在不同的jvm环境下执行)。所以HashMap需要重写序列化实现的过程,避免出现这种不一致的情况。
具体操作是将要自定义处理的属性定义为 transient
,然后复写 writeObject
,在其中做特殊处理
private void writeObject(java.io.ObjectOutputStream s) throws IOException { int buckets = capacity(); // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); //写入hash桶的容量 s.writeInt(buckets); //写入k-v的大小 s.writeInt(size); //遍历写入不为空的k-v internalWriteEntries(s); } 复制代码
因为在ArrayList中的数组容量基本上都会比实际的元素的数大, 为了避免序列化没有元素的数组而重写 writeObject
和 readObject
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ ... s.defaultWriteObject(); // 写入arraylist当前的大小 s.writeInt(size); // 按照相同顺序写入元素 for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } ... } 复制代码