在开发过程中经常会对实体进行序列化,但其实我们只是在“只知其然,不知其所以然”的状态,很多时候会有这些问题:

  • 什么是序列化和反序列化?为什么要序列化?
  • 怎么实现序列化?
  • 序列化的原理是什么呢?
  • 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设置中改个配置即可:




java 类不能序列化 java类序列化的原因_反序列化


之后再使用"Alt+Enter"键即可调出下图选项:


java 类不能序列化 java类序列化的原因_序列化_02


序列化的原理是什么呢?

既然知道了序列化是怎么使用的,那么序列化的原理是怎么样的呢?我们用上面的例子来作为探寻序列化原理的入口:


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


这次方法中我们可以看到:

  1. 调用writeUTF()方法将对象所属类的名字写入。
  2. 调用writeLong()方法将类的序列号serialVersionUID写入。
  3. 判断被序列化对象所属类的流类型flag,写入底层字节容器中(占两个字节)。
  4. 写入对象中的所有字段,以及对应的属性

所以直到这个方法的执行,一个对象及其对应类的所有属性和属性值才被序列化。当上述流程完成之后,回到writeOrdinaryObject()方法中,继续往下运行:


private


调用writeSerialData()方法将实例化数据写入:


private


当执行到defaultWriteFields()方法时,会将实例数据写入:


private


在执行完上述方法之后,程序将会回到writeNonProxyDesc()方法中,并且在writeClassDesc()中会将对象对应的类的父类信息进行写入:


private


至此,我们可以知道,整个序列化的过程其实就是一个递归写入的过程。将上面的过程进行简化,可以总结为这幅图:


java 类不能序列化 java类序列化的原因_反序列化_03


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实现原理的文章,还是觉得有点难度的,但是这对于源码分析能力还是有点提升的。在这个过程中最好多打断点,多调试。