目录
序列化的意义
Java 平台允许我们在内存中创建可复用的Java 对象,但一般情况下,只有当JVM 处于运行时,这些对象才可能存在,即这些对象的生命周期不会比JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java 对象序列化就能够帮助我们实现该功能。
简单来说,序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化。反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化。
序列化面临的挑战
评价一个序列化算法优劣的两个重要指标是:
一. 序列化以后的数据大小;
二. 序列化操作本身的速度及系统资源开销(CPU、内存)。
Java 语言本身提供了对象序列化机制,也是Java 语言本身最重要的底层机制之一。在Java 中,只要一个类实现了java.io.Serializable 接口,那么它就可以被序列化。
基于JDK 序列化方式实现
JDK 提供了Java 对象的序列化方式,主要通过输出流java.io.ObjectOutputStream 和输入流java.io.ObjectInputStream来实现。其中,被序列化的对象需要实现java.io.Serializable 接口。
import java.io.*;
import java.util.Date;
public class ObjectSaver {
public static void main(String[] args) throws Exception {
//序列化对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\temp.txt"));
Customer customer = new Customer("张三", 24);
out.writeObject("你好!"); //写入字面值常量
out.writeObject(new Date()); //写入匿名Date对象
out.writeObject(customer); //写入customer对象
out.close();
//反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\temp.txt"));
System.out.println("obj1 " + (String) in.readObject()); //读取字面值常量
System.out.println("obj2 " + (Date) in.readObject()); //读取匿名Date对象
Customer obj3 = (Customer) in.readObject(); //读取customer对象
System.out.println("obj3 " + obj3);
in.close();
}
}
class Customer implements Serializable {
private String name;
private int age;
public Customer(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "name=" + name + ", age=" + age;
}
}
序列化的高阶认识
serialVersionUID 的作用
Java 的序列化机制是通过判断类的serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。
当实现java.io.Serializable 接口的类没有显式地定义一个serialVersionUID 变量时候,Java 序列化机制会根据编译的Class 自动生成一个唯一的serialVersionUID,这种情况下,如果Class 文件的类名、方法名、属性名等没有发生变化,只是增加空格、换行、注释等,就算再编译多次,serialVersionUID 也不会变化。
静态变量序列化
序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。
父类的序列化
一. 当父类没有实现Serializable 接口,子类继承该父类并且实现了Serializable 接口,在反序列化该子类后,将无法获取到父类的属性值的;
二. 当父类实现了Serializable 接口,子类将自动支持序列化功能,不需要再显式实现Serializable 接口;
三. 当一个对象的实例变量引用了其他对象,序列化该对象时也会把引用对象进行序列化,但是前提是该引用对象必须实现了Serializable 接口。
Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,Transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
绕开Transient 机制实现自定义序列化
/**
* ObjectOutputStream使用了反射来寻找是否声明了该方法
* 实现自定义的序列化操作
*/
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
/**
* ObjectInputStream使用了反射来寻找是否声明了该方法
* 实现自定义的反序列化操作
*/
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
/**
* ObjectInputStream使用了反射来寻找是否声明了该方法
* 在使用单例时,如果使用的不是枚举的实现形式,为了保证反序列化出来后的对象,不会破坏单例的情况
* 使用该方法返回原有实例
*/
private Object readResolve()
以HashMap为例,为何HashMap要自己实现writeObject和readObject方法?
由于大部分情况序列化和反序列化所在的机器是不同的,而序列化和反序列化的一个最基本的要求就是,反序列化之后的对象与序列化之前的对象是一致的。HashMap中,由于Node 的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key 在不同的JVM实现中计算得出的Hash值可能是不同的。 Hash值不同就有可能导致一个HashMap对象的反序列化结果与序列化之前的结果不一致。
为了避免这个问题,HashMap采用了下面的方式来解决:
一. 将可能会造成数据不一致的元素使用transient关键字修饰,从而避免JDK中默认序列化方法对该对象的序列化操作。不序列化的包括:Node<K,V>[] table,Set<Map.Entry<K,V>> entrySet,int size,int modCount。
二. 自己实现writeObject和readObject方法。在序列化的时候不会将保存数据的数组序列化,而是将元素个数以及每个元素的Key和Value都进行序列化。在反序列化的时候,重新计算Key和Value的位置,重新填充一个数组,从而保证序列化和反序列化结果的一致性。
常见的序列化技术
JAVA序列化框架
使用JAVA 进行序列化有他的优点,也有他的缺点。
优点:JAVA 语言本身提供,使用比较方便和简单。
缺点:不支持跨语言处理、 性能相对不是很好,序列化以后产生的数据相对较大。
XML 序列化框架
XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大,而且效率不高,适用于对性能不高,而且QPS 较低的企业级内部系统之间的数据交换的场景,同时XML 又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的Webservice,就是采用XML 格式对数据进行序列化的。
JSON 序列化框架
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于XML 来说,JSON 的字节流更小,而且可读性也非常好。现在JSON 数据格式在企业运用是最普遍的。
JSON 序列化常用的开源工具有很多:
- Jackson (https://github.com/FasterXML/jackson)
- 阿里开源的FastJson (https://github.com/alibaba/fastjon)
- Google 的GSON (https://github.com/google/gson)
这几种json 序列化工具中,Jackson 与fastjson 要比GSON 的性能要好,但是Jackson、GSON 的稳定性要比Fastjson 好。而fastjson 的优势在于提供的api 非常容易使用。
Hessian 序列化框架
Hessian 是一个支持跨语言传输的二进制序列化协议,相对于Java 默认的序列化机制来说,Hessian 具有更好的性能和易用性,而且支持多种不同的语言。实际上Dubbo 采用的就是Hessian 序列化来实现,只不过Dubbo 对Hessian 进行了重构,性能更高。
Protobuf 序列化框架
Protobuf 是Google 的一种数据交换格式,它独立于语言、独立于平台。Google 提供了多种语言来实现,比如Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件。Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中。但是但是要使用Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器。
序列化技术的选型
考虑因素:
1. 序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能;
2. 序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间;
3. 序列化协议是否支持跨平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信需求,那么这个是必须要考虑的;
4. 可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新,这就要求我们采用的序列化协议基于良好的可扩展性/兼容性,比如在现有的序列化数据结构中新增一个业务字段,不会影响到现有的服务;
5. 技术的流行程度,越流行的技术意味着使用的公司多,技术解决方案也相对成熟;
6. 学习难度和易用性。
选型建议:
1. 对性能要求不高的场景,可以采用基于XML 的SOAP 协议;
2. 对性能和间接性有比较高要求的场景,那么Hessian、Protobuf、Thrift、Avro 都可以。
3. 基于前后端分离,或者独立的对外的api 服务,选用JSON 是比较好的,对于调试、可读性都很不错;
4. Avro 设计理念偏于动态类型语言,那么这类的场景使用Avro 是可以的。