在开发过程中经常会对实体进行序列化,但其实我们只是在“只知其然,不知其所以然”的状态,很多时候会有这些问题:
- 什么是序列化和反序列化?为什么要序列化?
- 怎么实现序列化?
- 序列化的原理是什么呢?
- transient关键字
- 序列化时应注意什么?
如果你也有这些疑问,不妨看看本文?
1.
Java序列化是指把Java对象转换为字节序列的过程;
Java反序列化是指把字节序列恢复为Java对象的过程;
2.
其实我们的对象不只是存储在内存中,它还需要在传输网络中进行传输,并且保存起来之后下次再加载出来,这时候就需要序列化技术。
- 一般Java对象的生命周期比Java虚拟机端,而实际开发中如果需要JVM停止后能够继续持有对象,则需要用到序列化技术将对象持久化到磁盘或数据库。
- 在多个项目进行RPC调用时,需要在网络上传输JavaBean对象,而网络上只允许二进制形式的数据进行传输,这时则需要用到序列化技术。
Java的序列化技术就是把对象转换成一串由二进制字节组成的数组,然后将这二进制数据保存在磁盘或传输网络。而后需要用到这对象时,磁盘或者网络接收者可以通过反序列化得到此对象,达到对象持久化的目的。
3.
序列化的过程一般会是这样的:
- 将对象实例相关的类元数据输出
- 递归地输出类的超类描述,直到没有超类
- 类元数据输出之后,开始从最顶层的超类输出对象实例的实际数据值
- 从上至下递归输出实例的数据
所以,如果父类已经序列化了,子类继承之后也可以进行序列化。实现第一步,则需要的先将对象实例相关的类标记为需要序列化。
实现序列化的要求:目标对象实现Serializable接口
我们先创建一个NY类,实现Serializable接口,并生成一个版本号:
public
在这里,Serializable接口的作用只是标识这个类是需要进行序列化的,而且Serializable接口中并没有提供任何方法。而且serialVersionUID序列化版本号的作用是用来区分我们所编写的类的版本,用于反序列化时确定版本。
JDK类库中序列化和反序列化API
java.io.ObjectInputStream:对象输入流
该类中的readObject()方法从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回。
java.io.ObjectOutputStream:对象输出流
该类的writeObject()方法将传入的obj对象进行序列化,把得到的字节序列写入到目标输出流中进行输出。结合上面的NY类,我们来看看使用JDK类库中的API怎么实现序列化和反序列化:
public
运行结果为:
NY
可以看到,这整个过程简单来说就是把对象存在磁盘,然后再从磁盘读出来。
但是我们平时看到序列化的实体中的serialVersionUID,为什么有的是1L,有的是一长串数字?
上面我们的提到serialVersionUID作用就是用来区分类的版本,所以无论是1L还是一长串数字,都是用来确认版本的。如果序列化的类版本改变,则在反序列化的时候就会报错。举个栗子,刚刚我们已经在磁盘中生成了NY对象的序列化文件,如果我们对NY类的serialVersionUID稍作改动,改成:
private
再执行一次反序列化方法,运行结果如下:
Exception
至于怎么让idea生成serialVersionUID,则需要在idea设置中改个配置即可:
之后再使用"Alt+Enter"键即可调出下图选项:
序列化的原理是什么呢?
既然知道了序列化是怎么使用的,那么序列化的原理是怎么样的呢?我们用上面的例子来作为探寻序列化原理的入口:
private
1. 进入ObjectOutputStream的构造函数:
public
我们进入writeStreamHeader()方法:
protected
这个方法是将序列化文件的魔数和版本写入序列化文件头:
final
2. 在 writeObject() 方法进行具体的序列化写入操作:
public
进入writeObject0()方法:
private
这一段代码中创建了ObjectStreamClass对象,并根据不同的对象类型来执行不同的写入操作。而在此例子中,对象对应的类实现了Serializable接口,所以下一步会执行writeOrdinaryObject()方法。writeOrdinaryObject()是当对象对应的类实现了Serializable接口的时才会被调用:
private
接下来是将类的描述写入类元数据中的writeClassDesc():
private
在desc为null时,会执行writeNull()方法:
private
可以看到,在writeNull()中,会将表示NULL的标识写入序列中。那么如果desc不为null时,一般执行writeNonProxyDesc()方法:
private
在上一个方法执行过程中,会执行writeClassDescriptor()方法将类的描述写入类元数据中:
protected
在这里我们可以看到,写入类元信息的方法调用了writeNonProxy()方法:
void
这次方法中我们可以看到:
- 调用writeUTF()方法将对象所属类的名字写入。
- 调用writeLong()方法将类的序列号serialVersionUID写入。
- 判断被序列化对象所属类的流类型flag,写入底层字节容器中(占两个字节)。
- 写入对象中的所有字段,以及对应的属性
所以直到这个方法的执行,一个对象及其对应类的所有属性和属性值才被序列化。当上述流程完成之后,回到writeOrdinaryObject()方法中,继续往下运行:
private
调用writeSerialData()方法将实例化数据写入:
private
当执行到defaultWriteFields()方法时,会将实例数据写入:
private
在执行完上述方法之后,程序将会回到writeNonProxyDesc()方法中,并且在writeClassDesc()中会将对象对应的类的父类信息进行写入:
private
至此,我们可以知道,整个序列化的过程其实就是一个递归写入的过程。将上面的过程进行简化,可以总结为这幅图:
transient关键字
在有些时候,我们并不想将一些敏感信息序列化,如密码等,这个时候就需要transient关键字来标注属性为非序列化属性。
transient关键字的使用
将上面的NY类中的name属性稍作修改:
private
当我们再次运行SerializeNY类中的main()方法时,运行结果如下:
NY
我们可以看到,name属性为null,说明反序列化时根本没有从文件中获取到信息。
transient关键字的特点
变量一旦被transient修饰,则不再是对象持久化的一部分了,而且变量内容在反序列化时也不能获得。transient关键字只能修饰变量,而不能修饰方法和类,而且本地变量是不能被transient修饰的,如果变量是类变量,则需要该类也实现Serializable接口。一个静态变量不管是否被transient修饰,都不会被序列化。关于这一点,可能会有读者感到疑惑。举个栗子,如果用static修饰NY类中的name:
private
运行SerializeNY类中的main程序,可以看到运行结果:
NY
嘶…这是翻车了吗?并没有,因为这里出现的name值是当前JVM中对应的static变量值,这个值是JVM中的而不是反序列化得出的。不信?我们来改变一下SerializeNY类中的serializeNY()函数
private
笔者在NY对象被序列化之后,改变了NY对象的name值。运行结果为:
NY
transient修饰的变量真的就不能被序列化了吗?
举个栗子:
public
运行结果为:
即使被transient修饰
我们可以看到,content变量在被transient修饰的情况下,还是被序列化了。因为在Java中,对象序列化可以通过实现两种接口来实现:
- 如果实现的是Serializable接口,则所有信息(不包括被static、transient修饰的变量信息)的序列化将自动进行。
- 如果实现的是Externalizable接口,则不会进行自动序列化,需要开发者在writeExternal()方法中手工指定需要序列化的变量,与是否被transient修饰无关。
序列化注意事项
- 序列化对象必须实现序列化接口Serializable。
- 序列化对象中的属性如果也有对象的话,其对象需要实现序列化接口。
- 类的对象序列化后,类的序列号不能轻易更改,否则反序列化会失败。
- 类的对象序列化后,类的属性增加或删除不会影响序列化,只是值会丢失。
- 如果父类序列化,子类会继承父类的序列化;如果父类没序列化,子类序列化了,子类中的属性能正常序列化,但父类的属性会丢失,不能序列化。
- 用Java序列化的二进制字节数据只能由Java反序列化,如果要转换成其他语言反序列化,则需要先转换成Json/XML通用格式的数据。
结语
第一次写关于JDK实现原理的文章,还是觉得有点难度的,但是这对于源码分析能力还是有点提升的。在这个过程中最好多打断点,多调试。