5.3 序列化

序列化


什么是序列化?

​ 在讨论序列化之前,我们需要对序列化这三个字有一个清晰的认知。

​ 摘自维基百科的解释是这样子的:
​ 序列化是指在计算机科学的资料处理中,将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。从一系列字节提取数据结构的反向操作,是反序列化。

​ 可以直观的感受到,序列化和反序列化,其实就是一组数据的编解码过程,用于数据的传输和存储。

​ 至于为什么需要序列化,我想,通过上文的描述,大家应该也可以想到了。

​ 我们可以想到平时在开发的时候,数据的两种常用表示形式,基于内存和基于外存。
​ 在内存中,我们的数据会保存在数据结构中,可以说数据结构和算法是程序的灵魂。
​ 在外存中,我们会将数据序列化后写入磁盘,或者通过网络发送给其他外部介质。

序列化方案的关键点

跨平台

​ 这是一个很重要的特性,这个特性会影响到该序列化方案的流行度和通用性,在一个公司中往往会存在多种编程语言的服务,服务和服务之间的调用可能是跨语言的,这个时候如果序列化方案支持跨平台跨语言,那么很有可能会被技术选型所采纳。而采纳的公司越多,则更容易推进该方案的发展。这是一个良性的循环

可读性

​ 这是一个对于我们研发人员的视角来说的一个关键点,如果序列化后的数据是人眼可读的,那么我们就能很快的定位到问题进行修复。

安全性

​ 序列化的安全性是很重要的一个点。以比较出名的例子来说,2015年11月,Apache Commons Collections 被爆出存在反序列化漏洞,能够导致任务代码执行。感兴趣的可以看一下这篇文章:https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/

性能和大小

​ 这个其实是序列化方案的重头戏,因为序列化和反序列化其实就是一个加密和解密的过程,其加解密的效率越高服务的性能就越高。此外,序列化后的数据大小也是一个很关键的因素,数据越小,在网络中传输所占用的资源就越少,也是提升程序性能的重要手段。

Java 的序列化实现

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = -6768869063601481932L;

    private String name;

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializationProcessor {
    public static void main(String[] args) {
        String filePath = "out.txt";

        try (FileOutputStream fileOutStream = new FileOutputStream(filePath);
             ObjectOutputStream objOutStream = new ObjectOutputStream(fileOutStream)) {
            User user = new User();
            user.setName("caitiezhu");

            objOutStream.writeObject(user);
            objOutStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializationProcessor {
    public static void main(String[] args) {
        String filePath = "out.txt";

        try (FileInputStream fileInStream = new FileInputStream(filePath);
             ObjectInputStream objInStream = new ObjectInputStream(fileInStream)) {

            User user = (User) objInStream.readObject();

            System.out.println(user);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

​ 执行上述的代码,最终我们可以在控制台看到User{name='caitiezhu'},这是一个很简单的序列化和反序列化demo。

od -d out.txt

​ 执行上述指令,我们可以在控制台看到如下输出:

0000000     60844    1280   29299    1024   29525   29285    4258   29477
0000020     59164   13335       2   19457    1024   24942   25965     116
0000040     19474   24938   24950   27695   28257   12135   29779   26994
0000060     26478   30779   29808    2304   24931   29801   25961   26746
0000100       117
0000101

可以以此来说明,java 的原生序列化输出后是人眼不可读的。

Serializable 和 serialVersionUID

​ 在上述的demo中,我们为 User 实现了一个interface Serializable,通过查看java.io.Serializable的注释,我们可以看到

Serializability of a class is enabled by the class implementing the java.io.Serializable interface. Classes that do not implement this interface will not have any of their state serialized or deserialized.  
All subtypes of a serializable class are themselves serializable.  
The serialization interface has no methods or fields and serves only to identify the semantics of being serializable.

类的可序列化由 java.io.Serializable 实现。不实现此接口的类将不会进行任何序列化或者反序列化操作。可序列化类的所有子类型本身都是可序列化的。 java.io.Serializable 仅作为标记接口,不存在任何方法或字段。
The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization.
If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender's class, then deserialization will result in an {@link InvalidClassException}.  
A serializable class can declare its own serialVersionUID explicitly by declaring a field named "serialVersionUID" that must be static, final, and of type long.

序列化操作在运行时会将一个名为 serialVersionUID 的版本号标记和每个关联的类绑定,其作用是在反序列化的时候,验证发送方和接受方使用的是可兼容的对象。如果反序列化的接受方使用的 serialVersionUID 和序列化的发送方使用的不同时,会抛出 InvalidClassException 异常。此外,serialVersionUID 一定是 static final long 的类型。

​ 也许我们在写代码的时候,有时候忘记写了serialVersionUID这个属性,但是程序还是能照常执行,别急,让我们继续来看注释,源码和注释是最好的文档。

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.  
However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassException during deserialization.  
Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value.  
It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class--serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes.

如果可序列化类中没有显示声明 serialVersionUID,则序列化执行的时候会基于当前的类,计算一个默认的 serialVersionUID。但是在 java 对象序列化规范中提到,强烈建议所有的可序列化类显示声明 serialVersionUID,因为默认的 serialVersionUID 是基于编译器实现而变化的高度敏感信息,很有可能在反序列化期间导致 InvalidClassException 异常。
因此为了保证不同编译器之间的 serialVersionUID 一直,可序列化强烈建议显示声明 serialVersionUID。
此外还强烈建议将 serialVersionUID 设置为 private,因为 serialVersionUID 只是标记当前类,对于继承没有任何的用处。

​ 上述的注释就很好的解释了,为什么我们在编写程序的时候,或者通过 ide 自动生成 serialVersionUID 时,要将属性设置为private static final long。对于一个知识,我们不然要知其然,还要知其所以然。

序列化技术选型

JSON 和 XML

​ JSON 和 XML 都是人眼可读的数据形式。这里插一嘴,其实 CSV 也可以作为序列化方案。相比于 JAVA 的原生序列化方案,JSON 和 XML 都是跨平台跨语言的。XML 经常被人诟病太多标签,太过复杂。而 JSON 则相对精简,而且因为它在 Web 浏览器中内置支持,所以 JSON 的流行度会远远高于 XML。

其他序列化方案
  • Protocol Buffers
    • 优点:性能高,有自定义的代码生成机制;向前/向后兼容;跨平台;网络开销小
    • 缺点:序列化后变为二进制格式,可读性差
  • Thrift
    • 和 Protocol Buffers 的优缺点基本相似,但是两者对比下来 Protocol Buffers 的功能可能相对单一,Thrift 的功能丰富,但是相对的学习成本也高。和 Protocol Buffers 不同,Protocol Buffers 只是一个单纯的序列化机制,而 Thrift 则提供了全套的 RPC 解决方案。

全部评论

相关推荐

喜欢核冬天的哈基米很想上市:会爆NullPointerException的
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务