Java 序列化与反序列化
什么是 Java 对象的序列化和反序列化?
以一个 POJO(PlainOrdinaryJavaObject,简单的Java对象)
的 User
类为例,当我们创建了一个 User
对象 myUser
之后,这个 myUser
对象就会存储在 JVM
(Java 虚拟机)上的堆内存中,只要垃圾回收机制没有盯上 myUser
,那么在程序运行期间我们总能有方法来访问到这个对象所包含的属性以及方法。而当 JVM
停止运行,那么 myUser
就会被回收,我们就再也拿不到 myUser
所包含的属性和方法了。
那问题是,如果我们需要保存这个 myUser
对象的状态信息,并加以保存,并在以后有需要的情况下将它原封不动的加载回来。那这个过程,就是 Java 对象的序列化与反序列化。我自己将这个过程画了个图,如下:
从术语角度来说, Serialization
是 Java 内置的一个 API
,通过这个步骤,将 对象状态信息
转化为字节流的方式,用于存储或者作为信息进行传输;而后再根据需要,通过反序列化的方式,将字节流转换成初始的对象。例如上图中类生成的对象小明,我们可以把他变成一种文本方式存储起来,等需要小明工作的时候,再将小明复原。这就是序列化和反序列的基本原理。
什么是 Serializable?
Serializable
是 Javaio
包中的一个内置接口,这个接口非常的有意思,从上面的过程来看,要实现这个接口的功能应该很复杂才对,但实际上,我们通过 IDEA 打开它的源码,就会发现:
里面啥也没有,只是一个 Serializable
接口声明。实际上,这个接口它仅用作标识实现该接口的类可以被序列化。
在序列化对象的过程中,声明实现该接口是必须的,如果上图中的类不实现 Serializable
这个接口,就会报出 NotSerializationException
这个异常。
在使用前应该注意的几个问题
在实现序列化和反序列化的过程中,需要注意几个问题。
静态变量在序列化过程中并不会被保存:序列化保存的是对象状态,而静态变量是属于类变量,因此并不会被保存。
一个类能不能序列化,就看它是否实现
Serializable
这个接口。并且如果子类的父类实现了该接口,那么这个子类也可以实现序列化。serialVersionUID
:这是一个与可序列化的类相关联的版本号,java培训在反序列化的过程中,这个序列号用来验证是不是该类进行的序列化过程。匹配成功,则进行反序列化,若serialVersionUID
匹配不成功,那么就会抛出InvalidClassException
这个异常。虽然可以不人为设定这个数值,但该接口的API
强烈建议开发者自行对这个版本号进行显式声明,并且规定这个值必须是static
类型和final
类型。通常一般我们也会将其设为priavte
私有变量的表述方式,例如:priavtestaticfinallongserialVersionUID=1L
Transient
关键字控制的变量不会序列化到目标文件中,当反序列化完成后,这个Transient
修饰的变量将被赋予 Java 规定的初始值。例如在下图中,字符串gender
被赋予了默认值null
如何持久化一个对象:序列化与反序列化?
以第一段中的 User
类为例,对这个 User
类的对象进行序列化过程。User
类是一个简单的 JaveBean
,它实现了序列化的接口 Serializable
,意味着这个类可以序列化。其中 gender
属性被 transient
修饰,意味着这个变量将不会被序列化。除此之外, serialVersionUID
被设置为 1L
,注意 long
长整型的后面需要加 L
修饰,并用大写字母 L
修饰,不要用小写 l
,避免与数字 1 发生混淆。
在定义了 User
类后,首先是利用该类创建一个对象小明。
然后是对象的序列化过程,在这个步骤中需要 ObjectOutputStream
将对象的基本数据类型写入流中。ObjectOutputStream
很独特,它只能将支持 Serializable
接口的对象写入字节流。默认的对象序列化机制写入字节流的内容包括:对象的类、类签名、非静态字段的值。现在我们需要把小明这个对象的状态写入文本中加以保存。
运行程序,就可以看到文件夹下生成了名为 tempFile
的文件,这就完成了对小明这个对象序列化的过程。打开文件查看里面的内容,但是会发现我们并不明白这到底写的是什么,但是这个就是小明被序列化后的模样。
由于写入的是字节流,因此对阅读者并不友好。后续会用一种更为完善的方法来解决这种问题。
在序列化之后,我们又突然需要复原小明,这就必须进行对象的反序列化过程。反序列化过程与序列化过程类似,它需要 ObjectInputStream
方法来读入需要复原的字节流,然后调用 readObject
的方法转换成对象。在这个过程中,会对序列号 serialVersionUID
进行校验工作,以确保类型匹配正确。
从控制台中查看复原的小明,看看他和我们序列化之前有什么区别:
可以看出,小明的 gender
因为被 transient
修饰,并没有被序列化,因此反序列化后字段被赋予了默认值 null
,就这样,小明被反序列化为一个身份不明的人。
序列化和反序列化在实际应用中十分广泛,例如远程通信传输对象信息的时候,就需要将对象转换成网络所允许的二进制字节流。但是在上面我们也发现,保存到本地的对象文本由于编码问题,导致可读性很差,有没有什么更好的替代方案呢?
Json 与 FastJson 库
Json 是一种轻量级的数据交换格式,在实际的工作中,Json 的使用非常的频繁。Json 通过键值对来表示对象的属性和属性值。如下的 Demo.json
展示了 Json 文件的一般形式。
Json 文件的特点在于结构清晰,简明易读。并且在序列化与反序列化上,Json 使用起来也更方便。在这个问题上,阿里巴巴提供了一个非常好用的工具 FastJson,它是阿里巴巴开源的 Json 解析库,并且支持将 JavaBean 序列化为 Json 字符串,也可以从 Json 字符串反序列为 JavaBean。
FastJson 使用非常简单,如果不是使用 Maven 构建项目,那就需要下载 FastJson 的 Jar 包进行导入,在 IDEA 中按照如下步骤添加 FastJson 库
点击 IDEA 的
File
,弹出后选择ProjectStructure
在弹出的页面选择
Modules
,并在随后的页面中选择Dependencies
在
Dependencies
页面下点击右侧绿色的+
号,弹出的小菜单中选择JARsordirectories
导入自己路径下的 FastJson 的 Jar 包,点击
Apply
后点击ok
测试 FastJson 库是否正常运行
使用 FastJson 对 JavaBean 进行序列化与反序列化简直不要太简单了,两个过程都只需要一行代码便可以完成相关操作。例如 FastJson 对小明进行序列化,见如下代码:
通过 JSONObject 调用 toJSONString
的方法,传入需要序列化的对象,只需要一行代码,就可以得到对象序列化之后的字符串。通过打印就可以看出这个对象里面到底有些啥。
依旧可以看出,被 transient
修饰的 gender
并没有被序列化,而这一次,我们很直观的看到了小明这个对象对序列化之后的结果。
反序列化过程也同样简洁:
反序列化过程的 parseObject
方法接受一个序列化后的字符串和目标对象的类,生成一个反序列化的对象后,通过 User
声明的 myUser
引用反序列化后的对象,打印这个对象,得到如下的结果:
注意这个对象的输出结果与上面序列化后的结果的差异, gender
反序列化后被赋予了 null
,其实这也说明了反序列化过程按照类的模板进行完全匹配的,对于没能序列化的对象状态,反序列化都尽量来恢复对象的初始状态。
由此可以看出,Json 文件和 FastJson 库在 Java 的序列化和反序列化的作用效率会更高,并且 JavaBean 不需要实现序列化 Serializable
接口,直接就配合 FastJson 可以使用,大幅提升了编码效率。
总结
在实际工作场景中,序列化和反序列化是一个十分常见常用的功能,现如今大部分都在采纳 Json 和 FastJson 解析库结合的这种方式进行交互,但是对于 Java 中 Serializable 的基本性质还是需要了解。更深入的,对于 Serializable 中的 writeObject
方法和 readObject
方法实现自定义的序列化策略也还需要进一步的研究和实践,后续会及时更新。
评论