[IO学习笔记]三、序列化与反序列化

693 阅读11分钟

Java IO系列文章:

  1. IO学习笔记]一、理清庞大的Java IO体系
  2. IO学习笔记]二、文件操作不得不了解的Path, Files, RondomAccessFile
  3. IO学习笔记]三、序列化与反序列化

Java原生序列化

一、保存和加载序列化对象

1. 基本使用

//java.io.ObjectOutputStream

//创建一个0bjectoutputstream使得你可以将对象写出到指定的OutputStream。
ObjectOutputStream(OutputStream out)
  
//写出指定的对象到0bjectoutputStream,这个方法将存储指定对象的类、类的签名以及这个类及其超类中所有非静态和非瞬时的域的值。
void writeobject ( object obj )
//java.io.ObjectInputStream

//创建一个0bjectInputStream用于从指定的InputStream中读回对象信息。
ObjectInputStream(InputStream in)
  
//从ObjectInputStream中读入一个对象。特别是,这个方法会读回对象的类、类的签名以及这个类及其超类中所有非静态和非瞬时的域的值。它执行的反序列化允许恢复多个对象引用。
Object readobject()

同时,被序列化的类都必须实现Serializable接口

class Employee implements serializable {......}

序列化例子:

// 1. 打开一个ObjectOutputStream对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.dat"));
// 2. 调用writeObject方法
Employee harry = new Employee ("Harry Hacker" , 50000 , 1989 , 10 , 1 );
Manager boss=new Manager("Carl Cracker",80000,1987,12,15);
out.writeObject(harry);
out.writeObject(boss);

反序列化例子:

// 1.打开一个ObjectInputStream对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.dat"));
// 2. 调用readObject()方法
Employee el = (Employee) in.readObject();
Employee e2 = (Employee) in.readObject();

2. 序列化遇到依赖如何处理

class Manager extends Employee {
  ...
  private Employee secretary;
  ...
}

这里肯定不能仅仅保存secretary的内存地址,因为当对象被重新加载时,他可能占据的是与原来完全不同的内存地址。

对此,Java的解决办法是,每个对象都是用一个序列号(serial number)保存,这就是这种机制之所以称为对象序列化的原因。下面是其算法:

在写出对象(序列化)的时候:

  • 对你遇到的每一个对象引用都关联一个序列号(如图2-6所示)。
  • 对于每个对象,当第一次遇到时,保存其对象数据到输出流中。
  • 如果某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为x的对象相同”。

在读入对象(反序列化)的时候,整个过程是反过来的:

  • 对于对象输入流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据来初

    始化它,然后记录这个顺序号和新对象之间的关联。

  • 当遇到“与之前保存过的序列号为x的对象相同”标记时,获取与这个顺序号相关联

    的对象引用。

二、修改默认的序列化机制

1. transient

某些数据域是不可以序列化的,例如,只对本地方法有意义的存储文件句柄或窗口句柄 的整数值,这种信息在稍后重新加载对象或将其传送到其他机器上时都是没有用处的,还可能导致系统出错。

Java拥有一种很简单的机制来防止这种域被序列化,那就是将它们标记成是transient

class Manager extends Employee {
  ...
  private transient String name;
  private Employee secretary;
  ...
}

2. 自定义序列化

序列化机制为单个的类提供了一种方式,去向默认的读写行为添加验证或任何其他想要的行为。可序列化的类可以定义具有下列签名的方法:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
private void writeObject(ObjectOutputStrean out) throws IOException;

之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。

通过这两个方法可以绕过transient。

示例:

public class LabeledPoint implements Serializable {
  private String label ;
	private transient Point2D.Double point;
}
private void writeObject (ObjectOutputStream out) throws IOException{
  out.defaultWriteObject(); // 先写出对象描述符和String域lable
	out.writeDouble(point.getX());
	out.writeDouble(point.getY());
}
// 在readObject方法中,反过来执行上述过程。
private void readObject(0bjectInputStream in) throws IOException {
  in.defaultReadObject();
	double X = in.readDouble();
	double y = in.readDouble();
	point = new Point2D.Double(x,y);
}

三、序列化单例和类型安全的枚举

在序列化和反序列化时,如果目标对象是唯一的,那么你必须加倍当心,这通常会在实现单例和类型安全的枚举时发生。

如果你使用Java语言的enum结构,那么你就不必担心序列化,它能够正常工作。

但如果是远古时候的代码,手写的私有化构造函数的枚举类型,就要小心了。如:

public class Orientation {
  public static final Orientation HORIZONTAL = new Orientation(1);
  public static final Orientation VERTICAL = new Orientation(2);
  private int values;
  private Orientation(int v) {value = v;}
}

对于这种情况,我们需要ReadResolve的特殊序列化方法。

如果定义了readResolve方法,在对象反序列化之后就会调用它,他必须返回一个对象,而该对象之后会成为readObject的返回值。

在上面的情况中,readResolve方法将检查value域并返回恰当的枚举类型

protected Object readResolve() throws ObjectStreamException {
	if(value=1)return Orientation.HORIZONTAL;
	if(value==2)return Orientation.VERTICAL;
	throw new ObjectStreamException()://this shouldn't happen
}

四、序列化的版本管理

正常来说,如果两个类有一丝不一样的地方,序列化就是失败。但是在实际开发过程中,可能只是对某个类进行微小的改动,若想要对之前,或者之后的版本进行序列化兼容,我们就需要用到一个静态数据成员:serialVersionUID

class Employee implements Serializable 
{
    ...
	public static final long serialVersionUID - -1814239825517340645L;
	...
}

如果一个类具有名为serialVersionUID的静态数据成员,它就不再需要人工地计算其指纹,而只需直接使用这个值。一旦这个静态数据成员被置于某个类的内部,那么序列化系统就可以读入这个类的对象的不同版本。

如果这个类的方法产生的变化,在读入新对象数据时是不会有任何问题的。

如果是数据域产生了变化:

  • 如果这两部分数据域之间名字匹配而类型不匹配,那么对象输入流不会尝试将一种类型转换成另一种类型,因为这两个对象不兼容;
  • 如果被序列化的对象具有在当前版本中所没有的数据域,那么对象输入流会忽略这些额外的数据;
  • 如果当前版本具有在被序列化的对象中所没有的数据域,那么这些新添加的域将被设置成它们的默认值(如果是对象则是nu11,如果是数字则为0,如果是boolean值则是false)。

五、序列化的应用-深克隆

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person p1 = new Person("JonyJava", 18);
        Person p2 = (Person) p1.clone();
        p2.age++;
        System.out.println("p1 = " + p1);
        System.out.println("p2 = " + p2);
    }
}

class Person implements Serializable, Cloneable {
    public String name;
    public Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected Object clone() {
        try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bout);
            out.writeObject(this);

            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
            ObjectInputStream in = new ObjectInputStream(bin);
            return in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

分布式环境下的序列化

分布式环境下序列化的性能需求:

  1. 序列化的计算性能
  2. 数据包的大小
  3. 跨语言

这时候,Java原生的序列化就十分的局限了。目前市面上也出现了很多优秀的序列化工具,可以参考dubbo源码里面,支持的序列化方式:

![image-20200921183934996](4. 序列化与反序列化.assets/image-20200921183934996.png)

各种序列化技术的简单介绍

XML序列化框架

XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大, 而且效率不高,适用于对性能不高,而且QPS 较低的企业级内部系统之间的数据交换的场景, 同时XML又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟 知的Webservice,就是采用XML 格式对数据进行序列化的。XML 序列化/反序列化的实现方 式有很多,熟知的方式有 XStream和 Java 自带的XML序列化和反序列化两种

JSON序列化框架

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于XML来说,JSON 的字节流更小,而且可读性也非常好。现在 JSON数据格式在企业运用是最普遍的 JSON序列化常用的开源工具有很多

  1. Jackson (github.com/FasterXML/j…
  2. 阿里开源的 FastJson (github.com/alibaba/fas…
  3. Google 的GSON (github.com/google/gson)

这几种 json 序列化工具中,Jackson 与 fastjson 要比 GSON 的性能要好,但是 Jackson、 GSON的稳定性要比 Fastjson 好。而 fastjson 的优势在于提供的 api 非常容易使用

Hessian 序列化框架

Hessian 是一个支持跨语言传输的二进制序列化协议,相对于 Java 默认的序列化机制来说, Hessian 具有更好的性能和易用性,而且支持多种不同的语言实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构, 性能更高

Avro 序列化

Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持 二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro 提供的机制使动 态语言可以方便地处理Avro 数据。

kyro 序列化框架

Kryo 是一种非常成熟的序列化实现,已经在Hive、Storm中使用得比较广泛,不过它不能 跨语言. 目前 dubbo 已经在 2.6 版本支持 kyro 的序列化机制。它的性能要优于之前的 hessian2

Protobuf 序列化框架

Protobuf 是Google 的一种数据交换格式,它独立于语言、独立于平台。Google 提供了多种 语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件, Protobuf 是一个纯粹的表示层协议,可以和各种传输层协议一起使用。 Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要 求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应 用在对象的持久化场景中 但是要使用 Protobuf会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要 用到的话必须要去投入成本在这个技术的学习中

protobuf 有个缺点就是要传输的每一个类的结构都要生成对应的proto 文件,如果某个类发 生修改,还得重新生成该类对应的 proto 文件

序列化技术的选型参考

技术层面

  1. 序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能
  2. 序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间
  3. 序列化协议是否支持跨平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信 需求,那么这个是必须要考虑的
  4. 可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新, 这就要求我们采用的序列化协议基于良好的可扩展性/兼容性,比如在现有的序列化数据结 构中新增一个业务字段,不会影响到现有的服务
  5. 技术的流行程度,越流行的技术意味着使用的公司多,那么很多坑都已经淌过并且得到了解决,技术解决方案也相对成熟
  6. 学习难度和易用性

选型建议

  1. 对性能要求不高的场景,可以采用基于 XML的 SOAP 协议
  2. 对性能和间接性有比较高要求的场景,那么 Hessian、Protobuf、Thrift、Avro 都可以。
  3. 基于前后端分离,或者独立的对外的 api 服务,选用 JSON 是比较好的,对于调试、可读 性都很不错
  4. Avro 设计理念偏于动态类型语言,那么这类的场景使用Avro 是可以的

这个地址有针对不同序列化技术进行性能比较:github.com/eishay/jvms…