Java序列化,看这篇就够了
1.什么是序列化
Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程:
- 序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。
- 反序列化:客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
2.序列化优点
一:对象序列化可以实现分布式对象。
主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
二:java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。
可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
三:序列化可以将内存中的类写入文件或数据库中。
比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。
总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。
四:对象、文件、数据,有许多不同的格式,很难统一传输和保存。
序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。
什么场景下会用到序列化
-
暂存大对象
-
Java对象需要持久化的时候
-
需要在网络,例如socket中传输Java对象。因为数据只能够以二进制的形式在网络中进行传输,因此当把对象通过网络发送出去之前需要先序列化成二进制数据,在接收 端读到二进制数据之后反序列化成Java对象
-
深度克隆(复制)
-
跨虚拟机通信
3.如何使用序列化
通过上面的介绍大家已经了解了什么是序列化,以及为什么要使用序列化。这一节我们一起来学习一下如何使用序列化。
首先我们要把准备要序列化类,实现 Serializabel接口,至于为什么要实现Serializabel接口,我们后面再详细介绍。
我们现在想要将Person序列化,Person类如下:
package com.wugongzi.day0112;
import java.io.Serializable;
/**
* add by wugongzi 2021/1/22
*/
public class Person implements Serializable {
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
序列化:
package com.wugongzi.day0112;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 序列化Person
* add by wugongzi 2021/1/22
*/
public class SerializationTest {
public static void main(String[] args) throws IOException {
Person p1 = new Person(1, "jack", 19);
Person p2 = new Person(2, "mary", 22);
List<Person> list = new ArrayList();
list.add(p1);
list.add(p2);
// 创建文件流
FileOutputStream fos = new FileOutputStream("/Users/File/person.txt");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(list);
os.close();
System.out.println("serialization success");
}
}
这里的person.txt就是序列化到本地的文件(打开会是乱码)
反序列化:
package com.wugongzi.day0112;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 反序列化Person
* add by wugongzi 2021/1/22
*/
public class DeserializationTest {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("/Users/File/person.txt");
ObjectInputStream is = new ObjectInputStream(fis);
Object obj = null;
List<Person> list = new ArrayList<>();
try {
list = (List<Person>)is.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
is.close();
//遍历list,输出
for (Person person:list){
System.out.println(person.toString());
}
}
}
输出结果:
Person{id=1, name='jack', age=19}
Person{id=2, name='mary', age=22}
序列化流程:
1)定义一个类,实现Serializable接口;
2)在程序代码中创建对象后,创建对象输出流ObjectOutputStream对象并在构造参数中指定流的输出目标(比如一个文件),通过objectOutputStream.writeObject(obj)把对象序列化并输出到流目标处;
3)在需要提取对象处:创建对象输入流ObjectInputStream对象并在构造参数中指定流的来源,然后通过readObject()方法获取对象,并通过强制类型转换赋值给类对象引用。
4.序列化原理
序列化算法会按步骤执行以下事情:
1)当前类描述的元数据输出为字节序列;【类定义描述、类中属性定义描述】
2)超类描述输出为字节序列;【如果超类还有超类,则依次递归,直至没有超类】
3)从最顶层超类往下,依次输出各类属性值描述,直至当前类对象属性值。
即:从下到上描述类定义,从上往下输出属性值。
5.为什么Java序列化要实现Serializable
首先我们来看一下Serializable接口源码:
package java.io;
public interface Serializable {
}
对,你没有看错,就是一个空接口。
既然这个接口里面什么东西都没有,那么实现这个接口意义何在呢?读到这里或许有很多同学会产生疑问:
一个空接口,里面啥都没有。为什么java设计的时候一定要实现Serializable才能序列化?不能去掉Serializable这个接口,让每个对象都能序列化吗?
比较有说服力的解释是:
总的就是说安全性问题,假如没有一个接口(即没有Serializable来标记是否可以序列化),让所有对象都可以序列化。那么所有对象通过序列化存储到硬盘上后,都可以在序列化得到的文件中看到属性对应的值(后面将会通过代码展示)。所以最后为了安全性(即不让一些对象中私有属性的值被外露),不能让所有对象都可以序列化。要让用户自己来选择是否可以序列化,因此需要一个接口来标记该类是否可序列化。
6.几个需要注意的点
1)静态变量和transient关键字修饰的变量不能被序列化;
序列化时并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。transient作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值设为初始值,如int型的是0。
2)反序列化时要按照序列化的顺序重构对象:如先序列化A后序列化B,则反序列化时也要先获取A后获取B,否则报错。
3)serialVersionUID(序列化ID)的作用:决定着是否能够成功反序列化。
虚拟机是否允许对象反序列化,不是取决于该对象所属类路径和功能代码是否与虚拟机加载的类一致,而是主要取决于对象所属类与虚拟机加载的该类的序列化 ID 是否一致。
java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。
4)自定义序列化方法的应用场景:对某些敏感数据进行加密操作后再序列化;反序列化对加密数据进行解密操作。
5)重复序列化:同一个对象重复序列化时,不会把对象内容再次序列化,而是新增一个引用指向第一次序列化时的对象而已。
6)序列化实现深克隆:在java中存在一个Cloneable接口,通过实现这个接口的类都会具备clone的能力,同时clone在内存中进行,在性能方面会比我们直接通过new生成对象要高一些,特别是一些大的对象的生成,性能提升相对比较明显。
7.常见的序列化技术
1、java 序列化
优点:java语言本省提供,使用比较方面和简单
缺点:不支持跨语言处理、性能相对不是很好,序列化以后产生的数据相对较大
2、XML序列化
XML序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的 字节码文件比较大,而且效率不高,适应于对性能不高,而且QPS较低的企业级内部系统之间的数据交换的场景,同时XML又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的WebService,就是采用XML格式对数据进行序列化的
3、JSON序列化
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于XML来说,JON的字节流较小,而且可读性也非常好。现在JSON数据格式的其他运用最普遍的。序列化方式还衍生了阿里的fastjson,美团的MSON,谷歌的GSON等更加优秀的转码工具。
4、Hessian 序列化框架子
Hessian是一个支持跨语言传输的二进制序列化协议,相对于Java默认的序列化机制来说,Hessian具有更好的性能和易用性,而且支持多重不同的语言,实际上Dubbo采用的就是Hessian序列化来实现,只不过Dubbo对Hessian进行重构,性能更高。
5、Protobuf 序列化框架
Protobuf是Google的一种数据交换格式,它独立于语言、独立于平台。
Google 提供了多种语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中但是但是要使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器。
选型建议
① 对性能要求不高的场景,可以采用基于 XML 的 SOAP 协议
② 对性能和间接性有比较高要求的场景,那么Hessian、Protobuf、Thrift、Avro 都可以
③ 基于前后端分离,或者独立的对外的 api 服务,选用 JSON 是比较好的,对于调试、可读性都很不错
④ Avro 设计理念偏于动态类型语言,那么这类的场景使用 Avro 是可以的