序列化算法一般会按步骤做如下事情:
1、将对象实例相关的类元数据输出。
2、递归地输出类的超类描述直到不再有超类。
3、类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
构造方法
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);//①
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;//②
writeStreamHeader();//③
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
①bout:用于写入一些类元数据还有对象中基本数据类型的值,在下面会分析。
②enableOverride :false 表示不支持重写序列化过程,如果为 true ,那么需要重写 writeObjectOverride 方法。这个一般不用管它。
③writeStreamHeader() 写入头信息
writeObject
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {//一般不会走这里,因为在 ObjectOutputStream 构造设置为 false 了
writeObjectOverride(obj);
return;
}
try {//代码会执行这里
writeObject0(obj, false);
} catch (IOException ex) {
...
}
}
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
...
try {
Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
//①
desc = ObjectStreamClass.lookup(cl, true);
...
//②
if (obj instanceof Class) {
writeClass((Class) obj, unshared);
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
// END Android-changed: Make Class and ObjectStreamClass replaceable.
} else 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());
}
}
}
...
}
① lookup 函数用于查找当前类的 ObjectStreamClass ,它是用于描述一个类的结构信息的,通过它就可以获取对象及其对象属性的相关信息,并且它内部持有该对象的父类的 ObjectStreamClass 实例。其内部大量使用了反射,读者可以去看看这个类的源码。下面看看它的构造函数
private ObjectStreamClass(final Class<?> cl) {
this.cl = cl;
name = cl.getName();
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl);
Class<?> superCl = cl.getSuperclass();
//superDesc 表示需要序列化对象的父类的 ObjectStreamClass,如果为空,则调用 lookUp 查找
superDesc = (superCl != null) ? lookup(superCl, false) : null;
//localDesc 表示自己
localDesc = this;
...
}
② 根据 obj 的类型去执行序列化操作,如果不符合序列化要求,那么会③位置抛出 NotSerializableException 异常。
③ writeOrdinaryObject
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
...
try {
desc.checkSerialize();
//①
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();
}
}
}
①写入类的元数据,TC_OBJECT. 声明这是一个新的对象,如果写入的是一个 String 类型的数据,那么就需要 TC_STRING 这个标识。
②writeClassDesc 方法主要作用就是自上而下(从父类写到子类,注意只会遍历那些实现了序列化接口的类)写入描述信息。该方法内部会不断的递归调用,我们只需要关系这个方法是写入描述信息就好了,读者可以查阅一下源码。
从这里可以知道,序列化过程需要额外的写入很多数据,例如描述信息,类数据等,因此序列化后占用的空间肯定会更大。
③ desc.isExternalizable() 判断需要序列化的对象是否实现了 Externalizable 接口,这个在上面已经演示过怎么使用的,在序列化过程就是在这个地方进行判断的。如果有,那么序列化的过程就会由程序员自己控制了哦,writeExternalData 方法会回调,在这里就可以愉快地编写需要序列化的数据拉。
Externalizable
public interface Externalizable extends Serializable {
//将要序列化的对象属性通过 var1.wrietXxx() 写入到序列化流中
void writeExternal(ObjectOutput var1) throws IOException;
//将要反序列化的对象属性通过 var1.readXxx() 读出来
void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException;
}
④ writeSerialData
在没有实现 Externalizable 接口时,就执行这个方法
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()) {//②
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
defaultWriteFields(obj, slotDesc);//③
}
}
}
1、desc.getClassDataLayout 会返回 ObjectStreamClass.ClassDataSlot[] ,我们来看看 ClassDataSlot 类,可以看到它是封装了 ObjectStreamClass 而已,所以我们就简单的认为 ① 这一步就是用于返回序列化对象及其父类的 ClassDataSlot[] 数组,我们可以从 ClassDataSlot 中获取对应 ObjectStreamClass 描述信息。
2、开始遍历返回的数组,slotDesc 这个我们就简单将其看成对一个对象的描述吧。hasWriteObjectMethod 表示的是什么呢?这个其实就是你要序列化这个对象是否有 writeObject 这个 private 方法,注意哦,这个方法并不是任何接口的方法,而是我们手动写的,读者可以参考 ArrayList 代码,它内部就有这个方法。那么这个方法的作用是什么呢?这个方法我们在上面也演示过具体的使用,它就是用于自定义序列化过程的,读者可以返回到上面看看如果使用这个 writeObject 实现自定义序列化过程的。注意:其实这个过程不像实现 Externalizable 接口那样,自己完全去自定义序列化数据。
自定义 Object#writeObject (java.io.ObjectOutputStream)
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
//执行 JVM 默认的序列化操作
s.defaultWriteObject();
//手动序列化 arr 前面30个元素
for (int i = 0; i < 30; i++) {
s.writeObject(arr[i]);
}
}
3、defaultWriteFields 这个方法就是 JVM 自动帮我们序列化了,
备注
UUID 兼容性
当一个类的 UID 改变时,这个类的兼容性就会出现问题。
什么时候 UID 会改变呢?
- 手动去修改导致当前的 serialVersionUID 与序列化前的不一样。
- 我们根本就没有手动去写这个 serialVersionUID 常量,那么 JVM 内部会根据类结构去计算得到这个 serialVersionUID 值,在类结构发生改变时(属性增加,删除或者类型修改了)这种也是会导致 serialVersionUID 发生变化。
- 假如类结构没有发生改变,并且没有定义 serialVersionUID ,但是反序列和序列化操作的虚拟机不一样也可能导致计算出来的 serialVersionUID 不一样。