文章最前: 我是Octopus,这个名字来源于我的中文名--章鱼;我热爱编程、热爱算法、热爱开源。所有源码在我的个人github ;这博客是记录我学习的点点滴滴,如果您对 Python、Java、AI、算法有兴趣,可以关注我的动态,一起学习,共同进步。
相关文章:
文章目录:
序列化
Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在 JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象;Java 对象序列化就能够帮助我们实现该功能,简单来说序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化。
序列化面临的挑战
一个序列化算法优劣的两个重要指标是:1.序列化以后的数据大小;2.序列化操作本身的速度及系统资源开销(CPU、内存)。Java 语言本身提供了对象序列化机制,也是 Java 语言本身最重要的底层机制之一,Java 本身提供的序列化机制存在两个问题:
1. 序列化的数据比较大,传输效率低
2. 其他语言无法识别和对接
在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化
1.定义接口:
package serializedemo;
/**
* @author zhangyu
* @version V1.0
* @ClassName: ISerializer
* @Description: 定义一个序列化接口,里面有两个方法:序列化方法和反序列化方法
* @date 2019/1/14 15:52
**/
public interface ISerializer {
<T> byte[] serialize(T obj);
<T> T deserialize(byte[] data, Class<T> clazz);
}
2.基于 JDK 序列化方式实现
JDK 提 供 了 Java 对 象 的 序 列 化 方 式 , 主 要 通 过 输 出 流java.io.ObjectOutputStream 和对象输入流 java.io.ObjectInputStream来实现。其中,被序列化的对象需要实现 java.io.Serializable 接口;
package serializedemo;
import java.io.*;
/**
* @author zhangyu
* @version V1.0
* @ClassName: SerializerUtils
* @Description: TOTO
* @date 2019/1/14 15:55
**/
public class SerializerUtils implements ISerializer {
// 序列化
@Override
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
return byteArrayOutputStream.toByteArray();
}
// 反序列化
@Override
public <T> T deserialize(byte[] data, Class<T> clazz) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
try {
ObjectInput objectInput = new ObjectInputStream(byteArrayInputStream);
return (T) objectInput.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3.通过对一个 user 对象进行序列化操作
package serializedemo;
import java.io.Serializable;
/**
* @author zhangyu
* @version V1.0
* @ClassName: User
* @Description: TOTO
* @date 2019/1/14 16:00
**/
public class User implements Serializable {
private String name;
private int age;
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;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
4.测试序列化和反序列化的过程:
package serializedemo;
import org.junit.Test;
/**
* @author zhangyu
* @version V1.0
* @ClassName: SerializeDemo
* @Description:测试序列化和反序列化
* @date 2019/1/14 16:04
**/
public class SerializeDemo {
@Test
public void fun() {
ISerializer iSerializer = new SerializerUtils();
User user = new User();
user.setAge(18);
user.setName("zhangyu");
// 序列化和反序列化
byte[] seralByte = iSerializer.serialize(user);
User dUser = iSerializer.deserialize(seralByte, User.class);
System.out.println(dUser);
}
}
序列化的深入认识
serialVersionUID 的作用
Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时, JVM 会把传来的字节流中的 serialVersionUID与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException如果没有为指定的 class 配置 serialVersionUID,那么 java 编译器会自动给这个 class 进行一个摘要算法,类似于指纹算法,只要这个文件有任何改动,得到的 UID 就会截然不同的,可以保证在这么多类中,这个编号是唯一的serialVersionUID 有两种显示的生成方式:一是默认的 1L,比如: private static final long serialVersionUID = 1L;二是根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段当 实 现 java.io.Serializable 接 口 的 类 没 有 显 式 地 定 义 一 个serialVersionUID 变量时候, Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作序列化版本比较用,这种情况下,如果Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次, serialVersionUID 也不会变化的。
静态变量序列化
在 User 中添加一个全局的静态变量 num , 在执行序列化以后修改num 的值为 10, 然后通过反序列化以后得到的对象去输出 num 的值;
package serializedemo;
import java.io.Serializable;
/**
* @author zhangyu
* @version V1.0
* @ClassName: User
* @Description: TOTO
* @date 2019/1/14 16:00
**/
public class User implements Serializable {
private String name;
private int age;
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;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public static int num = 5;
}
测试demo:
package serializedemo;
import org.junit.Test;
/**
* @author zhangyu
* @version V1.0
* @ClassName: SerializeDemo
* @Description:测试序列化和反序列化
* @date 2019/1/14 16:04
**/
public class SerializeDemo2 {
@Test
public void fun() {
ISerializer iSerializer = new SerializerUtils();
User user = new User();
user.setAge(18);
user.setName("zhangyu");
// 序列化和反序列化
byte[] seralByte = iSerializer.serialize(user);
user.num = 10;
User dUser = iSerializer.deserialize(seralByte, User.class);
System.out.println(dUser.num);
}
}
最后的输出是 10, 理论上打印的 num 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。
父类的序列化
一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable接口,在子类中设置父类的成员变量的值,接着序列化该子类对象;再反序列化出来以后输出父类属性的值。
1.superuser的类
package serializedemo;
/**
* @author zhangyu
* @version V1.0
* @ClassName: SuperClass
* @Description: 一个没有被序列化的父类user
* @date 2019/1/14 16:31
**/
public class SuperUser {
String sex;
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
2.继承superuser的类
package serializedemo;
import java.io.Serializable;
/**
* @author zhangyu
* @version V1.0
* @ClassName: User
* @Description: TOTO
* @date 2019/1/14 16:00
**/
public class User1 extends SuperUser implements Serializable {
private String name;
private int age;
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;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public static int num = 5;
}
3.测试的demo
package serializedemo;
import org.junit.Test;
/**
* @author zhangyu
* @version V1.0
* @ClassName: SerializeDemo
* @Description:测试序列化和反序列化
* @date 2019/1/14 16:04
**/
public class SerializeDemo3 {
@Test
public void fun() {
ISerializer iSerializer = new SerializerUtils();
User1 user1 = new User1();
user1.setAge(18);
user1.setName("zhangyu");
user1.setSex("male");
// 序列化和反序列化
byte[] seralByte = iSerializer.serialize(user1);
User1 dUser = iSerializer.deserialize(seralByte, User1.class);
System.out.println(dUser.getSex());
}
}
发现父类的 sex 字段的值为 null。也就是父类没有实现序列化。
结论:
1. 当一个父类没有实现序列化时,子类继承该父类并且实现了序列化。在反序列化该子类后,是没办法获取到父类的属性值;
2. 当一个父类实现序列化,子类自动实现序列化,不需要再显示实现Serializable 接口;
3. 当一个对象的实例变量引用了其他对象,序列化该对象时也会把引用对象进行序列化,但是前提是该引用对象必须实现序列化接口;
Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后, transient变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
1.transient关键字修饰字段不会被序列化
package serializedemo;
import java.io.Serializable;
/**
* @author zhangyu
* @version V1.0
* @ClassName: User
* @Description: TOTO
* @date 2019/1/14 16:00
**/
public class User2 extends SuperUser implements Serializable {
private String name;
private int age;
private transient String hobby;
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 String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
}
2.测试demo
package serializedemo;
import org.junit.Test;
/**
* @author zhangyu
* @version V1.0
* @ClassName: SerializeDemo
* @Description:测试序列化和反序列化
* @date 2019/1/14 16:04
**/
public class SerializeDemo4 {
@Test
public void fun() {
ISerializer iSerializer = new SerializerUtils();
User2 user2 = new User2();
user2.setAge(18);
user2.setName("zhangyu");
user2.setSex("male");
user2.setHobby("basketball");
// 序列化和反序列化
byte[] seralByte = iSerializer.serialize(user2);
User2 dUser = iSerializer.deserialize(seralByte, User2.class);
System.out.println(dUser.getHobby());
}
}
绕开 transient 机制的办法
writeObject和 readObject 这两个私有的方法,既不属于 Object、也不是 Serializable,为什么能够在序列化的时候被调用呢? 原因是, ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为 ObjectOutputStream使用 getPrivateMethod,所以这些方法必须声明为 priate 以至于供ObjectOutputStream 来使用:
package serializedemo;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @author zhangyu
* @version V1.0
* @ClassName: User
* @Description: 绕开transient机制的方法
* @date 2019/1/14 16:00
**/
public class User3 extends SuperUser implements Serializable {
private String name;
private int age;
private transient String hobby;
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 String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
// 序列化对象
private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {
objectOutputStream.defaultWriteObject();
objectOutputStream.writeObject(hobby);
}
// 反序列化
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
objectInputStream.defaultReadObject();
hobby = (String) objectInputStream.readObject();
}
}
序列化的存储规则
package serializedemo;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* @author zhangyu
* @version V1.0
* @ClassName: StoreDemo
* @Description: TOTO
* @date 2019/1/14 17:07
**/
public class StoreDemo {
@Test
public void fun() throws IOException {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.txt"));
User user = new User();
user.setName("zhangyu");
out.writeObject(user);
out.flush();
System.out.println(new File("user.txt").length());
out.writeObject(user);
out.close();
System.out.println(new File("user.txt").length());
}
}
同一对象两次(开始写入文件到最终关闭流这个过程算一次,上面的演示效果是不关闭流的情况才能演示出效果)写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小, 第二次写入对象时文件只增加了 5 字节,Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系.该存储规则极大的节省了存储空间。
序列化实现深克隆
在 Java 中存在一个 Cloneable 接口,通过实现这个接口的类都会具备clone 的能力,同时 clone 是在内存中进行,在性能方面会比我们直接通过 new 生成对象要高一些,特别是一些大的对象的生成,性能提升相对比较明显。 那么在 Java 领域中,克隆分为深度克隆和浅克隆。
1)浅克隆
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。实现一个邮件通知功能,告诉每个人今天晚上的上课时间,通过浅克隆。
实现如下
1.email.java邮件帮助类
package serializedemo;
/**
* @author zhangyu
* @version V1.0
* @ClassName: Email
* @Description: TOTO
* @date 2019/1/14 17:16
**/
public class Email {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
2.person.java人帮助类
package serializedemo;
/**
* @author zhangyu
* @version V1.0
* @ClassName: Person
* @Description: TOTO
* @date 2019/1/14 17:15
**/
public class Person implements Cloneable {
private String name;
private Email email;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public Person(String name) {
this.name = name;
}
public Person(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Email getEmail() {
return email;
}
public void setEmail(Email email) {
this.email = email;
}
}
打印内容:
zhangyu->今晚8点20开会!
张飞->今晚8点20开会!
通过结果发现,所有人的通知消息都发生了改变。这是因为 p2 克隆的这个对象的 Email 引用地址指向的是同一个。这就是浅克隆。
2)深克隆
被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
1.深克隆类email.java
package serializedemo;
import java.io.Serializable;
/**
* @author zhangyu
* @version V1.0
* @ClassName: Email
* @Description: TOTO
* @date 2019/1/14 17:16
**/
public class Email implements Serializable {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
2.person2.java
package serializedemo;
import java.io.*;
/**
* @author zhangyu
* @version V1.0
* @ClassName: Person
* @Description: TOTO
* @date 2019/1/14 17:15
**/
public class Person2 implements Cloneable,Serializable {
private String name;
private Email email;
public Person2 deepClone() throws IOException,ClassNotFoundException{
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
return (Person2) ois.readObject();
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public Person2(String name) {
this.name = name;
}
public Person2(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Email getEmail() {
return email;
}
public void setEmail(Email email) {
this.email = email;
}
}
3.测试克深隆类
package serializedemo;
import org.junit.Test;
import java.io.IOException;
/**
* @author zhangyu
* @version V1.0
* @ClassName: SerializeDemo
* @Description:测试序列化和反序列化
* @date 2019/1/14 16:04
**/
public class CloneDemo3 {
@Test
public void fun() throws IOException, ClassNotFoundException {
Email email = new Email();
email.setContent("今天晚上20:00有课");
Person2 p1 = new Person2("zhangyu");
p1.setEmail(email);
Person2 p2 = p1.deepClone();
p2.setName("tangtuo");
p2.getEmail().setContent("今天晚上20:30有课");
System.out.println(p1.getName() + "-->" + p1.getEmail().getContent());
System.out.println(p2.getName() + "-->" + p2.getEmail().getContent());
}
}
最后打印结果:
zhangyu-->今天晚上20:00有课
tangtuo-->今天晚上20:30有课
这样就能实现深克隆效果,原理是把对象序列化输出到一个流中,然后在把对象从序列化流中读取出来,这个对象就不是原来的对象了。
常见的序列化技术
使用 JAVA 进行序列化有他的优点,也有他的缺点
优点: JAVA 语言本身提供,使用比较方便和简单
缺点:不支持跨语言处理、 性能相对不是很好,序列化以后产生的数据相对较大
XML 序列化框架
XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大,而且效率不高,适用于对性能不高,而且 QPS 较低的企业级内部系统之间的数据交换的场景,同时 XML 又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的 Webservice,就是采用 XML 格式对数据进行序列化的。
JSON 序列化框架
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于 XML 来说, JSON 的字节流更小,而且可读性也非常好。现在 JSON数据格式在企业运用是最普遍的。
JSON 序列化常用的开源工具有很多
1. Jackson (https://github.com/FasterXML/jackson)
2. 阿里开源的 FastJson (https://github.com/alibaba/fastjon)
3. 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 序列化框架
Google 提供了多种语言来实现,比如 Java、 C、 Go、 Python,每一种实现都包含了相应语言的编译器和库文件Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中但是但是要使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器。
Protobuf 原理分析
核心原理: protobuf 使用 varint(zigzag)作为编码方式, 使用 T-LV 作为存储方式
varint 编码方式
varint 是一种数据压缩算法,其核心思想是利用 bit 位来实现数据压缩。比如: 对于 int32 类型的数字,一般需要 4 个字节 表示; 若采用Varint 编码,对于很小的 int32 类型 数字,则可以用 1 个字节假设我们定义了一个 int32 字段值=296。
第一步,转化为 2 进制编码;
第二步,提取字节;
第三步,继续提取字节;
第四步,拼接成一个新的字节串;
存储方式
经过编码以后的数据,大大减少了字段值的占用字节数,然后基于 T-LV 的方式进行存储。
总结
Protocol Buffer 的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其原因如下:序列化速度快的原因;
a. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成
序列化后的数据量体积小(即数据压缩效果好)的原因:
a. 采用了独特的编码方式,如 Varint、 Zigzag 编码方式等等。
b. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑。
序列化技术的选型
技术层面
1. 序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能。
2. 序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间。
3. 序列化协议是否支持跨平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信需求,那么这个是必须要考虑的。
4. 可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新,这就要求我们采用的序列化协议基于良好的可扩展性/兼容性,比如在现有的序列化数据结构中新增一个业务字段,不会影响到现有的服务。
5. 技术的流行程度,越流行的技术意味着使用的公司多,那么很多坑都已经淌过并且得到了解决,技术解决方案也相对成熟。
6. 学习难度和易用性。
选型建议
1. 对性能要求不高的场景,可以采用基于 XML 的 SOAP 协议。
2. 对性能和间接性有比较高要求的场景,那么 Hessian、Protobuf、Thrift、Avro 都可以。
3. 基于前后端分离,或者独立的对外的 api 服务,选用 JSON 是比较好的,对于调试、可读性都很不错。
4. Avro 设计理念偏于动态类型语言,那么这类的场景使用 Avro 是可以的。