面试题:Java序列化与反序列化
作者:小牛呼噜噜 | https://xiaoniuhululu.com
计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」
序列化和反序列化的概念
当我们在Java中创建对象的时候,对象会一直存在,直到程序终止时。但有时候可能存在一种"持久化"场景:我们需要让对象能够在程序不运行的情况下,仍能存在并保存其信息。当程序再次运行时 还可以通过该对象的保存下来的信息 来重建该对象。序列化和反序列化 就应运而生了,序列化机制可以使对象可以脱离程序的运行而独立存在。
- 序列化: 将对象转换成二进制字节流的过程
- 反序列化:从二进制字节流中恢复对象的过程
应用场景?
- 对象在进行网络传输的时候,需要先被序列化,接收到序列化的对象之后需要再进行反序列化;比如远程方法调用 RPC
- 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
- 将对象存储到内存中,需要进行序列化,将对象从内存中读取出来需要进行反序列化。
- 将对象存储到数据库(如 Redis)时,需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

序列化实现的方式
如果使用Jdk自带的序列化方式实现对象序列化的话,那么这个类应该实现Serializable接口或者Externalizable接口
继承Serializable接口,普通序列化
首先我们定义一个对象类User
public class User implements Serializable {
//序列化ID
private static final long serialVersionUID = 1L;
private int age;
private String name;
public User(int age, String name) {
this.age = age;
this.name = name;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后我们编写一下测试类:
public class serTest {
public static void main(String[] args) throws Exception, IOException {
SerializeUser();
DeSerializeUser();
}
/**
* 序列化方法
* @throws IOException
*/
private static void SerializeUser() throws IOException {
User user = new User(11, "小张");
//序列化对象到指定的文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\jun\\Desktop\\example"));
oos.writeObject(user);
oos.close();
System.out.println("序列化对象成功");
}
/**
* 反序列化方法
* @throws IOException
* @throws ClassNotFoundException
*/
private static void DeSerializeUser() throws IOException, ClassNotFoundException {
//读取指定的文件
File file = new File("C:\\Users\\jun\\Desktop\\example");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User newUser = (User)ois.readObject();
System.out.println("反序列化对象成功:"+ newUser.getName()+ ","+newUser.getAge());
}
}
结果:
序列化对象成功
反序列化对象成功:小张,11
一个对象想要被序列化,那么它的类就要继承Serializable接口或者它的子接口
继承Serializable接口类的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。如果不想序列化的字段可以使用transient关键字修饰
private int age;
private String name;
private transient password;//属性:密码,不想被序列化
我们需要注意的是:使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,这必然会导致了在反序列化时无法获取该属性的值。
其实我们完全可以在通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法来控制如何序列化各属性,某些属性是否被序列化
如果User有一个属性是引用类型的呢?比如User其中有一个属性是类Person:
private Person person;
那如果要想User可以序列化,那Person类也必须得继承Serializable接口,不然程序会报错
另外大家应该注意到serialVersionUID了吧,在日常开发的过程中,经常遇到,暂且放放,我们后文再详细讲解
继承Externalizable接口,强制自定义序列化
对于Externalizable接口,我们需要知道以下几点:
- Externalizable继承自Serializable接口
- 需要我们重写writeExternal()与readExternal()方法,这是强制性的
- 实现Externalizable接口的类必须要提供一个public的无参的构造器,因为反序列化的时候需要反射创建对象
- Externalizable接口实现序列化,性能稍微比继承自Serializable接口好一点
首先我们定义一个对象类ExUser
public class ExUser implements Externalizable {
private int age;
private String name;
//注意,必须加上pulic 无参构造器
public ExUser() {
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = (String)in.readObject();
this.age = in.readInt();
}
}
我们接着编写测试类:
public class serTest2 {
public static void main(String[] args) throws Exception, IOException {
SerializeUser();
DeSerializeUser();
}
/**
* 序列化方法
* @throws IOException
*/
private static void SerializeUser() throws IOException {
ExUser user = new ExUser();
user.setAge(10);
user.setName("小王");
//序列化对象到指定的文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\jun\\Desktop\\example"));
oos.writeObject(user);
oos.close();
System.out.println("序列化对象成功");
}
/**
* 反序列化方法
* @throws IOException
* @throws ClassNotFoundException
*/
private static void DeSerializeUser() throws IOException, ClassNotFoundException {
File file = new File("C:\\Users\\jun\\Desktop\\example");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
ExUser newUser = (ExUser)ois.readObject();
System.out.println("反序列化对象成功:"+ newUser.getName()+ ","+newUser.getAge());
}
}
结果:
序列化对象成功
反序列化对象成功:小王,10
因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,transient关键字在这里是无效的。
对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
serialVersionUID的作用
如果反序列化使用的serialVersionUID与序列化时使用的serialVersionUID不一致,会报InvalidCalssException异常。这样就保证了项目迭代升级前后的兼容性
serialVersionUID是序列化前后的唯一标识符,只要版本号serialVersionUID相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!
serialVersionUID有两种显式的生成方式:
- 默认的1L,比如:
private static final long serialVersionUID = 1L; - 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;
静态变量不会被序列化
凡是被static修饰的字段是不会被序列化的,我们来看一个例子:
//实体类
public class Student implements Serializable {
private String name;
public static Integer age;//静态变量
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static Integer getAge() {
return age;
}
public static void setAge(Integer age) {
Student.age = age;
}
}
//测试类
public class shallowCopyTest {
public static void main(String[] args) throws Exception {
Student student1 = new Student();
student1.age = 11;
//序列化,将数据写入指定的文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\student1"));
oos.writeObject(student1);
oos.close();
Student student2 = new Student();
student2.age = 21;
//序列化,将数据写入指定的文件中
ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("D:\\student2"));
oos2.writeObject(student1);
oos2.close();
//读取指定的文件
File file = new File("D:\\student1");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Student student1_new = (Student)ois.readObject();
System.out.println("反序列化对象,student1.age="+ student1_new.getAge());
//读取指定的文件
File file2 = new File("D:\\student1");
ObjectInputStream ois2 = new ObjectInputStream(new FileInputStream(file2));
Student student2_new = (Student)ois2.readObject();
System.out.println("反序列化对象,student2.age="+ student2_new.getAge());
}
}
结果:
反序列化对象,student1.age=21
反序列化对象,student2.age=21
为啥结果都是21?
我们知道对象的序列化是操作的堆内存中的数据,而静态的变量又称作类变量,其数据存放在方法区里,类一加载,就初始化了。
又因为静态变量age没有被序列化,根本就没写入文件流中,所以我们打印的值其实一直都是当前Student类的静态变量age的值,而静态变量又是所有的对象共享的一个变量,所以就都是21
使用序列化实现深拷贝
我们再来看一个例子:
//实体类 继承Cloneable
public class Person implements Serializable{
public String name;//姓名
public int height;//身高
public StringBuilder something;
...//省略 getter setter
public Object deepClone() throws Exception{
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}
//测试类,这边类名笔者就不换了,在之前的基础上改改
public class shallowCopyTest {
public static void main(String[] args) throws Exception {
Person p1 = new Person("小张", 180, new StringBuilder("今天天气很好"));
Person p2 = (Person)p1.deepClone();
System.out.println("对象是否相等:"+ (p1 == p2));
System.out.println("p1 属性值=" + p1.getName()+ ","+ p1.getHeight() + ","+ p1.getSomething());
System.out.println("p2 属性值=" + p2.getName()+ ","+ p2.getHeight() + ","+ p2.getSomething());
// change
p1.setName("小王");
p1.setHeight(200);
p1.getSomething().append(",适合出去玩");
System.out.println("...after p1 change....");
System.out.println("p1 属性值=" + p1.getName()+ ","+ p1.getHeight() + ","+ p1.getSomething());
System.out.println("p2 属性值=" + p2.getName()+ ","+ p2.getHeight() + ","+ p2.getSomething());
}
}
结果:
对象是否相等:false
p1 属性值=小张,180,今天天气很好
p2 属性值=小张,180,今天天气很好
...after p1 change....
p1 属性值=小王,200,今天天气很好,适合出去玩
p2 属性值=小张,180,今天天气很好
详情见:https://mp.weixin.qq.com/s/M4--Btn24NIggq8UBdWvAw
常见序列化协议对比
除了JDK 自带的序列化方式,还有一些其他常见的序列化协议:
- 基于二进制: hessian、kyro、protostuff
- 文本类序列化方式: JSON 和 XML
采用哪种序列化方式,我们一般需要考虑序列化之后的数据大小,序列化的耗时,是否支持跨平台、语言,或者公司团队的技术积累。这边就不展开讲了,大家感兴趣自行去了解
小结
- JDK自带序列化方法一般有2种:
继承Serializable接口和继承Externalizable接口 - static修饰的类变量、transient修饰的实例变量都不会被序列化。
- 序列化对象的引用类型成员变量,也必须是可序列化的
- serialVersionUID 版本号是序列化和反序列化前后唯一标识,建议显式定义
- 序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,反序列化出来的对象会有一定风险。可以重写readObject()方法,加以限制
- 除了JDK自带序列化方法,还有hessian、kyro、protostuff、 JSON 和 XML等
参考资料:
《On Java 8》
https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html
https://www.zhihu.com/question/26475281/answer/1898221893
本篇文章到这里就结束啦,很感谢你能看到最后,如果觉得文章对你有帮助,别忘记关注我!更多精彩的文章

面试题:Java序列化与反序列化的更多相关文章
- Java序列化与反序列化三连问:是什么?为什么要?如何做?
Java序列化与反序列化是什么? Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程: 序列化:对象序列化的最主要的用处就是在传递和保存对象 ...
- Java序列化与反序列化
Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?本文围绕这些问题进行了探讨. 1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列 ...
- [转] Java序列化与反序列化
原文地址:http://blog.csdn.net/wangloveall/article/details/7992448 Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java ...
- Java序列化与反序列化(Serializable)
Java序列化与反序列化(Serializable) 特别注意: 1.要序列化的类必须实现Serializable借口 2.在反序列化(读取对象)的时候必须额外捕获EOFException 3.序列化 ...
- Java基础(五)-Java序列化与反序列化
.output_wrapper pre code { font-family: Consolas, Inconsolata, Courier, monospace; display: block !i ...
- JAVA序列化和反序列化XML
package com.lss.utils; import java.beans.XMLDecoder; import java.beans.XMLEncoder; import java.io.Bu ...
- Java序列化与反序列化(实践)
Java序列化与反序列化(实践) 基本概念:序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储和传输数据. 昨天在一本书上 ...
- java序列化与反序列化(转)
Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?本文围绕这些问题进行了探讨. 1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列 ...
- Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?
Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?本文围绕这些问题进行了探讨. 1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节 ...
- (记录)Jedis存放对象和读取对象--Java序列化与反序列化
一.理论分析 在学习Redis中的Jedis这一部分的时候,要使用到Protostuff(Protobuf的Java客户端)这一序列化工具.一开始看到序列化这些字眼的时候,感觉到一头雾水.于是,参考了 ...
随机推荐
- go-zero微服务实战系列(五、缓存代码怎么写)
缓存是高并发服务的基础,毫不夸张的说没有缓存高并发服务就无从谈起.本项目缓存使用Redis,Redis是目前主流的缓存数据库,支持丰富的数据类型,其中集合类型的底层主要依赖:整数数组.双向链表.哈希表 ...
- redis主从复制(九)
先来简单了解下redis中提供的集群策略, 虽然redis有持久化功能能够保障redis服务器宕机也能恢复并且只有少量的数据损失,但是由于所有数据在一台服务器上,如果这台服务器出现硬盘故障,那就算是有 ...
- kubernetes之常用核心资源对象
部门产品线本身是做DEVOPS平台,最近部署架构也在往K8S上靠了,不得不学一下K8S.自己搭建了K8S集群与harbor仓库来学习. 1.kubernetes之常用核心资源对象 1.1.K8s服务部 ...
- Win10默认以管理员身份运行cmd命令提示符
如图所示操作
- Linux 磁盘挂载和swap空间管理
挂载:把指定的设备和根下面的某个文件夹建立关联 卸载:解除两者关系的过程 挂载文件系统:mount 格式:mount device mountpoint --- mount 设备名 挂载点 mount ...
- tail -f 、tail -F、tailf的区别
三者经常在工作中会使用到,以下是三条命令的区别,帮忙大家理解:1.tail -f 等同于--follow=descriptor,根据文件描述符进行追踪,当文件改名或被删除,追踪停止,但是不是断开. 2 ...
- 最小生成树 链式前向星 Prim&Kruskal
Prim: Prim的思想是将任意节点作为根,再找出与之相邻的所有边(用一遍循环即可),再将新节点更新并以此节点作为根继续搜,维护一个数组:dis,作用为已用点到未用点的最短距离. 证明:Prim算法 ...
- 隐私计算FATE-多分类神经网络算法测试
一.说明 本文分享基于 Fate 使用 横向联邦 神经网络算法 对 多分类 的数据进行 模型训练,并使用该模型对数据进行 多分类预测. 二分类算法:是指待预测的 label 标签的取值只有两种:直白来 ...
- 静态代码块和数组工具类Arrays
静态代码块 静态代码块:定义在成员位置,使用static修饰的代码块{ }. ~位置:类中方法外. ~执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行 格式: public cl ...
- 配置git的ssh
Linux,Windows就在git bash here里面输 ① 初始化git账户 git config --global user.name "Eisen" git confi ...