Java序列化原理与使用详解(含4种序列化方式)

序列化

序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。

序列化机制使得对象可以脱离程序的运行而独立存在。

  • 序列化:将一个Java对象写入IO流中
  • 反序列化:从IO流中恢复该Java对象

本文中用序列化来简称整个序列化和反序列化机制。

 

为什么需要序列化

之所以需要序列化和反序列化,主要是因为Java对象是在JVM中生成的,是内存中的数据。

如果需要把对象的字节序列远程传输或保存到硬盘上时,你就需要将Java对象转换成二进制流,这个转换过程就是序列化。

假如别人传给你一个二进制流数据,当你想要恢复成内存中的对象时,你就需要反序列化。

网络通信时,无论是何种类型的数据,在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常。

比如:RMI(Remote Method Invoke,即远程方法调用)过程中的参数和返回值,都会转成字节序列的形式在网络上传送。

发送方需要把这个Java对象转换为字节序列,才能在网络上传送,接收方则需要把字节序列再恢复为Java对象。

 

如何实现序列化

如果一个类的对象需要序列化,那么在Java语法层面,这个类需要:

  • 实现Serializable接口
  • 使用ObjectOutputStream将对象输出到流,实现对象的序列化;使用ObjectInputStream从流中读取对象,实现对象的反序列化。
  1. 首先实体类要实现 Serializable 接口
public class Student implements java.io.Serializable {
    private String name;
    private int age;
    // getter setter
    ...
}
  1. 然后可以使用 ObjectOutStream 序列化到本地文件
// 创建输出流
ObjectOutStream out = new ObjectOutputStream(new FileOutputStream("student.dat"))
// 创建需要序列化的对象    
Student jack = new Student("Jack", 21);
Student jim = new Student("Jim", 20);
// 写入流
out.writeObject(jack);
out.writeObject(jim);

 

JAVA 序列化的方式

1.Java原生序列化

在类中实现Serializable接口,通过objectinputstream进行序列化等。

建议指定 serialVersionUID 字段避免版本升级造成序列化和反序列化失败。

优点:

  • 简单

缺点:

  • 序列化码流太大
  • 序列化效率低
  • 无法跨语言

 

2.Hessian序列化

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。

Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。

优点:

  • 相对于JDk,JSON,更加高效,生成的字节数更小
  • 有非常好的兼容性和稳定性

缺点:

  • 官方版本对Java里面一些常见对象的类型不支持,
  • 比如LinkedHashMap、LinkedHashSet 等,但是可以通过扩展CollectionDeserializer 类修复,
  • Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
  • Byte/Short 反序列化的时候变成 Integer

3.Json序列化

通过 JSON 静态类进行序列化,转成 JSON 字符串。

优点:

  • 简洁明了

缺点:

  • JSON进行序列化的额外空间开销比较大,对于大数据量服务就意味着需要巨大的内存和磁盘开销
  • JSON没有类型,但像Java这种强类型语言,需要通过反射统一解决,所以性能不会太好

4.Protobuf序列化

Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。

Protobuf使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL编译器,生成序列化工具类。

优点:

  • 高效
  • 支持多种语言
  • 支持向前,向后兼容

缺点:

  • 为了提高性能,protobuf采用了二进制格式进行编码。这直接导致了可读性差
  • 对于具有反射和动态语言来讲,用起来比较费劲

 

Java序列化的例子

我们看下我们很常用的java ArrayList 的源码是怎么完成序列化的

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;
    
    ···
    
    /**
     * Save the state of the <tt>ArrayList</tt> instance to a stream (that
     * is, serialize it).
     *
     * @serialData The length of the array backing the <tt>ArrayList</tt>
     *             instance is emitted (int), followed by all of its elements
     *             (each an <tt>Object</tt>) in the proper order.
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

    /**
     * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
     * deserialize it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }
  1. 他实现了 java.io.Serializable
  2. 他在字段 elementData 数组上面用了 transient 关键字
  3. 他还写了writeObject 和 readObject 方法

你如果用IDE查询的话发现这两个方法没实现任何接口,那是再什么时候调用的呢? 经过一番查阅资料发现 java.io.Serializable 的注释里面有写道

Classes that require special handling during the serialization and
deserialization process must implement special methods with these exact
signatures:
<PRE>
private void writeObject(java.io.ObjectOutputStream out)
    throws IOException
private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;
private void readObjectNoData()
    throws ObjectStreamException;
</PRE>
···
@author  unascribed
@see java.io.ObjectOutputStream
@see java.io.ObjectInputStream
@see java.io.ObjectOutput
@see java.io.ObjectInput
@see java.io.Externalizable
@since   JDK1.1

也就是说这两个方法是特殊的回调方法, 当你的实体类很特殊需要手动序列化的时候就可以手动实现这两个方法

然后你可以返回去细品 ArrayList 是把 elementData 数组循环的writeObject 了

Java序列化原理与使用详解(含4种序列化方式)-mikechen

序列化的使用场景

1.所有可在网络上传输的对象都必须是可序列化的。

2.所有需要保存到磁盘的java对象都必须是可序列化的。

它是一种存储方式,只要你需要你就可以去用。

序列化总结

  1. 序列化使用 java.io.Serializable 接口
  2. 静态字段不会被序列化
  3. 字段屏蔽用 transient 关键字
  4. 自定义序列化编写 readObject 和 writeObject 方法
  5. 写入顺序和读取顺序要一致,就算不用也需要read一下。

陈睿mikechen

10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复面试即可获取《史上最全阿里Java面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法