序列化与反序列化


在Java开发中常会听到序列化与反序列化,特别是Web应用开发时,网络之间需要传输对象用到序列化的频率非常频繁。在此总结一下序列化的原理,在Java中实现序列化的常用方法是实现Serializable接口。

  • 序列化:把Java对象转换为字节序列的过程。
  • 反序列化:把字节序列恢复为Java对象的过程。

对象的序列化主要有两种用途

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象)
  • 在网络上传送对象的字节序列。(网络传输对象)

序列化在传递和保存对象时,保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。核心作用就是对象状态的保存和重建

序列化

序列化可以把一个对象转换成流的形势,可以用来网络传输或者保存起来。比如你为你的程序建立了一个任务队列来下载东西,可是你的队列在内存里,重启后就没了,所以,为了保存写个队列,对这个对象进行序列化,再保存成文件,下次启动时候反序列化来重新得到这个对象实例。在Java中,如果一个对象要想实现序列化,必须要实现下面两个接口之一:Serializable 接口Externalizable 接口那这两个接口是如何工作的呢?

Serializable 接口

一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰,static修饰的字段也不会被序列化。由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。

Externalizable 接口

它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。对实现Externalizable接口的对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。

对比

当类实现Serializable的接口时,Jvm完全负责序列化,而使用Externalizable的情况下,程序员负责整个序列化和反序列化过程,例如你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加上transient就可以实现了。如果要定义很多的特殊处理,比如给某个字段序列化时加密,反序列化时解密,就可以使用Externalizable。代码如下

package com.serialize;

import java.io.*;

/**
 * @Auther: Lushunjian
 * @Date: 2020/10/3 13:10
 * @Description:
 */
public class Person implements Externalizable {

    private String name;
    private String color;

    public String getName() { return name; }

    public void setName(String name) { this.name = name; }

    public String getColor() { return color; }

    public void setColor(String color) { this.color = color; }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name+"123");
        out.writeObject(color+"456");
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 读取字段时,可对字段进行处理
        name = (String)in.readObject();
        color = (String)in.readObject();
    }
}

测试类

package com.serialize;

import java.io.*;

/**
 * @Auther: Lushunjian
 * @Date: 2020/10/3 13:11
 * @Description:
 */
public class SerializableTest {

    private static void serialize(Person user) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("F:\\test\\person.txt")));
        oos.writeObject(user);
        oos.close();
    }

    private static Person deserialize() throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("F:\\test\\person.txt")));
        return (Person) ois.readObject();
    }

    public static void main(String[] args) throws Exception {
        Person person = new Person();
        person.setName("张三");
        person.setColor("黄种人");
        serialize(person);

        Person person1 = deserialize();
        System.out.println(person1.getName());
        System.out.println(person1.getColor());
    }
}

结果如下

张三123
黄种人456

Serializable接口使用反射和元数据,相对较慢的性能,而Externalizable 接口性能比较好

注意点

  1. 序列化时,只对对象的状态进行保存,而不管对象的方法;
  2. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
  3. 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化
  4. 并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如
    1. 安全方面的原因,有些对象涉及到服务的一些敏感信息,不允许被序列化
    2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现
  5. 声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。
  6. 序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途
    1. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID
    2. 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID

FastJson

JSON协议使用方便,越来越流行。JSON的处理器有很多。我们需要一个性能很好的JSON Parser,希望JSON Parser的性能有二进制协议一样好,比如和protobuf一样,这可不容易,但确实做到了。有人认为这从原理上就是不可能的,但是计算机乃实践科学,看实际的结果比原理推导更重要。 在国内JSON处理器比较主流的就是阿里巴巴开源的FastJson,和谷歌的Gson,下面是一些JSON工具的性能对比

序列化时间 反序列化时间 大小 压缩后大小
java序列化 8654 43787 889 541
hessian 6725 10460 501 313
protobuf 2964 1745 239 149
thrift 3177 1949 349 197
avro 3520 1948 221 133
json-lib 45788 149741 485 263
jackson 3052 4161 503 271
fastjson 2595 1472 468 251

这是一个468bytes的JSON Bytes测试,从测试结果来看,无论序列化和反序列化,Fastjson超越了protobuf,可以当之无愧fast! 它比java deserialize快超过30多倍,比json-lib快100倍。由于Fastjson的存在,你可以放心使用json统一协议,达到文本协议的可维护性,二进制协议的性能。

JSON处理主要包括两个部分,serialize和deserialize。Serialize就是把Java对象变成JSON String或者JSON Bytes。Deserialize是把JSON String或者Json Bytes变成java对象。其实这个过程有些JSON库是分三部分的,json string <--> json tree <--> java object。Fastjson也支持这种转换方式,但是这种转换方式因为有多余的步骤,性能不好,不推荐使用。

为什么Fastjson能够做到这么快

任何序列化工具都绕不开Serialzie和Deserializer,因此FastJson同样是围绕这两个步骤做了很多优化处理,下面分别对fastjson这两个步骤的优化说明,deserializer也称为parser或者decoder,fastjson在这方面投入的优化精力最多

Fastjson中Serialzie的优化实现

  1. 自行编写类似StringBuilder的工具类SerializeWriter

    把Java对象序列化成json文本,是不可能使用字符串直接拼接的,因为这样性能很差。比字符串拼接更好的办法是使用java.lang.StringBuilder。StringBuilder虽然速度很好了,但还能够进一步提升性能的,fastjson中提供了一个类似StringBuilder的类 com.alibaba.fastjson.serializer.SerializeWriter。 SerializeWriter提供一些针对性的方法减少数组越界检查。例如public void writeIntAndChar(int i, char c) {},这样的方法一次性把两个值写到buf中去,能够减少一次越界检查。目前SerializeWriter还有一些关键的方法能够减少越界检查的,我还没实现。也就是说,如果实现了,能够进一步提升serialize的性能。

  2. 使用ThreadLocal来缓存buf

    这个办法能够减少对象分配和gc,从而提升性能。SerializeWriter中包含了一个char[] buf,每序列化一次,都要做一次分配,使用ThreadLocal优化,能够提升性能

  3. 使用asm避免反射

    获取java bean的属性值,需要调用反射,fastjson引入了asm的来避免反射导致的开销。fastjson内置的asm是基于objectweb asm 3.3.1改造的,只保留必要的部分,fastjson asm部分不到1000行代码,引入了asm的同时不导致大小变大太多。

  4. 使用一个特殊的IdentityHashMap优化性能

    fastjson对每种类型使用一种serializer,于是就存在class -> JavaBeanSerizlier的映射。fastjson使用IdentityHashMap而不是HashMap,避免equals操作。我们知道HashMap的算法的transfer操作,并发时可能导致死循环,但是ConcurrentHashMap比HashMap系列会慢,因为其使用volatile和lock。fastjson自己实现了一个特别的IdentityHashMap,去掉transfer操作的IdentityHashMap,能够在并发时工作,但是不会导致死循环

  5. 缺省启用sort field输出

    son的object是一种key/value结构,正常的hashmap是无序的,fastjson缺省是排序输出的,这是为deserialize优化做准备

  6. 集成jdk实现的一些优化算法

    在优化fastjson的过程中,参考了jdk内部实现的算法,比如int to char[]算法等等

Fastjson的deserializer的主要优化

  1. 读取token基于预测

    所有的parser基本上都需要做词法处理,json也不例外。fastjson词法处理的时候,使用了基于预测的优化算法。比如key之后,最大的可能是冒号”:”,value之后,可能是有两个,逗号”,”或者右括号”}”。在com.alibaba.fastjson.parser.JSONScanner中提供了这样的方法。源码如下:

    public void nextToken(int expect) {  
        for (;;) {  
            switch (expect) {  
                case JSONToken.COMMA: //   
                    if (ch == ',') {  
                        token = JSONToken.COMMA;  
                        ch = buf[++bp];  
                        return;  
                    }  
    
                    if (ch == '}') {  
                        token = JSONToken.RBRACE;  
                        ch = buf[++bp];  
                        return;  
                    }  
    
                    if (ch == ']') {  
                        token = JSONToken.RBRACKET;  
                        ch = buf[++bp];  
                        return;  
                    }  
    
                    if (ch == EOI) {  
                        token = JSONToken.EOF;  
                        return;  
                    }  
                    break;  
            // ... ...  
        }  
    }  
  2. sort field fast match算法

    fastjson的serialize是按照key的顺序进行的,于是fastjson做deserializer时候,采用一种优化算法,就是假设key/value的内容是有序的,读取的时候只需要做key的匹配,而不需要把key从输入中读取出来。通过这个优化,使得fastjson在处理json文本的时候,少读取超过50%的token,这个是一个十分关键的优化算法。基于这个算法,使用asm实现,性能提升十分明显,超过300%的性能提升。

  3. 使用asm避免反射

    deserialize的时候,会使用asm来构造对象,并且做batch set,也就是说合并连续调用多个setter方法,而不是分散调用,这个能够提升性能。

  4. 对utf-8的json bytes,针对性使用优化的版本来转换编码

    这个类是com.alibaba.fastjson.util.UTF8Decoder,来源于JDK中的UTF8Decoder,但是它使用ThreadLocal Cache Buffer,避免转换时分配char[]的开销。
    ThreadLocal Cache的实现是这个类com.alibaba.fastjson.util.ThreadLocalCache。第一次1k,如果不够,会增长,最多增长到128k。

    //代码摘抄自com.alibaba.fastjson.JSON  
    public static final <T> T parseObject(byte[] input, int off, int len, CharsetDecoder charsetDecoder, Type clazz,  Feature... features) {  
        charsetDecoder.reset();  
        int scaleLength = (int) (len * (double) charsetDecoder.maxCharsPerByte());  
         // 使用ThreadLocalCache,避免频繁分配内存  
        char[] chars = ThreadLocalCache.getChars(scaleLength);
        ByteBuffer byteBuf = ByteBuffer.wrap(input, off, len);  
        CharBuffer charByte = CharBuffer.wrap(chars);  
        IOUtils.decode(charsetDecoder, byteBuf, charByte);  
        int position = charByte.position();  
    
        return (T) parseObject(chars, position, clazz, features);  
    }  
  5. symbolTable算法

    在看xml或者javac的parser实现,经常会看到有一个这样的东西symbol table,它就是把一些经常使用的关键字缓存起来,在遍历char[]的时候,同时把hash计算好,通过这个hash值在hashtable中来获取缓存好的symbol,避免创建新的字符串对象。这种优化在fastjson里面用在key的读取,以及enum value的读取。这是也是parse性能优化的关键算法之一,以下是摘抄自JSONScanner类中的代码,这段代码用于读取类型为enum的value。

    int hash = 0;  
    for (;;) {  
        ch = buf[index++];  
        if (ch == '\"') {  
            bp = index;  
            this.ch = ch = buf[bp];  
            strVal = symbolTable.addSymbol(buf, start, index - start - 1, hash); // 通过symbolTable来获得缓存好的symbol,包括fieldName、enumValue  
            break;  
        }  
    
        hash = 31 * hash + ch; // 在token scan的过程中计算好hash  
    
        // ... ...  
    }  

Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
编译原理(一) 编译原理(一)
对于计算机科学的专业来说,编译原理是一门必修课。尽管在大学时代,我也研读过编译原理,但当时只看得云里雾里,不知所以。工作多年后,重新审视编程语言时有诸多疑问,例如编程语言与我们日常生活所说的自然语言有什么区别,为什么设计编程语言时不能设计的
2020-10-04
Next 
spring的切面编程 spring的切面编程
Spring框架的AOP机制(切面编程)可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。从而使得业务逻辑各部分之间的耦合度
2020-09-26
  TOC