第十三篇:四种的序列化方式(JavaIO序列化 XML序列化 Hessian序列化 Protobuf序列化)

一、前言

当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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祖母绿宝石

打赏一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值