首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

深入学习 Java 序列化

内容来源:天凉好个秋,链接:beautyboss.farbox.com/post/study/shen-ru-xue-xi-javaxu-lie-hua

阅读字数:16708 | 42分钟阅读

前言

对于Java的序列化,一直只知道只需要实现Serializbale这个接口就可以了,具体内部实现一直不是很了解,正好这次在重复造RPC的轮子的时候涉及到序列化问题,就抽时间看了下 Java序列化的底层实现,这篇文章算是这次的学习小结吧。

第一部分:What

Java序列化是指把Java对象保存为二进制字节码的过程,Java反序列化是指把二进制码重新转换成Java对象的过程。

那么为什么需要序列化呢?

第一种情况是:一般情况下Java对象的声明周期都比Java虚拟机的要短,实际应用中我们希望在JVM停止运行之后能够持久化指定的对象,这时候就需要把对象进行序列化之后保存。

第二种情况是:需要把Java对象通过网络进行传输的时候。因为数据只能够以二进制的形式在网络中进行传输,因此当把对象通过网络发送出去之前需要先序列化成二进制数据,在接收端读到二进制数据之后反序列化成Java对象。

第二部分:How

本部分以序列化到文件为例讲解Java序列化的基本用法。

程序执行完用vim打开temp.out文件,可以看到

第三部分:Why

调用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()之后究竟做了什么?temp.out文件中的二进制分别代表什么意思?

别急,且听我娓娓道来。

1. ObjectStreamClass类

官方文档对这个类的介绍如下

Serialization’s descriptor for classes. It contains the name and serialVersionUID of the class. The ObjectStreamClass for a specific class loaded in this Java VM can be found/created using the lookup method.

可以看到ObjectStreamClass这个是类的序列化描述符,这个类可以描述需要被序列化的类的元数据,包括被序列化的类的名字以及序列号。可以通过lookup()方法来查找/创建在这个JVM中加载的特定的ObjectStreamClass对象。

2. 序列化:writeObject()

在调用wroteObject()进行序列化之前会先调用ObjectOutputStream的构造函数生成一个ObjectOutputStream对象,构造函数如下:

构造函数中首先会把bout对绑定到底层的字节数据容器,接着会调用writeStreamHeader()方法,该方法实现如下:

在writeStreamHeader()方法中首先会往底层字节容器中写入表示序列化的Magic Number以及版本号,定义为

接下来会调用writeObject()方法进行序列化,实现如下:

正常情况下会调用writeObject0()进行序列化操作,该方法实现如下:

从代码里面可以看到,程序会

生成一个描述被序列化对象的类的类元信息的ObjectStreamClass对象。

根据传入的需要序列化的对象的实际类型进行不同的序列化操作。从代码里面可以很明显的看到,对于String类型、数组类型和Enum可以直接进行序列化。如果被序列化对象实现了Serializable对象,则会调用writeOrdinaryObject()方法进行序列化。

这里可以解释一个问题:Serializbale接口是个空的接口,并没有定义任何方法,为什么需要序列化的接口只要实现Serializbale接口就能够进行序列化。

答案是:Serializable接口这是一个标识,告诉程序所有实现了”我”的对象都需要进行序列化。

因此,序列化过程接下来会执行到writeOrdinaryObject()这个方法中,该方法实现如下:

在这个方法中首先会往底层字节容器中写入TC_OBJECT,表示这是一个新的Object

接下来会调用writeClassDesc()方法写入被序列化对象的类的类元数据,writeClassDesc()方法实现如下:

在这个方法中会先判断传入的desc是否为null,如果为null则调用writeNull()方法

如果不为null,则一般情况下接下来会调用writeNonProxyDesc()方法,该方法实现如下:

在这个方法中首先会写入一个字节的TC_CLASSDESC,这个字节表示接下来的数据是一个新的Class描述符,接着会调用writeNonProxy()方法写入实际的类元信息,writeNonProxy()实现如下:

writeNonProxy()方法中会按照以下几个过程来写入数据:

2. 接下来会调用writeLong()方法写入类的序列号UID,UID是通过getSerialVersionUID()方法来获取。

3. 接着会判断被序列化的对象所属类的flag,并写入底层字节容器中(占用两个字节)。类的flag分为以下几类:

final static byte SC_EXTERNALIZABLE = 0×04;表示该类为Externalizable类,即实现了Externalizable接口。

final static byte SC_SERIALIZABLE = 0×02;表示该类实现了Serializable接口。

final static byte SC_WRITE_METHOD = 0×01;表示该类实现了Serializable接口且自定义了writeObject()方法。

final static byte SC_ENUM = 0×10;表示该类是个Enum类型。

对于本例中flag = 0×02表示只是Serializable类型。

4. 第四步会依次写入被序列化对象的字段的元数据。

首先会写入被序列化对象的字段的个数,占用两个字节。本例中为2,因为TestObject类中只有两个字段,一个是int类型的testValue,一个是InnerObject类型的innerValue。

依次写入每个字段的元数据。每个单独的字段由ObjectStreamField类来表示。

1) 写入字段的类型码,占一个字节。 类型码的映射关系如下:

2) 调用writeUTF()方法写入每个字段的名字。注意,writeUTF()方法会先写入名字占用的字节数。

3) 如果被写入的字段不是基本类型,则会接着调用writeTypeString()方法写入代表对象或者类的类型字符串,该方法需要一个参数,表示对应的类或者接口的字符串,最终调用的还是writeString()方法,实现如下

在这个方法中会先写入一个标志位TC_STRING表示接下来的数据是一个字符串,接着会调用writeUTF()写入字符串。

执行完上面的过程之后,程序流程重新回到writeNonProxyDesc()方法中

接下来会写入一个字节的标志位TC_ENDBLOCKDATA表示对一个object的描述块的结束。

然后会调用writeClassDesc()方法,传入父类的ObjectStreamClass对象,写入父类的类元数据。

需要注意的是writeClassDesc()这个方法是个递归调用,调用结束返回的条件是没有了父类,即传入的ObjectStreamClass对象为null,这个时候会写入一个字节的标识位TC_NULL.

在递归调用完成写入类的类元数据之后,程序执行流程回到wriyeOrdinaryObject()方法中,

从上面的分析中我们可以知道,当写入类的元数据的时候,是先写子类的类元数据,然后递归调用的写入父类的类元数据。

接下来会调用writeSerialData()方法写入被序列化的对象的字段的数据,方法实现如下:

在这个方法中首先会调用getClassDataSlot()方法获取被序列化对象的数据的布局,关于这个方法官方文档中说明如下:

需要注意的是这个方法会把从父类继承的数据一并返回,并且表示从父类继承的数据的ClassDataSlot对象在数组的最前面。

对于没有自定义writeObject()方法的对象来说,接下来会调用defaultWriteFields()方法写入数据,该方法实现如下:

可以看到,在这个方法中会做下面几件事情:

获取对应类的基本类型的字段的数据,并写入到底层的字节容器中。

获取对应类的Object类型(非基本类型)的字段成员,递归调用writeObject0()方法写入相应的数据。

从上面对写入数据的分析可以知道,写入数据是是按照先父类后子类的顺序来写的。

至此,Java序列化过程分析完毕,总结一下,在本例中序列化过程如下:

现在可以来分析下第二步中写入的temp.out文件的内容了。

接下来开始写入数据,从父类Parent开始

接下来是写入InnerObject的类元信息

反序列化过程就是按照前面介绍的序列化算法来解析二进制数据。

有一个需要注意的问题就是,如果子类实现了Serializable接口,但是父类没有实现Serializable接口,这个时候进行反序列化会发生什么情况?

答:如果父类有默认构造函数的话,即使没有实现Serializable接口也不会有问题,反序列化的时候会调用默认构造函数进行初始化,否则的话反序列化的时候会抛出.InvalidClassException:异常,异常原因为no valid constructor。

第四部分:Other

1. static和transient字段不能被序列化。

序列化的时候所有的数据都是来自于ObejctStreamClass对象,在生成ObjectStreamClass的构造函数中会调用fields = getSerialFields(cl);这句代码来获取需要被序列化的字段,getSerialFields()方法实际上是调用getDefaultSerialFields()方法的,getDefaultSerialFields()实现如下:

从上面的代码中可以很明显的看到,在计算需要被序列化的字段的时候会把被static和transient修饰的字段给过滤掉。

在进行反序列化的时候会给默认值。

2. 如何实现自定义序列化和反序列化?

只需要被序列化的对象所属的类定义了void writeObject(ObjectOutputStream oos)和void readObject(ObjectInputStream ois)方法即可,Java序列化和反序列化的时候会调用这两个方法,那么这个功能是怎么实现的呢?

1. 在ObjectClassStream类的构造函数中有下面几行代码:

getPrivateMethod()方法实现如下:

可以看到在ObejctStreamClass的构造函数中会查找被序列化类中有没有定义为void writeObject(ObjectOutputStream oos) 的函数,如果找到的话,则会把找到的方法赋值给writeObjectMethod这个变量,如果没有找到的话则为null。

2. 在调用writeSerialData()方法写入序列化数据的时候有

首先会调用hasWriteObjectMethod()方法判断有没有自定义的writeObject(),代码如下

hasWriteObjectMethod()这个方法仅仅是判断writeObjectMethod是不是等于null,而上面说了,如果用户自定义了void writeObject(ObjectOutputStream oos)这么个方法,则writeObjectMethod不为null,在if()代码块中会调用slotDesc.invokeWriteObject(obj, this);方法,该方法中会调用用户自定义的writeObject()方法。

以上为所有分享内容,谢谢大家!有问题可以在评论区与大家讨论,欢迎广大技术爱好朋友关注。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180524A1SAMI00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券