一 概述
Java序列化是指把Java对象转换为Java对象的过程。字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。
序列化分为两大部分:序列化和反序列化 。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。
常见序列化反序列化协议
1. XML&SOAP
XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议
JSON
Protobuf
二 序列化实现
注意:只有实现了Serializable或者externalizable接口的类的对象才能被序列化为字节序列(否则抛出异常)
Serializable是一个空接口(所以只是用来标记)
1 2 public interface Serializable {}
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。
Serializable 接口的基本使用
通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
Serializable接口特点
1. 序列化类属性没用实现Serialzable 那么在序列化时就回报错
1 2 3 4 5 6 7 8 9 10 public class Person implements Serializable { private String name; private int age; /* 其中 Color 类也需要是实现序列化接口 */ private Color color; //若不实现序列化接口,则Person在序列化时也会报错 }
在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造 函数来重新创建对象。
Animal 是父类,它没有实现 Serilizable 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Animal { private String color; public Animal () { System.out.println("调用 Animal 无参构造" ); } public Animal (String color) { this .color = color; System.out.println("调用 Animal 有 color 参数的构造" ); } @Override public String toString () { return "Animal{" + "color='" + color + '\'' + '}' ; } }
BlackCat 是 Animal 的子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class BlackCat extends Animal implements Serializable { private static final long serialVersionUID = 1L ; private String name; public BlackCat () { super (); System.out.println("调用黑猫的无参构造" ); } public BlackCat (String color, String name) { super (color); this .name = name; System.out.println("调用黑猫有 color 参数的构造" ); } @Override public String toString () { return "BlackCat{" + "name='" + name + '\'' +super .toString() +'\'' + '}' ; } }
SuperMain 测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class SuperMain { private static final String FILE_PATH = "./super.bin" ; public static void main (String[] args) throws Exception { serializeAnimal(); deserializeAnimal(); } private static void serializeAnimal () throws Exception { BlackCat black = new BlackCat ("black" , "我是黑猫" ); System.out.println("序列化前:" +black.toString()); System.out.println("=================开始序列化================" ); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream (FILE_PATH)); oos.writeObject(black); oos.flush(); oos.close(); } private static void deserializeAnimal () throws Exception { System.out.println("=================开始反序列化================" ); ObjectInputStream ois = new ObjectInputStream (new FileInputStream (FILE_PATH)); BlackCat black = (BlackCat) ois.readObject(); ois.close(); System.out.println(black); } }
输出结果
1 2 3 4 5 6 7 调用 Animal 有 color 参数的构造 调用黑猫有 color 参数的构造 序列化前:BlackCat{name='我是黑猫' Animal{color='black' }'} =================开始序列化================ =================开始反序列化================ 调用 Animal 无参构造 BlackCat{name=' 我是黑猫'Animal{color=' null '}' }
从上面的执行结果来看,如果要序列化的对象的父类 Animal 没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。
实现Serializable接口的子类也是可以被序列化的
静态成员变量不能被序列化
transient标识的对象成员变量不参与序列化(在属性前加关键字transient ,序列化时,就不会序列化到指定位置)
操作文件流的类
1.
ObjectOutputStream代表对象输出流:
它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中
ObjectInputStream代表对象输入流:
它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
三 序列化ID
在进行序列化时,有一个serialVersionUID 。这就是序列化ID
1 private static final long serialVersionUID = 1L ;
这个序列化ID起着关键的作用,它决定着是否能够成功反序列化!java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的 ,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。
即序列化ID是为了保证成功进行反序列化
如何生成UID
1.
使用 AS plugin 插件就可以生成
2.
在JDK中,可以利用 JDK 的 bin 目录下的 serialver 工具产生这个serialVersionUID,对于 Student.class,执行命令:serialver com.example.seriable.Student
1 2 ➜ classes git:(master) ✗ /Library/Java/JavaVirtualMachines/jdk1.8 .0_111 .jdk/Contents/Home/bin/serialver com.example.seriable.Student com.example.seriable.Student: private static final long serialVersionUID = -6840182814363029482L ;
使用 AS plugin 的方式应该底层也是使用到这个 JDK 工具去生成的 SerialVersionUID 值,测试结果来看这两个生成的值是一样的。
serialVersionUID 的兼容性问题是什么?
具体的兼容性问题如下:
1 2 java.io.InvalidClassException: com.example.seriable.Student; local class incompatible : stream classdesc serialVersionUID = -926212341182608815 , local class serialVersionUID = -6840182814363029482
关于这个异常,它是属于兼容问题异常,是发生在反序列化阶段,检测到 serialVersionUID 不一致导致的。具体的分析如下:
1 2 序列化时使用的 serialVersionUID = -926212341182608815L ,如果期间属性被修改了,如果 serialVersionUID 发生改变 -6840182814363029482 ,那么 反序列化时就会出现类不兼容问题。
四 Java的序列化步骤与数据结构分析
序列化算法一般会按步骤做如下事情:
将对象实例相关的类元数据输出。
递归地输出类的超类描述直到不再有超类。
类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
从上至下递归输出实例的数据
writeObject原理分析
ObjectOutputStream 构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ; } }
A bout:用于写入一些类元数据还有对象中基本数据类型的值,在下面会分析。
B enableOverride :false 表示不支持重写序列化过程,如果为 true ,那么需要重写 writeObjectOverride 方法。这个一般不用管它。
C writeStreamHeader() 写入头信息,具体看下面分析。
ObjectOUtStream#writeObject(obj);
1 2 3 4 protected void writeStreamHeader () throws IOException { bout.writeShort(STREAM_MAGIC); bout.writeShort(STREAM_VERSION); }
①STREAM_MAGIC 声明使用了序列化协议,bout 就是一个流,将对应的头数据写入该流中
②STREAM_VERSION 指定序列化协议版本
ObjectOUtStream#writeObject(obj);
上面是 ObjectOutStream 构造中做的事,下面来看看具体 writeObject 方法内部做了什么事?
1 2 3 4 5 6 7 8 9 10 11 public final void writeObject (Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return ; } try { writeObject0(obj, false ); } catch (IOException ex) { ... } }
ObjectOutStream#writeObject0()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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); } 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 实例。
② 根据 obj 的类型去执行序列化操作,如果不符合序列化要求,那么会③位置抛出 NotSerializableException
异常。
ObjectOutputStream#writeOrdinaryObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 方法会回调,在这里就可以愉快地编写需要序列化的数据拉。
④ writeSerialData 在没有实现 Externalizable 接口时,就执行这个方法
ObjectOutputstream#writeSerialData
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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); } } }
① desc.getClassDataLayout 会返回 ObjectStreamClass.ClassDataSlot[] ,我们来看看 ClassDataSlot 类,可以看到它是封装了 ObjectStreamClass 而已,所以我们就简单的认为 ① 这一步就是用于返回序列化对象及其父类的 ClassDataSlot[] 数组,我们可以从 ClassDataSlot 中获取对应 ObjectStreamClass 描述信息。
② 开始遍历返回的数组,slotDesc 这个我们就简单将其看成对一个对象的描述吧。hasWriteObjectMethod 表示的是什么呢?这个其实就是你要序列化这个对象是否有 writeObject 这个 private 方法,注意哦,这个方法并不是任何接口的方法,而是我们手动写的,读者可以参考 ArrayList 代码,它内部就有这个方法。那么这个方法的作用是什么呢?这个方法我们在上面也演示过具体的使用,它就是用于自定义序列化过程的,读者可以返回到上面看看如果使用这个 writeObject 实现自定义序列化过程的。注意:其实这个过程不像实现 Externalizable 接口那样,自己完全去自定义序列化数据。
③ defaultWriteFields 这个方法就是 JVM 自动帮我们序列化了
这个方法主要分为以下两步
① 写入基本数据类型的数据
②写入引用数据类型的数据,这里最终又调用到了 writeObject0() 方法,读者可以返回到上面去看看具体的实现。
readObject 原理分析
从流中读取类的描述信息 ObjectStreamClass 实例,通过这个对象就可以创建出序列化的对象。
1 2 3 4 5 6 7 8 9 10 11 ObjectStreamClass desc = readClassDesc(false );... Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null ; } catch (Exception ex) { throw (IOException) new InvalidClassException ( desc.forClass().getName(), "unable to create instance" ).initCause(ex); }
读取该对象及其对象的父类的 ObjectStreamClass信息
1 ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
然后遍历得到每一个 ObjectStreamClass 对象,将对应的属性值赋值给需要反序列化的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private void defaultReadFields (Object obj, ObjectStreamClass desc) throws IOException { Class<?> cl = desc.forClass(); if (cl != null && obj != null && !cl.isInstance(obj)) { throw new ClassCastException (); } int primDataSize = desc.getPrimDataSize(); if (primVals == null || primVals.length < primDataSize) { primVals = new byte [primDataSize]; } bin.readFully(primVals, 0 , primDataSize, false ); if (obj != null ) { desc.setPrimFieldValues(obj, primVals); } int objHandle = passHandle; ObjectStreamField[] fields = desc.getFields(false ); Object[] objVals = new Object [desc.getNumObjFields()]; int numPrimFields = fields.length - objVals.length; for (int i = 0 ; i < objVals.length; i++) { ObjectStreamField f = fields[numPrimFields + i]; objVals[i] = readObject0(f.isUnshared()); if (f.getField() != null ) { handles.markDependency(objHandle, passHandle); } } if (obj != null ) { desc.setObjFieldValues(obj, objVals); } passHandle = objHandle; }