【Java】序列化和反序列化

文章目录

  • 一、前言
  • 二、JavaBean的序列化和反序列化
    • 2.1 socket通信中的序列化
    • 2.2 序列化封装为工具方法
    • 2.3 显示定义SerializableID属性
    • 2.4 transient 修饰的属性不被序列化
    • 2.5 transient 属性被序列化
  • 三、各种序列化方式
    • 3.1 Java IO序列化
      • 3.1.1 Java IO序列化
      • 3.1.2 序列化及其运行结果
      • 3.1.3 Java IO序列化评价
    • 3.2 XML序列化
      • 3.2.1 pom.xml 导入依赖
      • 3.2.2 序列化及其运行结果
      • 3.2.3 XML序列化评价
    • 3.3 Hessian序列化
      • 3.3.1 pom.xml 导入依赖
      • 3.3.2 序列化及其运行结果
      • 3.3.3 Hessian序列化评价
  • 四、终结者——ProtoBuf序列化
    • 4.1 ProtoBuf写法
    • 4.2 Protobuf原理(解密二进制流 10 3 77 105 99 16 18 )
    • 4.3 Tag底层计算方式
    • 4.4 数据压缩(age=300)
    • 4.5 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客户端类。

【Java】序列化和反序列化_第1张图片

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类对象,服务端收到,打印出来

【Java】序列化和反序列化_第2张图片

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 对象唯一标志
【Java】序列化和反序列化_第3张图片

【Java】序列化和反序列化_第4张图片
alter + enter 生成序列化id
【Java】序列化和反序列化_第5张图片

序列化id是对象的唯一标志,当需要网络传输或读写磁盘的时候,判断对象是不是同一个。

序列化id和构造函数一样,没有显示定义就用系统自动生成的(根据Bean类中属性自动生成),已经显示定义就用显示定义的。

值得注意的是,序列化id无论有没有显示定义都没问题,但是如果写磁盘-读磁盘中一个使用自动生成,一个使用显示定义,就会出现InValidClassException local Class incompatible;同样,发送端-接收端中一个使用自动生成,一个使用显示定义,也会出现InValidClassException local Class incompatible。

2.4 transient 修饰的属性不被序列化

应业务需求,transient 修饰的属性不需要被序列化
【Java】序列化和反序列化_第6张图片
运行结果:
【Java】序列化和反序列化_第7张图片

2.5 transient 属性被序列化

readObject writeObject 重新序列化transient修饰的属性。

【Java】序列化和反序列化_第8张图片
运行结果:

【Java】序列化和反序列化_第9张图片

注意1,这里是反射调用, transient修饰的属性不让默认的序列化来传输,需要先完成一些业务逻辑,然后再序列化传输,只是不用默认的方式。比如hashMap中就有很多这样的transient修饰的属性,不让默认的序列化来传输,需要先完成一些业务逻辑,然后再序列化传输。

注意2,这里的writeObject readObject对transient修饰的属性序列化仅对Java
IO序列化有用,对其他序列化方式,XML序列化、Hessian序列化是无用的。

三、各种序列化方式

这里介绍分布式架构中序列化机制——只要对象读写磁盘、网络传输就有序列化。

注意两点,

第一,各个序列化方式都是提供网络传输时对JavaBean类型的序列化,但是这个JavaBean类必须是可序列化的,即这个JavaBean类必须实现Serializable接口。换一种说法,JavaBean类实现Serializable接口只是说明它是可序列的,真正的网络传输中的序列化还需要具体的序列化方式。

第二,序列化评价两个因子 跨语言特性 + 压缩比。

这部分介绍三种序列化方式,分别是Java输入输出流序列化、XML序列化、Hessian序列化,如下:
【Java】序列化和反序列化_第10张图片

3.1 Java IO序列化

3.1.1 Java IO序列化

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{
        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);
    }
}

运行结果
【Java】序列化和反序列化_第11张图片

3.1.3 Java IO序列化评价

优点:简单,不需要外部依赖。
缺点:序列化后二进制字节流较大,不利于网络传输。

所以,需要寻找二进制字节流更小的序列化方法。

注意,上面这个序列化得到的二进制流打印出来是十进制的。

3.2 XML序列化

3.2.1 pom.xml 导入依赖

【Java】序列化和反序列化_第12张图片

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);
    }
}

运行结果

【Java】序列化和反序列化_第13张图片

3.2.3 XML序列化评价

xml的优点:跨语言、稳定可靠;

xml的缺点:序列化后二进制流比较大,网络传输效率低。

所以网络传输不使用xml格式,但是配置文件使用xml格式

3.3 Hessian序列化

3.3.1 pom.xml 导入依赖

【Java】序列化和反序列化_第14张图片

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);
    }
}

运行结果

【Java】序列化和反序列化_第15张图片

3.3.3 Hessian序列化评价

优点:序列速度快,可跨语言
缺点:序列化后二进制占用比较大,不利于网络传输。

所以,我们要寻找更好的序列化方案。

四、终结者——ProtoBuf序列化

4.1 ProtoBuf写法

ProtoBuf是google提供的一种序列化方式,跨平台,跨语言,非常高效。地址:https://github.com/protocolbuffers/protobuf,如下:

【Java】序列化和反序列化_第16张图片

【Java】序列化和反序列化_第17张图片

【Java】序列化和反序列化_第18张图片
得到一个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=./

【Java】序列化和反序列化_第19张图片

【Java】序列化和反序列化_第20张图片

【Java】序列化和反序列化_第21张图片

导入依赖

【Java】序列化和反序列化_第22张图片

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);
    }
}

运行结果:
【Java】序列化和反序列化_第23张图片

4.2 Protobuf原理(解密二进制流 10 3 77 105 99 16 18 )

对于序列列后,用于网络传输是二进制字节流,单元数据是Tag-Length-Value格式。其中,Tag和Value为必选字段,对于某些类型(比如int类型),Length为可选字段。
【Java】序列化和反序列化_第24张图片

上面Protobuf序列化程序中二进制流为10 3 77 105 99 16 18

存放 字符串“Mic” 为 10 3 77 105 99,其中,10表示Tag,3表示Length长度,77 表示 字符‘M’ 105 表示字符 ‘i’ 99 表示字符 ‘c’。

【Java】序列化和反序列化_第25张图片
【Java】序列化和反序列化_第26张图片

【Java】序列化和反序列化_第27张图片

存放 数字18 为 16 18,其中,16表示Tag,int32 类型忽略Length,18表示数字18。

注意,按照user.proto中定义的顺序来存放,只存放value值(即 “Mic” 18),不存放key(即name age)。

4.3 Tag底层计算方式

首先引入一个wire_type的概念,不同类型都有对应的wire_type值。
【Java】序列化和反序列化_第28张图片

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,再次运行,如下:

【Java】序列化和反序列化_第29张图片

数据压缩算法 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 。

4.5 protobuf序列化评价

protobuf 编码解码 基于位运算,位运算速度快,位存储占用小

五、小结

讲述了序列化和反序列化的基本知识,还有几种序列化方法(Java IO序列化、xml序列化、Hession序列化),最后引入一种大公司常用的基于位运算的高效序列化方法 protobuf。

天天打码,天天进步!!!

源码地址:https://download.csdn.net/download/qq_36963950/12540753

你可能感兴趣的:(Java)