一、前言
当Java类对象需要读写磁盘或网络传输的时候,需要实现Serializable接口变为一个可序列化对象,同时,要提供具体的序列化方法,如最常用的Java IO序列化,本文讲述序列化/反序列化的相关知识,由浅入深。
序列化的定义:将一个对象以某种特定的方式,转换为数据流,这就是序列化。
为什么要转为数据流?因为要到计算机网络上传输
某种特定的方式是哪种方式?随便,只要可以完成网络传输就行,只要发送端和接收端对齐就行,另外,数据流占用空间越小越好
序列化是为了能够网络传输
编码解码是定义一种消息格式,解决网络传输中的拆包粘包问题
二、JavaBean的序列化和反序列化
2.1 socket通信中的序列化
可序列化对象:Java语言中,当对象用于读写磁盘(内存–磁盘)或读写网络(内存–网络),这个对象要求是可序列的,即实现Serializable接口。
序列化两种场景:序列化就是写入磁盘文件或写入网络,将对象转换为二进制流;反序列化就是从磁盘文件中读出或从网络中接收,将二进制流转换为对象。
序列化实现方式:序列化的方式有很多,Java中可以通过输入输出流来序列化和反序列化,其他的,还包括json序列化、xml序列化。评价不同的序列化方式,包括序列化后二进制流大小和该序列化方式是否可以跨平台。
新建两个maven工程 server client ,在Socket 通信中传递JavaBean对象,该Bean类必须实现Serializable接口,变为一个可序列化类;
新建三个类,User类作为JavaBean类,实现Serializable接口,用于Socket网络传输,ServerSocketDemo作为socket服务端类,ClientSocketDemo作为socket客户端类。
public class User implements Serializable {
private String name;
private int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + 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;
}
}
public class ServerSocketDemo {
static ServerSocket serverSocket;
public static void main(String[] args) throws Exception {
serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
ObjectInputStream objectOutputStream = new ObjectInputStream(socket.getInputStream());
User user = (User) objectOutputStream.readObject();
System.out.println(user);
}
}
public class ClientSocketDemo {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 8080);
User user = new User();
user.setName("Mic");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(user);
}
}
先运行服务端程序,再运行客户端程序,连接服务端socket,给服务端发送User类对象,服务端收到,打印出来:
2.2 序列化封装为工具方法
现在我们新建一个User4,如下:
public class User4 implements Serializable {
private String name;
private int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + 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;
}
}
封装两个工具方法,序列化和反序列化:
public interface ISerialize {
public <T> byte[] toolFunction_serialize(T obj) throws Exception;
public <T> T toolFunction_deserialize(byte[] data) throws Exception;
}
public class Utils_JavaSerialize implements ISerialize {
// 序列化工具方法 输入是对象 输出是byte数组
public <T> byte[] toolFunction_serialize(T obj) throws Exception{
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
// 反序列化工具方法 输入是byte数组 输出是对象
public <T> T toolFunction_deserialize(byte[] data) throws Exception{ // byte数组读入到内存里面,使用对象输入流输入
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
return (T)objectInputStream.readObject();
}
}
2.3 显示定义SerializableID属性
序列化ID是对象唯一标志,如下:
idea技巧:alter + enter 生成序列化id。
金手指1:
(1)序列化id是对象的唯一标志,当需要网络传输或读写磁盘的时候,判断对象是不是同一个。
(2)序列化id和构造函数一样,没有显示定义就用系统自动生成的(根据Bean类中属性自动生成),已经显示定义就用显示定义的。
(3)值得注意的是,序列化id无论有没有显示定义都没问题,但是如果写磁盘-读磁盘中一个使用自动生成,一个使用显示定义,就会出现InValidClassException local Class incompatible;同样,发送端-接收端中一个使用自动生成,一个使用显示定义,也会出现InValidClassException local Class incompatible。
2.4 transient 修饰的属性不被序列化
应业务需求,transient 修饰的属性不需要被序列化,则
运行结果:
2.5 transient 属性被序列化
readObject writeObject 重新序列化transient修饰的属性,如下:
运行结果:
金手指2:
(1)这里是反射调用, transient修饰的属性不让默认的序列化来传输,需要先完成一些业务逻辑,然后再序列化传输,只是不用默认的方式。比如hashMap中就有很多这样的transient修饰的属性,不让默认的序列化来传输,需要先完成一些业务逻辑,然后再序列化传输。
(2)这里的writeObject readObject对transient修饰的属性序列化仅对Java IO序列化有用,对其他序列化方式,XML序列化、Hessian序列化是无用的。
三、各种序列化方式
这里介绍分布式架构中序列化机制——只要对象读写磁盘、网络传输就有序列化。
金手指3:
(1)各个序列化方式都是提供网络传输时对JavaBean类型的序列化,但是这个JavaBean类必须是可序列化的,即这个JavaBean类必须实现Serializable接口。换一种说法,JavaBean类实现Serializable接口只是说明它是可序列的,真正的网络传输中的序列化还需要具体的序列化方式。
(2)序列化评价两个因子 跨语言特性 + 压缩比。
这部分介绍三种序列化方式,分别是Java输入输出流序列化、XML序列化、Hessian序列化,如下:
3.1 Java IO序列化
3.1.1 Java IO序列化 readObject(Object obj) writeObject(Object obj)
Java 中默认序列化的实现是二进制字节流,JaveBean类实现Serializable接口,表示该类对象是可序列化的。
3.1.2 序列化及其运行结果
public interface ISerialize {
public <T> byte[] toolFunction_serialize(T obj) throws Exception;
public <T> T toolFunction_deserialize(byte[] data) throws Exception;
}
public class Utils_JavaSerialize implements ISerialize {
// 序列化工具方法 输入是对象 输出是byte数组 写出去
public <T> byte[] toolFunction_serialize(T obj) throws Exception{ // 对象写成到网络,变成了byte数组
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
// 反序列化工具方法 输入是byte数组 输出是对象 读进来
public <T> T toolFunction_deserialize(byte[] data) throws Exception{ // byte数组读入到内存里面,变为了对象
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
return (T)objectInputStream.readObject();
}
}
public class JavaSerializeMain {
public static void main(String[] args) throws Exception{
User4 user4=new User4();
user4.setName("Mic");
user4.setAge(18);
ISerialize iSerialize=new Utils_JavaSerialize();
byte[] bytes=iSerialize.toolFunction_serialize(user4);
System.out.println(bytes.length);
for(int i=0;i<bytes.length;i++)
System.out.print(bytes[i]+ " ");
System.out.println();
User4 userRever=iSerialize.toolFunction_deserialize(bytes); // 把bytes注入进来
System.out.println(userRever);
}
}
运行结果:
3.1.3 Java IO序列化评价
金手指4:Java IO序列化 writeObject(Object obj) readObject(Object obj)
优点:简单,不需要外部依赖;
缺点:序列化后二进制字节流较大,不利于网络传输。
所以,需要寻找二进制字节流更小的序列化方法。
注意,上面这个序列化得到的二进制流打印出来是十进制的。
3.2 XML序列化 toXML() fromXML()
3.2.1 pom.xml 导入依赖
3.2.2 序列化及其运行结果
public class XMLSerializer implements ISerialize {
XStream xStream=new XStream(new DomDriver());
@Override
public <T> byte[] toolFunction_serialize(T obj) throws Exception {
return xStream.toXML(obj).getBytes();
}
@Override
public <T> T toolFunction_deserialize(byte[] data) throws Exception{
return (T)xStream.fromXML(new String(data));
}
}
public class SerializeMain_XML {
public static void main(String[] args) throws Exception{
User4 user4=new User4();
user4.setName("Mic");
user4.setAge(18);
ISerialize iSerialize=new XMLSerializer();
byte[] bytes=iSerialize.toolFunction_serialize(user4);
System.out.println(bytes.length);
System.out.println(new String(bytes));
for(int i=0;i<bytes.length;i++)
System.out.print(bytes[i]+ " ");
System.out.println();
User4 userRever=iSerialize.toolFunction_deserialize(bytes); // bytes拿来
System.out.println(userRever);
}
}
运行结果
3.2.3 XML序列化评价
金手指5: XML序列化 toXML() fromXML()
xml的优点:跨语言、稳定可靠;
xml的缺点:序列化后二进制流比较大,网络传输效率低。
所以网络传输不使用xml格式,但是配置文件使用xml格式
3.3 Dubbo中的Hessian序列化
3.3.1 pom.xml 导入依赖
3.3.2 序列化及其运行结果
public class HessianSerializer implements ISerialize {
@Override
public <T> byte[] toolFunction_serialize(T obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
HessianOutput objectOutputStream=new HessianOutput(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
@Override
public <T> T toolFunction_deserialize(byte[] data) throws Exception {
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data);
HessianInput objectInputStream=new HessianInput(byteArrayInputStream);
return (T)objectInputStream.readObject();
}
}
public class SerializeMain_Hessian {
public static void main(String[] args) throws Exception{
User4 user4=new User4();
user4.setName("Mic");
user4.setAge(18);
ISerialize iSerialize=new HessianSerializer();
byte[] bytes=iSerialize.toolFunction_serialize(user4);
System.out.println(bytes.length);
for(int i=0;i<bytes.length;i++)
System.out.print(bytes[i]+ " ");
System.out.println();
User4 userRever=iSerialize.toolFunction_deserialize(bytes);
System.out.println(userRever);
}
}
运行结果
3.3.3 Hessian序列化评价
金手指6:Dubbo中的Hessian序列化
优点:序列速度快,可跨语言;
缺点:序列化后二进制占用比较大,不利于网络传输。
所以,我们要寻找更好的序列化方案。
四、终结者——ProtoBuf序列化
4.1 ProtoBuf写法
金手指7:ProtoBuf是google提供的一种序列化方式,跨平台,跨语言,非常高效。
ProtoBuf是google提供的一种序列化方式,跨平台,跨语言,非常高效。地址:https://github.com/protocolbuffers/protobuf,如下:
得到一个protoc.exe文件
protoc.exe同级目录下新建文本文档,重命名为User.proto,文本内容为:
syntax="proto2";
package com.package2;
option java_package="com.package2";
option java_outer_classname="UserProto";
message User{
required string name=1;
required int32 age=2;
}
注意,这里是int32,不是int。这里的 1 2 表示存放顺序,用于序列化和反序列化。
打印命令行编译该文件:
protoc.exe ./User.Proto --java_out=./
导入依赖
public class ProtoBufMain {
public static void main(String[] args) throws Exception{
UserProto.User user=UserProto.User.newBuilder().setName("Mic").setAge(18).build();
ByteString bytes=user.toByteString();
System.out.println(bytes.size());
for (byte bt:bytes.toByteArray()){
System.out.print(bt+" ");
}
System.out.println();
UserProto.User userRever = UserProto.User.parseFrom(bytes);
System.out.println(userRever);
}
}
运行结果:
4.2 Protobuf原理(解密二进制流 10 3 77 105 99 16 18 )
对于序列化后,用于网络传输是二进制字节流,单元数据是Tag-Length-Value格式。其中,Tag和Value为必选字段,对于某些类型(比如int类型),Length为可选字段。
上面Protobuf序列化程序中二进制流为10 3 77 105 99 16 18,存放 字符串“Mic” 为 10 3 77 105 99,其中,10表示Tag,3表示Length长度,77 表示 字符‘M’ 105 表示字符 ‘i’ 99 表示字符 ‘c’。
注意:ASCII表中,大写M为77,小写i为105,小写c为99。
存放 数字18 为 16 18,其中,16表示Tag,int32 类型忽略Length,18表示数字18。
注意:按照user.proto中定义的顺序来存放,只存放value值(即 “Mic” 18),不存放key(即name age)。
金手指8:
对于序列化后,用于网络传输是二进制字节流,单元数据是Tag-Length-Value格式。其中,Tag和Value为必选字段,对于某些类型(比如int类型),Length为可选字段。
存放 字符串“Mic” 为 10 3 77 105 99,其中,10表示Tag,3表示Length长度,77 表示 字符‘M’ 105 表示字符 ‘i’ 99 表示字符 ‘c’。
length就是属性长度,直接存放;
value是属性值的ascii码。
Tag计算方式: Tag底层计算方法: num<<3 | wire_type (序号左移3位,与wire_type按位或) 因为name是string类型,所以wire_type是2,因为age是int32类型,所以wire_type是0。
对于一个对象中的多个属性,按照xx.photo文件中的顺序来存放。
4.3 Tag底层计算方式
首先引入一个wire_type的概念,不同类型都有对应的wire_type值。
Tag底层计算方法: num<<3 | wire_type (序号左移3位,与wire_type按位或)
因为name是string类型,所以wire_type是2,因为age是int32类型,所以wire_type是0。
1 << 3 | 2 = 1000 | 0010 = 1010 =10 所以name的Tag为10;
2 << 3 | 0 = 0001 0000 | 0000 0000 =16 所以age的Tag为16。
4.4 数据压缩(age=300)
将程序中的age为300,再次运行,如下:
数据压缩算法 300 变为 -84 2 ,因为是int32类型,所以32位
300 为正数,原码即使补码,为
0000 0000 0000 0000 0000 0001 0010 1100
算法:截取每个有效位的7位,如果高位后续字节还存在有意义的数据,则截取这个7个字节最高位设置为1。
则从低位到高位,开始:
第一个有效字节 0010 1100,截取7位,最高位设置为1,得到 1010 1100,这里原来的最高位0变成下一个有效字节的最低位;
第二个有效字节 0000 0010,由于接下来已经不存在有意义的数据,所以不变,仍为0000 0010则拼接,第一个得到字节 1010 1100 和 第二个得到字节 0000 0010,为 1010 1100 0000 0010,十进制即 -84 2 。
金手指9:数据压缩算法
数据压缩算法:截取每个有效位的7位,如果高位后续字节还存在有意义的数据,则截取这个7个字节最高位设置为1。
4.5 protobuf序列化评价
protobuf 编码解码 基于位运算,位运算速度快,位存储占用小。
金手指10:protobuf 编码解码
基于位运算,位运算速度快,位存储占用小。
五、面试金手指
5.1 三种常用的序列化方式
Java IO序列化 writeObject(Object obj) readObject(Object obj)
优点:简单,不需要外部依赖;
缺点:序列化后二进制字节流较大,不利于网络传输。
XML序列化 toXML() fromXML()
xml的优点:跨语言、稳定可靠;
xml的缺点:序列化后二进制流比较大,网络传输效率低。
所以网络传输不使用xml格式,但是配置文件使用xml格式。
Dubbo中的Hessian序列化
优点:序列速度快,可跨语言;
缺点:序列化后二进制占用比较大,不利于网络传输。
5.2 Protobuf定义
定义:ProtoBuf是google提供的一种序列化方式,跨平台,跨语言,非常高效;
两个优点:protobuf 编码解码 基于位运算,位运算速度快,位存储占用小。
ProtoBuf底层原理,压缩率高的底层原理
对于序列化后,用于网络传输是二进制字节流,单元数据是Tag-Length-Value格式。其中,Tag和Value为必选字段,对于某些类型(比如int类型),Length为可选字段。
存放 字符串“Mic” 为 10 3 77 105 99,其中,10表示Tag,3表示Length长度,77 表示 字符‘M’ 105 表示字符 ‘i’ 99 表示字符 ‘c’。
length就是属性长度,直接存放
value是属性值的ascii码。
Tag计算方式:num<<3 | wire_type (序号左移3位,与wire_type按位或) 因为name是string类型,所以wire_type是2,因为age是int32类型,所以wire_type是0。
ps:对于一个对象中的多个属性,按照xx.photo文件中的顺序来存放。
5.3 数据压缩
数据压缩算法:截取每个有效位的7位,如果高位后续字节还存在有意义的数据,则截取这个7个字节最高位设置为1。
六、尾声
讲述了序列化和反序列化的基本知识,还有几种序列化方法(Java IO序列化、xml序列化、Hession序列化),最后引入一种大公司常用的基于位运算的高效序列化方法 protobuf。
天天打码,天天进步!!!
源码地址:https://download.csdn.net/download/qq_36963950/12540753