说一说关于序列化/反序列化中的细节问题

简介: 我是小假 期待与你的下一次相遇 ~

一、介绍

序列化和反序列化几乎是工程师们每天都需要面对的事情,尤其是当前流行的微服务开发。

光看定义上,对于初学者来说,可能很难一下子理解序列化的意义,尤其是面对这种特别学术词语的时候,内心会不由自主的发问:它到底是啥,用来干嘛的?

如果用通俗的方式来理解,可以用变魔术的方式来理解它,就好比把一件铁器从一个地方运往到另一个地方,在出发的时候,通过魔术方式将这个东西融化成一桶铁水,当到达目的地之后,又通过变魔术的方式,将这桶铁水还原成一件铁器。当铁器变成铁水的过程,可以理解为序列化;从铁水变成铁器,可以理解为反序列化。

站在程序世界的角度看,都知道计算机之间传递信息的最小单元是字节流,序列化其实就是将一个对象变成所有的计算机都能识别的字节流;反序列化就是将接受到的字节流还原成一个程序能识别的对象。

简单的说,序列化最终的目的是为了对象可以更方面的进行跨平台存储和进行网络传输。

基本上只要是涉及到跨平台存储或者进行网络传输的数据,都需要进行序列化。

互联网早期的序列化方式主要有COM和CORBA。

COM主要用于Windows平台,并没有真正实现跨平台,另外COM的序列化的原理利用了编译器中虚表,使得其学习成本巨大(想一下这个场景, 工程师需要是简单的序列化协议,但却要先掌握语言编译器)。由于序列化的数据与编译器紧耦合,扩展属性非常麻烦。

CORBA是早期比较好的实现了跨平台,跨语言的序列化协议。COBRA的主要问题是参与方过多带来的版本过多,版本之间兼容性较差,以及使用复杂晦涩。这些政治经济,技术实现以及早期设计不成熟的问题,最终导致COBRA的渐渐消亡。J2SE 1.3之后的版本提供了基于CORBA协议的RMI-IIOP技术,这使得Java开发者可以采用纯粹的Java语言进行CORBA的开发。

随着软件技术的快速发展,之后逐渐出现了比较流行的序列化方式,例如:XML、JSON、Protobuf、Thrift 和 Avro等等。

这些序列化方式各有千秋,不能简单的说哪一种序列化方式是最好的,只能从当时环境下去选择最适合序列化方式,如果要为公司项目进行序列化技术的选型,主要可以从以下几个方面进行考虑:

  • 是否支持跨平台:尤其是多种语言混合开发的项目,是否支持跨平台直接决定了系统开发难度
  • 序列化的速度:速度快的方式会为系统性能提升不少
  • 序列化出来的大小:数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能

应该如何使用序列化呢,以及序列化的过程中应该需要注意的问题。

下面,一起来了解一下!

二、代码实践

2.1、序列化操作

java 实现序列化方式非常简单,只需要实现Serializable接口即可,例如下面这个类。

  1. public class Student implements Serializable {
  2.    /**
  3.     * 用户名
  4.     */
  5.    private String name;
  6.    /**
  7.     * 年龄
  8.     */
  9.    private Integer age;
  10.    public Student(String name, Integer age) {
  11.        this.name = name;
  12.        this.age = age;
  13.    }
  14.    @Override
  15.    public String toString() {
  16.        return "Student1{" +
  17.                "name='" + name + '\'' +
  18.                ", age=" + age +
  19.                '}';
  20.    }
  21. }

来测试一下,将Student对象进行二进制的数据存储后,并从文件中读取数据出来转成Student对象,这个过程其实就是一个序列化和反序列化的过程。

  1. public class ObjectMainTest {
  2.    public static void main(String[] args) throws Exception {
  3.        //序列化
  4.        serializeAnimal();
  5.        //反序列化
  6.        deserializeAnimal();
  7.    }
  8.    private static void serializeAnimal() throws Exception {
  9.        Student black = new Student("张三", 20);
  10.        System.out.println(black.toString());
  11.        System.out.println("=================开始序列化================");
  12.        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.log"));
  13.        oos.writeObject(black);
  14.        oos.flush();
  15.        oos.close();
  16.    }
  17.    private static void deserializeAnimal() throws Exception {
  18.        System.out.println("=================开始反序列化================");
  19.        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.log"));
  20.        Student black = (Student) ois.readObject();
  21.        ois.close();
  22.        System.out.println(black.toString());
  23.    }
  24. }

输出结果:

  1. Student{name='张三', age=20}
  2. =================开始序列化================
  3. =================开始反序列化================
  4. Student{name='张三', age=20}

看起来是不是超级简单,但是别大意,这里面的坑还真不少,请看下面的问题汇总!

三、序列化问题汇总

3.1、static 属性不能被序列化

实际在序列化的时候,被static修饰的属性字段是不能被序列化进去的,因为静态变量属于类的状态,序列化并不保存静态变量!

3.2、Transient 属性不会被序列化

Transient修饰的属性无法被序列化,眼见为实,给Student类的name字段加一个transient修饰符。

  1. public class Student implements Serializable {
  2.    /**
  3.     * 用户名
  4.     */
  5.    private transient String name;
  6. //...省略
  7. }

运行测试方法,输出结果如下:

  1. Student{name='张三', age=20}
  2. =================开始序列化================
  3. =================开始反序列化================
  4. Student{name='null', age=20}

很明显,被transient修饰的name属性,反序列化后的结果为null

3.3、序列化版本号 serialVersionUID 问题

只要是实现了Serializable接口的类都会有一个版本号,如果没有定义,JDK 工具会按照对象的属性生成一个对应的版本号,当然还可以自定义,例如给Student类自定义一个序列化版本号,操作如下。

  1. public class Student implements Serializable {
  2. //自定义序列化版本号
  3.   private static final long serialVersionUID = 1l;
  4. //...省略
  5. }

如何验证这一点呢?

首先,先序列化一个Student对象,里面没有自定义版本号,然后在反序列化的时候,给这个对象自定义一个版本号,运行测试程序,看能不能反序列化成功?

  1. Exception in thread "main" java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student; local class incompatible: stream classdesc serialVersionUID = 821478144412499207, local class serialVersionUID = 1
  2. at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
  3. at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
  4. at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
  5. at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
  6. at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
  7. at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)

答案很明显,反序列化失败!

分析原因:Student对象序列化时的版本号是821478144412499207,反序列化时的版本号是1,两者不一致,导致无法反序列化成功!

当没有显式的自定义序列化版本号时,JDK 会根据当前对象的属性自动生成一个对象的版本号,只要对象的属性不会发生变化,这个版本号也基本上不会发生变化,但是当对象的属性发生了变化,对应的反序列化对象没有跟着一起变化,大概率会出现反序列化失败!

为了眼见为实,继续以实际案例给大家演示一下。

还是以上面那个为主,先序列化一个Student对象,里面没有自定义版本号,然后在反序列化操作的时候,给Student对象新增一个属性email,同时也不自定义版本号。

  1. public class Student implements Serializable {
  2.    /**
  3.     * 用户名
  4.     */
  5.    private String name;
  6.    /**
  7.     * 年龄
  8.     */
  9.    private Integer age;
  10.    /**
  11.     * 邮箱
  12.     */
  13.    private String email;
  14.    //省略set、get...
  15. }

看看运行效果:

  1. Exception in thread "main" java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student; local class incompatible: stream classdesc serialVersionUID = 821478144412499207, local class serialVersionUID = -5996907635197467174
  2. at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
  3. at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
  4. at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
  5. at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
  6. at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
  7. at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)

答案很显然,反序列化报错了!两者的版本号不一致!

在平时开发的过程中,实体类的属性难免会发生改动,在写代码的时候只是把序列化的接口实现了,但是没有自定义版本号,在这点上,强烈建议大家一定要给每个实现了Serializable接口的类,自定义一个版本号,即使对象的属性发生了变化,也不会影响到数据的序列化和反序列化操作!

操作很简单,直接在实体类里面加上这个静态变量即可!

  1. //自定义序列化版本号
  2. private static final long serialVersionUID = 1l;

3.4、父类、子类序列化问题

在实际的开发过程中,尤其是实体类,为了对象属性的复用,往往会采用继承的方式来处理。

使用了继承之后,父类属性是否可以正常被序列化呢?下面来看看!

  • 父类没有实现序列化,子类实现序列化

首先创建两个类ParentChildChild继承自Parent

  1. public class Parent {
  2.    private String name;
  3.    public String getName() {
  4.        return name;
  5.    }
  6.    public Parent setName(String name) {
  7.        this.name = name;
  8.        return this;
  9.    }
  10. }
  11. public class Child extends Parent implements Serializable{
  12.    private static final long serialVersionUID = 1l;
  13.    private String id;
  14.    public String getId() {
  15.        return id;
  16.    }
  17.    public Child setId(String id) {
  18.        this.id = id;
  19.        return this;
  20.    }
  21. }

编写测试类,先序列化,然后再反序列化!

  1. public class ObjectMainTest {
  2.    public static void main(String[] args) throws Exception {
  3.        serializeAnimal();
  4.        deserializeAnimal();
  5.    }
  6.    private static void serializeAnimal() throws Exception {
  7.        Child black = new Child();
  8.        black.setId("123");
  9.        black.setName("张三");
  10.        System.out.println("id:" +  black.getId() + ",name:" +  black.getName());
  11.        System.out.println("=================开始序列化================");
  12.        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.log"));
  13.        oos.writeObject(black);
  14.        oos.flush();
  15.        oos.close();
  16.    }
  17.    private static void deserializeAnimal() throws Exception {
  18.        System.out.println("=================开始反序列化================");
  19.        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.log"));
  20.        Child black = (Child) ois.readObject();
  21.        ois.close();
  22.        System.out.println("id:" +  black.getId() + ",name:" +  black.getName());
  23.    }
  24. }

运行结果如下:

  1. id:123,name:张三
  2. =================开始序列化================
  3. =================开始反序列化================
  4. id:123,name:null

结果很明显,父类的属性没有被序列化进去!

再来试试,另一种常见

  • 父类实现序列化,子类不实现序列化 ```java public class Parent implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    public String getName() {
  1.  return name;
  • }
    public Parent setName(String name) {
  1.  this.name = name;
  2.  return this;
  • }

}

public class Child extends Parent {

  1. private String id;
  2. public String getId() {
  3.    return id;
  4. }
  5. public Child setId(String id) {
  6.    this.id = id;
  7.    return this;
  8. }

}

  1. 接着运行一次程序,结果如下!
  2. ```java
  3. id:123,name:张三
  4. =================开始序列化================
  5. =================开始反序列化================
  6. id:123,name:张三

结果很明显,父类的属性被序列化进去!

假如,子类和父类,都实现了序列化,并且序列化版本号都不一样,会不会出现问题呢?

  • 父类实现序列化,子类实现序列化 ```java public class Parent implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    public String getName() {
  1.  return name;
  • }
    public Parent setName(String name) {
  1.  this.name = name;
  2.  return this;
  • }

}

public class Child extends Parent implements Serializable{

  1. private static final long serialVersionUID = 2l;
  2. private String id;
  3. public String getId() {
  4.    return id;
  5. }
  6. public Child setId(String id) {
  7.    this.id = id;
  8.    return this;
  9. }

}

  1. 运行一次程序,结果如下!
  2. ```java
  3. id:123,name:张三
  4. =================开始序列化================
  5. =================开始反序列化================
  6. id:123,name:张三

父类的属性序列化依然成功,当父、子类都实现了序列化,并且定义了不同的版本号,这种情况下,版本号是跟着子类的版本号走的!

总结起来,当父类实现序列化时,子类所有的属性也会全部被序列化;但是当父类没有实现序列化,子类在序列化时,父类属性并不会被序列化!

3.5、自定义序列化过程

Serializable接口内部序列化是 JVM 自动实现的,但是在某些少数的场景下,可能想自定义序列化和反序列化的内容,但是又不想改实体类属性,这个时候可以采用自定义序列化的实现方式。

自定义序列化方式,其实也很简单,只需要实现 JDK 自身提供的Externalizable接口就行,里面有两个核心方法,一个是数据写入,另一个是数据的读取。

  1. public interface Externalizable extends java.io.Serializable {
  2.    void writeExternal(ObjectOutput out) throws IOException;
  3.    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
  4. }

Externalizable接口的实现过程也很简单,创建一个Person,实现自Externalizable的两个方法。

  1. public class Person implements Externalizable {
  2.    private static final long serialVersionUID = 1l;
  3.    private String name;
  4.    private int age;
  5.    /**
  6.     * 实现了Externalizable这个接口时需要提供无参构造,在反序列化时会检测
  7.     */
  8.    public Person() {
  9.        System.out.println("Person: empty");
  10.    }
  11.    public Person(String name, int age) {
  12.        this.name = name;
  13.        this.age = age;
  14.    }
  15.    @Override
  16.    public void writeExternal(ObjectOutput out) throws IOException {
  17.        System.out.println("person writeExternal...");
  18.        out.writeObject(name);
  19.        out.writeInt(age);
  20.    }
  21.    @Override
  22.    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
  23.        System.out.println("person readExternal...");
  24.        name = (String) in.readObject();
  25.        age = in.readInt();
  26.    }
  27.    @Override
  28.    public String toString() {
  29.        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
  30.    }
  31. }

测试Person对象的序列化和反序列化。

  1. public class ExternalizableMain {
  2.    public static void main(String[] args) throws IOException, ClassNotFoundException {
  3.        serializable();
  4.        deserializable();
  5.    }
  6.    private static void serializable() throws IOException {
  7.        Person person = new Person("张三", 15);
  8.        System.out.println(person.toString());
  9.        System.out.println("=================开始序列化================");
  10.        FileOutputStream boas = new FileOutputStream("person.log");
  11.        ObjectOutputStream oos = new ObjectOutputStream(boas);
  12.        oos.writeObject(person);
  13.        oos.close();
  14.        boas.close();
  15.    }
  16.    private static void deserializable() throws IOException, ClassNotFoundException {
  17.        System.out.println("============反序列化=============");
  18.        ObjectInputStream bis = new ObjectInputStream(new FileInputStream("person.log"));
  19.        Person person = (Person)bis.readObject();
  20.        System.out.println(person.toString());
  21.    }
  22. }

运行结果如下:

  1. Person{name='张三', age=15}
  2. =================开始序列化================
  3. person writeExternal...
  4. ============反序列化=============
  5. Person: empty
  6. person readExternal...
  7. Person{name='张三', age=15}

四、小结

对象的序列化,在实际的开发过程中,使用的非常频繁,尤其是微服务开发,如果用的是SpringBoot + Dubbo组合的框架,那么在通过rpc调用的时候,如果传输的对象没有实现序列化,会直接报错!

在使用序列化的时候,坑点还不少,尤其是版本号的问题,这个很容易被忽略,在实际开发的时候,强烈推荐自定义版本号,这样可以避免传输的对象属性发生变化的时候,接口反序列化出错的概率!


相关文章
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
189 1
|
2月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
213 1
|
6月前
|
JSON Java 数据库连接
|
7月前
|
存储 安全 IDE
说一说序列化与反序列化中存在的问题
本文详细解析了Java中的序列化机制,包括序列化的概念、实现方式及应用场景。通过Student类的实例演示了对象的序列化与反序列化过程,并分析了`Serializable`接口的作用以及`serialVersionUID`的重要意义。此外,文章还探讨了如何通过自定义`readObject()`方法增强序列化的安全性,以及解决可序列化单例模式中可能产生的多实例问题。最后提供了代码示例和运行结果,帮助读者深入理解序列化的原理与实践技巧。
181 2
|
7月前
|
JSON JavaScript 前端开发
Go语言JSON 序列化与反序列化 -《Go语言实战指南》
本文介绍了 Go 语言中使用 `encoding/json` 包实现 JSON 与数据结构之间的转换。内容涵盖序列化(`Marshal`)和反序列化(`Unmarshal`),包括基本示例、结构体字段标签的使用、控制字段行为的标签(如 `omitempty` 和 `-`)、处理 `map` 和切片、嵌套结构体序列化、反序列化未知结构(使用 `map[string]interface{}`)以及 JSON 数组的解析。最后通过表格总结了序列化与反序列化的方法及类型要求,帮助开发者快速掌握 JSON 数据处理技巧。
|
JSON 数据格式 索引
Python中序列化/反序列化JSON格式的数据
【11月更文挑战第4天】本文介绍了 Python 中使用 `json` 模块进行序列化和反序列化的操作。序列化是指将 Python 对象(如字典、列表)转换为 JSON 字符串,主要使用 `json.dumps` 方法。示例包括基本的字典和列表序列化,以及自定义类的序列化。反序列化则是将 JSON 字符串转换回 Python 对象,使用 `json.loads` 方法。文中还提供了具体的代码示例,展示了如何处理不同类型的 Python 对象。
508 1
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
存储 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第9天】在Java的世界里,对象序列化是连接数据持久化与网络通信的桥梁。本文将深入探讨Java对象序列化的机制、实践方法及反序列化过程,通过代码示例揭示其背后的原理。从基础概念到高级应用,我们将一步步揭开序列化技术的神秘面纱,让读者能够掌握这一强大工具,以应对数据存储和传输的挑战。
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第3天】在Java编程的世界里,对象序列化与反序列化是实现数据持久化和网络传输的关键技术。本文将深入探讨Java序列化的原理、应用场景以及如何通过代码示例实现对象的序列化与反序列化过程。从基础概念到实践操作,我们将一步步揭示这一技术的魅力所在。

热门文章

最新文章