写点什么

一文带你全面了解 java 对象的序列化和反序列化

发布于: 2021 年 05 月 10 日

​​​​​​摘要:这篇文章主要给大家介绍了关于 java 中对象的序列化与反序列化的相关内容,文中通过详细示例代码介绍,希望能对大家有所帮助。


本文分享自华为云社区《java中什么是序列化和反序列化?》,原文作者:dayu_dls 。

 

这篇文章主要给大家介绍了关于 java 中对象的序列化与反序列化的相关内容,文中通过详细示例代码介绍,希望能对大家有所帮助。

1、序列化是干啥用的?


序列化的原本意图是希望对一个 Java 对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,所以概念上很好理解:


  • 序列化:把 Java 对象转换为字节序列。

  • 反序列化:把字节序列恢复为原先的 Java 对象。


而且序列化机制从某种意义上来说也弥补了平台化的一些差异,毕竟转换后的字节流可以在其他平台上进行反序列化来恢复对象。

2、对象序列化的方式?


在 Java 中,如果一个对象要想实现序列化,必须要实现下面两个接口之一:


  • Serializable 接口

  • Externalizable 接口


那这两个接口是如何工作的呢?两者又有什么关系呢?我们分别进行介绍。


2.1 Serializable 接口


一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。


这个对象的所有属性(包括 private 属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用 transient 修饰。


由于 Serializable 对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此 Serializable 类无需默认构造函数,但是当 Serializable 类的父类没有实现 Serializable 接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。


使用 transient 关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的 Java 类里加入 writeObject()方法与 readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。


2.2 Externalizable 接口


它是 Serializable 接口的子类,用户要实现的 writeExternal()和 readExternal() 方法,用来决定如何序列化和反序列化。


因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而 transient 在这里无效。


对 Externalizable 对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为 private、默认或 protected 级别,会抛出 java.io.InvalidException:no valid constructor 异常,因此 Externalizable 对象必须有默认构造函数,而且必需是 public 的。


2.3 对比


使用时,你只想隐藏一个属性,比如用户对象 user 的密码 pwd,如果使用 Externalizable,并除了 pwd 之外的每个属性都写在 writeExternal()方法里,这样显得麻烦,可以使用 Serializable 接口,并在要隐藏的属性 pwd 前面加上 transient 就可以实现了。如果要定义很多的特殊处理,就可以使用 Externalizable。


当然这里我们有一些疑惑,Serializable 中的 writeObject()方法与 readObject()方法科可以实现自定义序列化,而 Externalizable 中的 writeExternal()和 readExternal() 方法也可以,他们有什么异同呢?


  • readExternal(),writeExternal()两个方法,这两个方法除了方法签名和 readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。


  • 需要指出的是,当使用 Externalizable 机制反序列化该对象时,程序会使用 public 的无参构造器创建实例,然后才执行 readExternal()方法进行反序列化,因此实现 Externalizable 的序列化类必须提供 public 的无参构造。


  • 虽然实现 Externalizable 接口能带来一定的性能提升,但由于实现 ExternaLizable 接口导致了编程复杂度的增加,所以大部分时候都是采用实现 Serializable 接口方式来实现序列化。

3、Serializable 如何序列化对象?


3.1 Serializable 演示


然而 Java 目前并没有一个关键字可以直接去定义一个所谓的“可持久化”对象。


对象的持久化和反持久化需要靠程序员在代码里手动显式地进行序列化和反序列化还原的动作。


举个例子,假如我们要对 Student 类对象序列化到一个名为 student.txt 的文本文件中,然后再通过文本文件反序列化成 Student 类对象:


1、Student 类定义


public class Student implements Serializable {
private String name; private Integer age; private Integer score; @Override public String toString() { return "Student:" + '\n' + "name = " + this.name + '\n' + "age = " + this.age + '\n' + "score = " + this.score + '\n' ; } // ... 其他省略 ...}
复制代码


2、序列化


public static void serialize(  ) throws IOException {
Student student = new Student(); student.setName("CodeSheep"); student.setAge( 18 ); student.setScore( 1000 );
ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) ); objectOutputStream.writeObject( student ); objectOutputStream.close(); System.out.println("序列化成功!已经生成student.txt文件"); System.out.println("==============================================");}
复制代码


3、反序列化


public static void deserialize(  ) throws IOException, ClassNotFoundException {    ObjectInputStream objectInputStream =         new ObjectInputStream( new FileInputStream( new File("student.txt") ) );    Student student = (Student) objectInputStream.readObject();    objectInputStream.close();        System.out.println("反序列化结果为:");    System.out.println( student );}
复制代码


4、运行结果


控制台打印:


序列化成功!已经生成student.txt文件==============================================反序列化结果为:Student:name = CodeSheepage = 18score = 1000
复制代码


3.2 Serializable 接口有何用?


上面在定义 Student 类时,实现了一个 Serializable 接口,然而当我们点进 Serializable 接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!


试想,如果上面在定义 Student 类时忘了加 implementsSerializable 时会发生什么呢?


实验结果是:此时的程序运行会报错,并抛出 NotSerializableException 异常:



​我们按照错误提示,由源码一直跟到 ObjectOutputStream 的 writeObject0()方法底层一看,才恍然大悟:



如果一个对象既不是字符串、数组、枚举,而且也没有实现 Serializable 接口的话,在序列化时就会抛出 NotSerializableException 异常!


原来 Serializable 接口也仅仅只是做一个标记用!!!它告诉代码只要是实现了 Serializable 接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。


3.3 serialVersionUID 号有何用?


相信你一定经常看到有些类中定义了如下代码行,即定义了一个名为 serialVersionUID 的字段:


private static final long serialVersionUID = -4392658638228508589L;
复制代码


​你知道这句声明的含义吗?为什么要搞一个名为 serialVersionUID 的序列号?


继续来做一个简单实验,还拿上面的 Student 类为例,我们并没有人为在里面显式地声明一个 serialVersionUID 字段。


我们首先还是调用上面的 serialize()方法,将一个 Student 对象序列化到本地磁盘上的 student.txt 文件:


接下来我们在 Student 类里面动点手脚,比如在里面再增加一个名为 id 的字段,表示学生学号:


public class Student implements Serializable {    private String name;    private Integer age;    private Integer score;    private Integer id;
复制代码


​这时候,我们拿刚才已经序列化到本地的 student.txt 文件,还用如下代码进行反序列化,试图还原出刚才那个 Student 对象:


运行发现报错了,并且抛出了 InvalidClassException 异常



​这地方提示的信息非常明确了:序列化前后的 serialVersionUID 号码不兼容!


从这地方最起码可以得出两个重要信息:


1、serialVersionUID 是序列化前后的唯一标识符

2、默认如果没有人为显式定义过 serialVersionUID,那编译器会为它自动声明一个!


第 1 个问题: serialVersionUID 序列化 ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM 会把字节流中的序列号 ID 和被序列化类中的序列号 ID 做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。


第 2 个问题: 如果在定义一个可序列化的类时,没有人为显式地给它定义一个 serialVersionUID 的话,则 Java 运行时环境会根据该类的各方面信息自动地为它生成一个默认的 serialVersionUID,一旦像上面一样更改了类的结构或者信息,则类的 serialVersionUID 也会跟着变化!


所以,为了 serialVersionUID 的确定性,写代码时还是建议,凡是 implementsSerializable 的类,都最好人为显式地为它声明一个 serialVersionUID 明确值!


当然,如果不想手动赋值,你也可以借助 IDE 的自动添加功能,比如我使用的 IntelliJIDEA,按 alt + enter 就可以为类自动生成和添加 serialVersionUID 字段,十分方便:

 

两种特殊情况


1、凡是被 static 修饰的字段是不会被序列化的

2、凡是被 transient 修饰符修饰的字段也是不会被序列化的


对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略 static 静态域也是理所应当的。


对于第二点,就需要了解一下 transient 修饰符的作用了。


如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用 transient 修饰符来修饰该字段。


比如在之前定义的 Student 类中,加入一个密码字段,但是不希望序列化到 txt 文本,则可以:


public class Student implements Serializable {    private static final long serialVersionUID = -4392658638228508589L;    private transient String name;    private Integer age;    private Integer score;    private transient String passwd;
复制代码


​这样在序列化 Student 类对象时,password 字段会设置为默认值 null,这一点可以从反序列化所得到的结果来看出:


public static void serialize() throws IOException {
Student student = new Student(); student.setName("CodeSheep"); student.setAge(18); student.setScore(1000); student.setPasswd("123");
复制代码



4、实现 Externalizable


public UserInfo() {    userAge=20;//这个是在第二次测试使用,判断反序列化是否通过构造器}public void writeExternal(ObjectOutput out) throws IOException  {    //  指定序列化时候写入的属性。这里仍然不写入年龄    out.writeObject(userName);    out.writeObject(usePass);}public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException  {    // 指定反序列化的时候读取属性的顺序以及读取的属性    // 如果你写反了属性读取的顺序,你可以发现反序列化的读取的对象的指定的属性值也会与你写的读取方式一一对应。因为在文件中装载对象是有序的    userName=(String) in.readObject();    usePass=(String) in.readObject();}
复制代码


​我们在序列化对象的时候,由于这个类实现了 Externalizable 接口,在 writeExternal()方法里定义了哪些属性可以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自动调用 readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列。


Externalizable 实例类的唯一特性是可以被写入序列化流中,该类负责保存和恢复实例内容。 若某个要完全控制某一对象及其超类型的流格式和内容,则它要实现 Externalizable 接口的 writeExternal 和 readExternal 方法。这些方法必须显式与超类型进行协调以保存其状态。这些方法将代替定制的 writeObject 和 readObject 方法实现。


writeExternal(ObjectOutput out)          

该对象可实现 writeExternal 方法来保存其内容,它可以通过调用 DataOutput 的方法来保存其基本值,或调用 ObjectOutput 的 writeObject 方法来保存对象、字符串和数组。

readExternal(ObjectInput in)          

对象实现 readExternal 方法来恢复其内容,它通过调用 DataInput 的方法来恢复其基础类型,调用 readObject 来恢复对象、字符串和数组。


externalizable 和 Serializable 的区别:


1、实现 serializable 接口是默认序列化所有属性,如果有不需要序列化的属性使用 transient 修饰。externalizable 接口是 serializable 的子类,实现这个接口需要重写 writeExternal 和 readExternal 方法,指定对象序列化的属性和从序列化文件中读取对象属性的行为。


2、实现 serializable 接口的对象序列化文件进行反序列化不走构造方法,载入的是该类对象的一个持久化状态,再将这个状态赋值给该类的另一个变量。实现 externalizable 接口的对象序列化文件进行反序列化先走构造方法得到控对象,然后调用 readExternal 方法读取序列化文件中的内容给对应的属性赋值。

5、序列化的受控和加强


5.1 约束性加持


从上面的过程可以看出,序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了。


毕竟反序列化也相当于一种 “隐式的”对象构造 ,因此我们希望在反序列化时,进行受控的对象反序列化动作。


那怎么个受控法呢?


答案就是: 自行编写 readObject()函数,用于对象的反序列化构造,从而提供约束性。


既然自行编写 readObject()函数,那就可以做很多可控的事情:比如各种判断工作。


还以上面的 Student 类为例,一般来说学生的成绩应该在 0 ~ 100 之间,我们为了防止学生的考试成绩在反序列化时被别人篡改成一个奇葩值,我们可以自行编写 readObject()函数用于反序列化的控制:


private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
// 调用默认的反序列化函数 objectInputStream.defaultReadObject();
// 手工检查反序列化后学生成绩的有效性,若发现有问题,即终止操作! if( 0 > score || 100 < score ) { throw new IllegalArgumentException("学生分数只能在0到100之间!"); }}
复制代码


​比如我故意将学生的分数改为 101,此时反序列化立马终止并且报错:



对于上面的代码,为什么自定义的 private 的 readObject()方法可以被自动调用,跟一下底层源码来一探究竟,跟到了 ObjectStreamClass 类的最底层,是反射机制在起作用!是的,在 Java 里,果然万物皆可“反射”(滑稽),即使是类中定义的 private 私有方法,也能被抠出来执行了,简直引起舒适了。


5.2 单例模式增强


一个容易被忽略的问题是:可序列化的单例类有可能并不单例


举个代码小例子就清楚了。


比如这里我们先用 java 写一个常见的「静态内部类」方式的单例模式实现:


public class Singleton implements Serializable {
private static final long serialVersionUID = -1576643344804979563L;
private Singleton() { }
private static class SingletonHolder { private static final Singleton singleton = new Singleton(); }
public static synchronized Singleton getSingleton() { return SingletonHolder.singleton; }}
复制代码


​然后写一个验证主函数:


public class Test2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("singleton.txt") ) ); // 将单例对象先序列化到文本文件singleton.txt中 objectOutputStream.writeObject( Singleton.getSingleton() ); objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream( new File("singleton.txt") ) ); // 将文本文件singleton.txt中的对象反序列化为singleton1 Singleton singleton1 = (Singleton) objectInputStream.readObject(); objectInputStream.close();
Singleton singleton2 = Singleton.getSingleton();
// 运行结果竟打印 false ! System.out.println( singleton1 == singleton2 ); }
}
复制代码


​运行后我们发现:反序列化后的单例对象和原单例对象并不相等了,这无疑没有达到我们的目标。

解决办法是:在单例类中手写 readResolve()函数,直接返回单例对象:


private Object readResolve() {    return SingletonHolder.singleton;}
复制代码


package serialize.test;
import java.io.Serializable;
public class Singleton implements Serializable {
private static final long serialVersionUID = -1576643344804979563L;
private Singleton() { }
private static class SingletonHolder { private static final Singleton singleton = new Singleton(); }
public static synchronized Singleton getSingleton() { return SingletonHolder.singleton; } private Object readResolve() { return SingletonHolder.singleton; }}
复制代码

这样一来,当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象替代反序列化新建的对象。


点击关注,第一时间了解华为云新鲜技术~

发布于: 2021 年 05 月 10 日阅读数: 87
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
一文带你全面了解java对象的序列化和反序列化