前言
相信大家日常开发中, 经常看到 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
- java.io.ObjectOutputStream
- java.io.ObjectInputStream
- java.io.Serializable
- java.io.Externalizable
Serializable 接口
Serializable 接口是一个标记接口, 没有方法或字段. 一旦实现了此接口, 就标志该类的对象就是可序列化的.
- public interface Serializable {
- }
Externalizable 接口
Externalizable 继承了 Serializable 接口, 还定义了两个抽象方法: writeExternal()和 readExternal(), 如果开发人员使用 Externalizable 来实现序列化和反序列化, 需要重写 writeExternal()和 readExternal()方法
- public interface Externalizable extends java.io.Serializable {
- void writeExternal(ObjectOutput out) throws IOException;
- void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
- }
java.io.ObjectOutputStream 类
表示对象输出流, 它的 writeObject(Object obj)方法可以对指定 obj 对象参数进行序列化, 再把得到的字节序列写到一个目标输出流中.
java.io.ObjectInputStream
表示对象输入流,
它的 readObject()方法, 从输入流中读取到字节序列, 反序列化成为一个对象, 最后将其返回.
五, 序列化的使用
序列化如何使用? 来看一下, 序列化的使用的几个关键点吧:
声明一个实体类, 实现 Serializable 接口
使用 ObjectOutputStream 类的 writeObject 方法, 实现序列化
使用 ObjectInputStream 类的 readObject 方法, 实现反序列化
声明一个 Student 类, 实现 Serializable
- public class Student implements Serializable {
- private Integer age;
- private String name;
- public Integer getAge() {
- return age;
- }
- public void setAge(Integer age) {
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
使用 ObjectOutputStream 类的 writeObject 方法, 对 Student 对象实现序列化
把 Student 对象设置值后, 写入一个文件, 即序列化, 哈哈~
- ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:\\text.out"));
- Student student = new Student();
- student.setAge(25);
- student.setName("jayWei");
- objectOutputStream.writeObject(student);
- objectOutputStream.flush();
- objectOutputStream.close();
看看序列化的可爱模样吧, test.out 文件内容如下(使用 UltraEdit 打开):
使用 ObjectInputStream 类的 readObject 方法, 实现反序列化, 重新生成 student 对象
再把 test.out 文件读取出来, 反序列化为 Student 对象
- ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
- Student student = (Student) objectInputStream.readObject();
- System.out.println("name="+student.getName());
六, 序列化底层
Serializable 底层
Serializable 接口, 只是一个空的接口, 没有方法或字段, 为什么这么神奇, 实现了它就可以让对象序列化了?
- public interface Serializable {
- }
为了验证 Serializable 的作用, 把以上 demo 的 Student 对象, 去掉实现 Serializable 接口, 看序列化过程怎样吧~
序列化过程中抛出异常啦, 堆栈信息如下:
- Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student
- at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
- at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
- at com.example.demo.Test.main(Test.java:13)
顺着堆栈信息看一下, 原来有重大发现, 如下~
原来底层是这样:
ObjectOutputStream 在序列化的时候, 会判断被序列化的 Object 是哪一种类型, String?array?enum? 还是 Serializable, 如果都不是的话, 抛出 NotSerializableException 异常. 所以呀, Serializable 真的只是一个标志, 一个序列化标志~
writeObject(Object)
序列化的方法就是 writeObject, 基于以上的 demo, 我们来分析一波它的核心方法调用链吧~(建议大家也去 debug 看一下这个方法, 感兴趣的话)
writeObject 直接调用的就是 writeObject0()方法,
- public final void writeObject(Object obj) throws IOException {
- ......
- writeObject0(obj, false);
- ......
- }
writeObject0 主要实现是对象的不同类型, 调用不同的方法写入序列化数据, 这里面如果对象实现了 Serializable 接口, 就调用 writeOrdinaryObject()方法~
- private void writeObject0(Object obj, boolean unshared)
- throws IOException
- {
- ......
- //String 类型
- if (obj instanceof String) {
- writeString((String) obj, unshared);
- // 数组类型
- } else if (cl.isArray()) {
- writeArray(obj, desc, unshared);
- // 枚举类型
- } else if (obj instanceof Enum) {
- writeEnum((Enum<?>) obj, desc, unshared);
- //Serializable 实现序列化接口
- } else if (obj instanceof Serializable) {
- writeOrdinaryObject(obj, desc, unshared);
- } else{
- // 其他情况会抛异常~
- if (extendedDebugInfo) {
- throw new NotSerializableException(
- cl.getName() + "\n" + debugInfoStack.toString());
- } else {
- throw new NotSerializableException(cl.getName());
- }
- }
- ......
writeOrdinaryObject()会先调用 writeClassDesc(desc), 写入该类的生成信息, 然后调用 writeSerialData 方法, 写入序列化数据
- private void writeOrdinaryObject(Object obj,
- ObjectStreamClass desc,
- boolean unshared)
- throws IOException
- {
- ......
- // 调用 ObjectStreamClass 的写入方法
- writeClassDesc(desc, false);
- // 判断是否实现了 Externalizable 接口
- if (desc.isExternalizable() && !desc.isProxy()) {
- writeExternalData((Externalizable) obj);
- } else {
- // 写入序列化数据
- writeSerialData(obj, desc);
- }
- .....
- }
writeSerialData()实现的就是写入被序列化对象的字段数据
- private void writeSerialData(Object obj, ObjectStreamClass desc)
- throws IOException
- {
- for (int i = 0; i < slots.length; i++) {
- if (slotDesc.hasWriteObjectMethod()) {
- // 如果被序列化的对象自定义实现了 writeObject()方法, 则执行这个代码块
- slotDesc.invokeWriteObject(obj, this);
- } else {
- // 调用默认的方法写入实例数据
- defaultWriteFields(obj, slotDesc);
- }
- }
- }
defaultWriteFields()方法, 获取类的基本数据类型数据, 直接写入底层字节容器; 获取类的 obj 类型数据, 循环递归调用 writeObject0()方法, 写入数据~
- private void defaultWriteFields(Object obj, ObjectStreamClass desc)
- throws IOException
- {
- // 获取类的基本数据类型数据, 保存到 primVals 字节数组
- desc.getPrimFieldValues(obj, primVals);
- //primVals 的基本类型数据写到底层字节容器
- bout.write(primVals, 0, primDataSize, false);
- // 获取对应类的所有字段对象
- ObjectStreamField[] fields = desc.getFields(false);
- Object[] objVals = new Object[desc.getNumObjFields()];
- int numPrimFields = fields.length - objVals.length;
- // 获取类的 obj 类型数据, 保存到 objVals 字节数组
- desc.getObjFieldValues(obj, objVals);
- // 对所有 Object 类型的字段, 循环
- for (int i = 0; i < objVals.length; i++) {
- ......
- // 递归调用 writeObject0()方法, 写入对应的数据
- writeObject0(objVals[i],
- fields[numPrimFields + i].isUnshared());
- ......
- }
- }
七, 日常开发序列化的一些注意点
static 静态变量和 transient 修饰的字段是不会被序列化的
serialVersionUID 问题
如果某个序列化类的成员变量是对象类型, 则该对象类型的类必须实现序列化
子类实现了序列化, 父类没有实现序列化, 父类中的字段丢失问题
static 静态变量和 transient 修饰的字段是不会被序列化的
static 静态变量和 transient 修饰的字段是不会被序列化的, 我们来看例子分析一波~ Student 类加了一个类变量 gender 和一个 transient 修饰的字段 specialty
- public class Student implements Serializable {
- private Integer age;
- private String name;
- public static String gender = "男";
- transient String specialty = "计算机专业";
- public String getSpecialty() {
- return specialty;
- }
- public void setSpecialty(String specialty) {
- this.specialty = specialty;
- }
- @Override
- public String toString() {
- return "Student{" +"age=" + age + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", specialty='" + specialty + '\'' +
- '}';
- }
- ......
打印学生对象, 序列化到文件, 接着修改静态变量的值, 再反序列化, 输出反序列化后的对象~
运行结果:
序列化前 Student{age=25, name='jayWei', gender='男', specialty='计算机专业'}
序列化后 Student{age=25, name='jayWei', gender='女', specialty='null'}
对比结果可以发现:
1)序列化前的静态变量性别明明是'男', 序列化后再在程序中修改, 反序列化后却变成'女'了, what? 显然这个静态属性并没有进行序列化. 其实, 静态 (static) 成员变量是属于类级别的, 而序列化是针对对象的~ 所以不能序列化哦.
2)经过序列化和反序列化过程后, specialty 字段变量值由'计算机专业'变为空了, 为什么呢? 其实是因为 transient 关键字, 它可以阻止修饰的字段被序列化到文件中, 在被反序列化后, transient 字段的值被设为初始值, 比如 int 型的值会被设置为 0, 对象型初始值会被设置为 null.
serialVersionUID 问题
serialVersionUID 表面意思就是序列化版本号 ID, 其实每一个实现 Serializable 接口的类, 都有一个表示序列化版本标识符的静态变量, 或者默认等于 1L, 或者等于对象的哈希码.
private static final long serialVersionUID = -6384871967268653799L;
serialVersionUID 有什么用?
JAVA 序列化的机制是通过判断类的 serialVersionUID 来验证版本是否一致的. 在进行反序列化时, JVM 会把传来的字节流中的 serialVersionUID 和本地相应实体类的 serialVersionUID 进行比较, 如果相同, 反序列化成功, 如果不相同, 就抛出 InvalidClassException 异常.
接下来, 我们来验证一下吧, 修改一下 Student 类, 再反序列化操作
- Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student;
- local class incompatible: stream classdesc serialVersionUID = 3096644667492403394,
- local class serialVersionUID = 4429793331949928814
- at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
- at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876)
- at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745)
- at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)
- at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
- at com.example.demo.Test.main(Test.java:20)
从日志堆栈异常信息可以看到, 文件流中的 class 和当前类路径中的 class 不同了, 它们的 serialVersionUID 不相同, 所以反序列化抛出 InvalidClassException 异常. 那么, 如果确实需要修改 Student 类, 又想反序列化成功, 怎么办呢? 可以手动指定 serialVersionUID 的值, 一般可以设置为 1L 或者, 或者让我们的编辑器 IDE 生成
private static final long serialVersionUID = -6564022808907262054L;
实际上, 阿里开发手册, 强制要求序列化类新增属性时, 不能修改 serialVersionUID 字段~
如果某个序列化类的成员变量是对象类型, 则该对象类型的类必须实现序列化
给 Student 类添加一个 Teacher 类型的成员变量, 其中 Teacher 是没有实现序列化接口的
- public class Student implements Serializable {
- private Integer age;
- private String name;
- private Teacher teacher;
- ...
- }
- //Teacher 没有实现
- public class Teacher {
- ......
- }
序列化运行, 就报 NotSerializableException 异常啦
- Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher
- at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
- at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
- at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
- at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
- at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
- at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
- at com.example.demo.Test.main(Test.java:16)
其实这个可以在上小节的底层源码分析找到答案, 一个对象序列化过程, 会循环调用它的 Object 类型字段, 递归调用序列化的, 也就是说, 序列化 Student 类的时候, 会对 Teacher 类进行序列化, 但是对 Teacher 没有实现序列化接口, 因此抛出 NotSerializableException 异常. 所以如果某个实例化类的成员变量是对象类型, 则该对象类型的类必须实现序列化
子类实现了 Serializable, 父类没有实现 Serializable 接口的话, 父类不会被序列化.
子类 Student 实现了 Serializable 接口, 父类 User 没有实现 Serializable 接口
- // 父类实现了 Serializable 接口
- public class Student extends User implements Serializable {
- private Integer age;
- private String name;
- }
- // 父类没有实现 Serializable 接口
- public class User {
- String userId;
- }
- Student student = new Student();
- student.setAge(25);
- student.setName("jayWei");
- student.setUserId("1");
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\text.out"));
- objectOutputStream.writeObject(student);
- objectOutputStream.flush();
- objectOutputStream.close();
- // 反序列化结果
- ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
- Student student1 = (Student) objectInputStream.readObject();
- System.out.println(student1.getUserId());
- //output
- /**
- * null
- */
从反序列化结果, 可以发现, 父类属性值丢失了. 因此子类实现了 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 关键字修字段饰, 可以阻止该字段被序列化到文件中.
参考与感谢
Java 基础学习总结 --Java 对象的序列化和反序列化
10 个艰难的 Java 面试题与答案 https://segmentfault.com/a/1190000019962661
个人公众号
觉得写得好的小伙伴给个点赞 + 关注啦, 谢谢~
如果有写得不正确的地方, 麻烦指出, 感激不尽.
同时非常期待小伙伴们能够关注我公众号, 后面慢慢推出更好的干货~ 嘻嘻
来源: https://www.cnblogs.com/jay-huaxiao/p/12730437.html