概况
对象序列化API提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象。“将一个对象编码成一个字节流”,称作将对象序列化(serializing);相反的处理过程被称为反序列化(deserializing)。
一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机被传递到另一台虚拟机上,或者被存储到磁盘上,供以后反序列化时用。序列化技术为远程通信提供了标准的线路级对象表示法,也为JavaBeans组件结构提供了标准的持久化数据格式。
序列化其实可以看成是一种机制,按照一定的格式将 Java 对象的某状态转成介质可接受的形式,以方便存储或传输。序列化时将 Java 对象相关的类信息、属性及属性值等等保存起来,反序列化时再根据这些信息构建出 Java 对象。
序列化的作用
- 对于远程调用,能方便对对象进行编码和解码,就像实现对象直接传输。
- 可以将对象持久化到介质中,就像实现对象直接存储。
- 提供一种简单又可扩展的对象保存恢复机制。
- 允许对象自定义外部存储的格式。
serialVersionUID 有什么用
在序列化操作时,经常会看到实现了 Serializable 接口的类会存在一个 serialVersionUID 属性,并且它是一个固定数值的静态变量。这个属性有什么作用?其实它主要用于验证版本一致性,每个类都拥有这么一个 ID,在序列化的时候会一起被写入流中,那么在反序列化的时候就被拿出来跟当前类的 serialVersionUID 值进行比较,两者相同则说明版本一致,可以序列化成功,而如果不同则序列化失败。
如果我们不显示定义 serialVersionUID 的话,这不代表不存在 serialVersionUID,而是由 JDK 帮我们生成,生成规则是会利用类名、类修饰符、接口名、字段、静态初始化信息、构造函数信息、方法名、方法修饰符、方法签名等组成的信息,经过 SHA 算法生成摘要即是最终的 serialVersionUID 值。
如何实现
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException
实现序列化
- 实现Serializable接口(或者父类实现)
- 只需要标注该接口就行,不需要实现该接口的方法
- 如果不在类中实现writeObject和readObject方法,那么将采用默认的序列化机制,否则将调用这两个方法实现序列化和反序列化。如果你实现了这两个方法的同时,又想利用Java的默认序列化,那么就在两个方法中分别调用defaultWriteObject和defaultReadObject方法,这种方式可用于对象加密。
- 可以用transient修饰不必序列化的属性。
- 实现Externalizable接口,并且实现该接口的writeExternal和readExternal方法,自己对序列化的内容进行控制。(或者父类实现)
在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
实现反序列化
- 对于实现Serializable接口的类,并不要求该类具有一个无参的构造方法, 因为在反序列化的过程中实际上是去其继承树上找到一个没有实现Serializable接口的父类(最终会找到Object), 然后构造该类的对象, 再逐层往下的去设置各个可以反序列化的属性(也就是没有被transient修饰的非静态属性).
- 对于实现Externalizable接口的类,需要其拥有无参构造器,因为对该类对象进行反序列化先走构造方法得到控对象,然后调用readExternal方法读取序列化文件中的内容给对应的属性赋值。
常见序列化协议: Java原生的序列化协议,Protobuf, Thrift, Hessian, Kryo等等,都是指Java的基于二进制的协议。还有XML, JSON这种常用格式
示例
public static void main(String[] args) throws IOException, ClassNotFoundException {
Set<String> hashSet = new HashSet<>();
hashSet.add("anthony");
hashSet.add("zero");
System.out.println(hashSet);
try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("E://set.obj"))) {
outputStream.writeObject(hashSet);
}
hashSet.clear();
System.out.println(hashSet);
try(ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("E://set.obj"))){
hashSet = (Set<String>) inputStream.readObject();
System.out.println(hashSet);
}
}
程序输出:
[zero, anthony]
[]
[zero, anthony]
HashSet(JDK1.8)源码writeObject方法源码:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
// Write out size
s.writeInt(map.size());
// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}
总结
- 序列化之后保存的是对象的信息
- 被声明为transient的属性不会被序列化,这就是transient关键字的作用
- 被声明为static的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是static修饰的变量是属于类的而不是属于对象的,因此序列化的时候不会序列化它
- 通过ObjectOutputStream和 ObjectInputStream对对象进行序列化及反序列化
- 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
- 要想将父类对象也序列化,就需要让父类也实现Serializable 接口。
- 在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
- 如果一个类想被序列化,需要实现Serializable接口。否则将抛出NotSerializableException异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于Enum、Array和Serializable类型其中的任何一种。
- 在类中增加writeObject 和 readObject 方法可以实现自定义序列化策略。
- 在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法时,会通过反射的方式调用类中的writeObject 和 readObject方法
调用栈:writeObject—>writeObject0—>writeOrdinaryObject—>writeSerialData—>invokeWriteObject(反射的方式调用)