Java对象序列化
前言
相信大家日常开发中,经常看到Java对象“implements Serializable”。那么,它到底有什么用呢?本文从以下几个角度来解析序列这一块知识点~
- 什么是Java序列化?
- 为什么需要序列化?
- 序列化用途
- Java序列化常用API
- 序列化的使用
- 序列化底层
- 日常开发序列化的注意点
- 序列化常见面试题
一、什么是Java序列化?
- 序列化:把Java对象转换为字节序列的过程
- 反序列:把字节序列恢复为Java对象的过程
二、为什么需要序列化?
Java对象是运行在JVM的堆内存中的,如果JVM停止后,它的生命也就戛然而止。
如果想在JVM停止后,把这些对象保存到磁盘或者通过网络传输到另一远程机器,怎么办呢?磁盘这些硬件可不认识Java对象,它们只认识二进制这些机器语言,所以我们就要把这些对象转化为字节数组,这个过程就是序列化啦~
打个比喻,作为大城市漂泊的码农,搬家是常态。当我们搬书桌时,桌子太大了就通不过比较小的门,因此我们需要把它拆开再搬过去,这个拆桌子的过程就是序列化。 而我们把书桌复原回来(安装)的过程就是反序列化啦。
三、序列化用途
序列化使得对象可以脱离程序运行而独立存在,它主要有两种用途:
- 1) 序列化机制可以让对象地保存到硬盘上,减轻内存压力的同时,也起了持久化的作用;
比如 Web服务器中的Session对象,当有 10+万用户并发访问的,就有可能出现10万个Session对象,内存可能消化不良,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
- 2) 序列化机制让Java对象在网络传输不再是天方夜谭。
我们在使用Dubbo远程调用服务框架时,需要把传输的Java对象实现Serializable接口,即让Java对象序列化,因为这样才能让对象在网络上传输。
四、Java序列化常用API
1 | java.io.ObjectOutputStream |
Serializable 接口
Serializable接口是一个标记接口,没有方法或字段。一旦实现了此接口,就标志该类的对象就是可序列化的。
1 | public interface Serializable { |
Externalizable 接口
Externalizable继承了Serializable接口,还定义了两个抽象方法:writeExternal()和readExternal(),如果开发人员使用Externalizable来实现序列化和反序列化,需要重写writeExternal()和readExternal()方法
1 | public interface Externalizable extends java.io.Serializable { |
java.io.ObjectOutputStream类
表示对象输出流,它的writeObject(Object obj)方法可以对指定obj对象参数进行序列化,再把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream
表示对象输入流,
它的readObject()方法,从输入流中读取到字节序列,反序列化成为一个对象,最后将其返回。
五、序列化的使用
序列化如何使用?来看一下,序列化的使用的几个关键点吧:
- 声明一个实体类,实现Serializable接口
- 使用ObjectOutputStream类的writeObject方法,实现序列化
- 使用ObjectInputStream类的readObject方法,实现反序列化
声明一个Student类,实现Serializable
1 | public class Student implements Serializable { |
使用ObjectOutputStream类的writeObject方法,对Student对象实现序列化
把Student对象设置值后,写入一个文件,即序列化,哈哈~
1 | ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:\\text.out")); |
看看序列化的可爱模样吧,test.out文件内容如下(使用UltraEdit打开):
使用ObjectInputStream类的readObject方法,实现反序列化,重新生成student对象
再把test.out文件读取出来,反序列化为Student对象
1 | ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out")); |
六、序列化底层
Serializable底层
Serializable接口,只是一个空的接口,没有方法或字段,为什么这么神奇,实现了它就可以让对象序列化了?
1 | public interface Serializable { |
为了验证Serializable的作用,把以上demo的Student对象,去掉实现Serializable接口,看序列化过程怎样吧~
序列化过程中抛出异常啦,堆栈信息如下:
1 | Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student |
顺着堆栈信息看一下,原来有重大发现,如下~
原来底层是这样:
ObjectOutputStream 在序列化的时候,会判断被序列化的Object是哪一种类型,String?array?enum?还是 Serializable,如果都不是的话,抛出 NotSerializableException异常。所以呀,Serializable真的只是一个标志,一个序列化标志~
writeObject(Object)
序列化的方法就是writeObject,基于以上的demo,我们来分析一波它的核心方法调用链吧~(建议大家也去debug看一下这个方法,感兴趣的话)
writeObject直接调用的就是writeObject0()方法,
1 | public final void writeObject(Object obj) throws IOException { |
writeObject0 主要实现是对象的不同类型,调用不同的方法写入序列化数据,这里面如果对象实现了Serializable接口,就调用writeOrdinaryObject()方法~
1 | private void writeObject0(Object obj, boolean unshared) |
writeOrdinaryObject()会先调用writeClassDesc(desc),写入该类的生成信息,然后调用writeSerialData方法,写入序列化数据
1 | private void writeOrdinaryObject(Object obj, |
writeSerialData()实现的就是写入被序列化对象的字段数据
1 | private void writeSerialData(Object obj, ObjectStreamClass desc) |
defaultWriteFields()方法,获取类的基本数据类型数据,直接写入底层字节容器;获取类的obj类型数据,循环递归调用writeObject0()方法,写入数据~
1 | private void defaultWriteFields(Object obj, ObjectStreamClass desc) |
七、日常开发序列化的一些注意点
- static静态变量和transient 修饰的字段是不会被序列化的
- serialVersionUID问题
- 如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化
- 子类实现了序列化,父类没有实现序列化,父类中的字段丢失问题
static静态变量和transient 修饰的字段是不会被序列化的
static静态变量和transient 修饰的字段是不会被序列化的,我们来看例子分析一波~ Student类加了一个类变量gender和一个transient修饰的字段specialty
1 | public class Student implements Serializable { |
打印学生对象,序列化到文件,接着修改静态变量的值,再反序列化,输出反序列化后的对象~
运行结果:
1 | 序列化前Student{age=25, name='jayWei', gender='男', specialty='计算机专业'} |
对比结果可以发现:
- 1)序列化前的静态变量性别明明是‘男’,序列化后再在程序中修改,反序列化后却变成‘女’了,what?显然这个静态属性并没有进行序列化。其实,静态(static)成员变量是属于类级别的,而序列化是针对对象的~所以不能序列化哦。
- 2)经过序列化和反序列化过程后,specialty字段变量值由’计算机专业’变为空了,为什么呢?其实是因为transient关键字,它可以阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null。
serialVersionUID问题
serialVersionUID 表面意思就是序列化版本号ID,其实每一个实现Serializable接口的类,都有一个表示序列化版本标识符的静态变量,或者默认等于1L,或者等于对象的哈希码。
1 | private static final long serialVersionUID = -6384871967268653799L; |
serialVersionUID有什么用?
JAVA序列化的机制是通过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同,反序列化成功,如果不相同,就抛出InvalidClassException异常。
接下来,我们来验证一下吧,修改一下Student类,再反序列化操作
1 | Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student; |
从日志堆栈异常信息可以看到,文件流中的class和当前类路径中的class不同了,它们的serialVersionUID不相同,所以反序列化抛出InvalidClassException异常。那么,如果确实需要修改Student类,又想反序列化成功,怎么办呢?可以手动指定serialVersionUID的值,一般可以设置为1L或者,或者让我们的编辑器IDE生成
1 | private static final long serialVersionUID = -6564022808907262054L; |
实际上,阿里开发手册,强制要求序列化类新增属性时,不能修改serialVersionUID字段~
如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化
给Student类添加一个Teacher类型的成员变量,其中Teacher是没有实现序列化接口的
1 | public class Student implements Serializable { |
序列化运行,就报NotSerializableException异常啦
1 | Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher |
其实这个可以在上小节的底层源码分析找到答案,一个对象序列化过程,会循环调用它的Object类型字段,递归调用序列化的,也就是说,序列化Student类的时候,会对Teacher类进行序列化,但是对Teacher没有实现序列化接口,因此抛出NotSerializableException异常。所以如果某个实例化类的成员变量是对象类型,则该对象类型的类必须实现序列化
子类实现了Serializable,父类没有实现Serializable接口的话,父类不会被序列化。
子类Student实现了Serializable接口,父类User没有实现Serializable接口
1 | //父类实现了Serializable接口 |
从反序列化结果,可以发现,父类属性值丢失了。因此子类实现了Serializable接口,父类没有实现Serializable接口的话,父类不会被序列化。
八、序列化常见面试题
- 序列化的底层是怎么实现的?
- 序列化时,如何让某些成员不要序列化?
- 在 Java 中,Serializable 和 Externalizable 有什么区别
- serialVersionUID有什么用?
- 是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程?
- 在 Java 序列化期间,哪些变量未序列化?
1.序列化的底层是怎么实现的?
本文第六小节可以回答这个问题,如回答Serializable关键字作用,序列化标志啦,源码中,它的作用啦还有,可以回答writeObject几个核心方法,如直接写入基本类型,获取obj类型数据,循环递归写入,哈哈
2.序列化时,如何让某些成员不要序列化?
可以用transient关键字修饰,它可以阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null。
3.在 Java 中,Serializable 和 Externalizable 有什么区别
Externalizable继承了Serializable,给我们提供 writeExternal() 和 readExternal() 方法, 让我们可以控制 Java的序列化机制, 不依赖于Java的默认序列化。正确实现 Externalizable 接口可以显著提高应用程序的性能。
4.serialVersionUID有什么用?
可以看回本文第七小节哈,JAVA序列化的机制是通过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同,反序列化成功,如果不相同,就抛出InvalidClassException异常。
5.是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程?
可以的。我们都知道,对于序列化一个对象需调用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 读取对象, 但 Java 虚拟机为你提供的还有一件事, 是定义这两个方法。如果在类中定义这两种方法, 则 JVM 将调用这两种方法, 而不是应用默认序列化机制。同时,可以声明这些方法为私有方法,以避免被继承、重写或重载。
6.在 Java 序列化期间,哪些变量未序列化?
static静态变量和transient 修饰的字段是不会被序列化的。静态(static)成员变量是属于类级别的,而序列化是针对对象的。transient关键字修字段饰,可以阻止该字段被序列化到文件中。
参考与感谢
个人公众号
- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~
- 如果有写得不正确的地方,麻烦指出,感激不尽。
- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻
- github地址:https://github.com/whx123/JavaHome