一文看懂Java序列化
一文看懂Java序列化
简介
首先我们看一下wiki上面对于序列化的解释。
序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。
以最简单的方式来说,序列化就是将内存中的对象变成网络或则磁盘中的文件。而反序列化就是将文件变成内存中的对象。(emm,序列化就是将脑海中的“老婆”变成纸片人?反序列化就是将纸片人变成脑海中的“老婆”?当我没说)如果说的代码中具体一点,序列化就是将对象变成字节,而反序列化就是将字节恢复成对象。
当然,你在一个平台进行序列化,在另外一个平台也可以进行反序列化。
对象的序列化主要有两种用途:
1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(比如说服务器上用户的session对象)
2. 在网络上传送对象的字节序列。(比如说进行网络通信,消息(可以是文件)肯定要变成二进制序列才能在网络上面进行传输)
OK,既然我们已经了解到什么是(反)序列化了,那么多说无益,让我们来好好的看一看Java是怎么实现的吧。
Java实现
对于Java这把轻机枪来说,既然序列化是一个很重要的部分,那么它肯定自身提供了序列化的方案。
在Java中,只有实现了Serializable和Externalizable接口的类的对象才能够进行序列化。在下面将分别对两者进行介绍。
Serializable
最基本情况
Serializable可以说是最简单的序列化实现方案了。它就是一个接口,里面没有任何的属性和方法。一个类通过implements Serializable标示着这个类是可序列化的。下面将举一个简单的例子:
public class People implements Serializable {
private String name;
private int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
People类显而易见,是可序列化的。那么我们如何来实现可序列化呢?在序列化的过程中,有两个步骤:
- 序列化
- 创建一个ObjectOutputStream输出流。
- 调用ObjectOutputStream的writeObject函数输出可序列化的对象。
public class Main {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
People people = new People("name", 18);
oos.writeObject(people);
}
}
ObjectOutputStream对象中需要一个输出流,这里使用的是文件输出流(也可以是用其他输出流,例如System.out,输出到控制台)。然后我们通过调用writeObject就可以讲people对象写入到“object.txt”了。
- 反序列化
我们重新编辑People的构造方法,在里面添加一个输出来查看反序列化是否会进行调用构造函数。
public class People implements Serializable {
private String name;
private int age;
public People(String name, int age) {
System.out.println("是否调用序列化?");
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
反序列化和序列化一样,也分为2个步骤:
- 创建一个ObjectInputStream输入流
- 调用ObjectInputStream中的readObject函数得到序列化的对象
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people = (People) ois.readObject();
System.out.println(people);
}
}
下面是程序运行之后的控制台的图片。

可以很明显的看见,反序列化的时候,并没有调用People的构造方法。反序列化的对象是由JVM自己生成的对象,而不是通过构造方法生成。
Ok,通过上面我们简单的学会了序列化的使用,那么,我们会有一个问题,一个对象在序列化的过程中,有哪一些属性是可是序列化的,哪一些是不可序列化的呢?
通过查看源代码,我们可以知道:

对象的类,签名和非transient和非static变量会写入到类中。
类的成员为引用
看到很多博客都是这样说的:
如果一个可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
其实这样说不是很准确,因为即使是String类型,里面也实现了Serializable这个接口。

我们新建一个Man类,但是它并没有实现Serializable方法。
public class Man{
private String sex;
public Man(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Man{" +
"sex='" + sex + '\'' +
'}';
}
}
然后在People类中进行引用。
public class People implements Serializable {
private String name;
private int age;
private Man man;
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
", man=" + man +
'}';
}
public People(String name, int age, Man man) {
this.name = name;
this.age = age;
this.man = man;
}
}
如果我们进行序列化,会发生以下错误:
java.io.NotSerializableException: People
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Main.main(Main.java:41)
因为Man是不可序列化的,也就导致了People类是不可序列化的。
同一对象多次序列化
大家看一下下面的这段代码:
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
People people = new People("name", 11);
oos.writeObject(people);
oos.writeObject(people);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
People people2 = (People) ois.readObject();
System.out.println(people1 == people2);
}
}
你们觉得会输出啥?
最后的结果会输出true。
然后大家再看一段代码,与上面代码不同的是,People在第二次writeObject的时候,对name进行了重新赋值操作。
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
People people = new People("name", 11);
oos.writeObject(people);
people.setName("hello");
oos.writeObject(people);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
People people2 = (People) ois.readObject();
System.out.println(people1 == people2);
}
}
结果会输出啥?
结果还是:true,同时在people1和people2对象中,name都为“name”,而不是为“hello”。
why??为什么会这样?
在默认情况下,对于一个实例的多个引用,为了节省空间,只会写入一次。而当写入多次时,只会在后面追加几个字节而已(代表某个实例的引用)。
但是我们如果向在后面追加实例而不是引用那么我们应该怎么做?使用rest或writeUnshared即可。
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
People people = new People("name", 11);
oos.writeObject(people);
people.setName("hello");
oos.reset();
oos.writeObject(people);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
People people2 = (People) ois.readObject();
System.out.println(people1);
System.out.println(people2);
System.out.println(people1 == people2);
}
}
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
People people = new People("name", 11);
oos.writeObject(people);
people.setName("hello");
oos.writeUnshared(people);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
People people2 = (People) ois.readObject();
System.out.println(people1);
System.out.println(people2);
System.out.println(people1 == people2);
}
}
子父类引用序列化
子类和父类有两种情况:
- 子类没有序列化,父类进行了序列化
- 子类进行序列化,父类没有进行序列化
emm,第一种情况不需要考虑,肯定不会出错。让我们来看一看第二种情况会怎么样!!
父类Man类
public class Man {
private String sex;
public Man(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Man{" +
"sex='" + sex + '\'' +
'}';
}
}
子类People类:
public class People extends Man implements Serializable {
private String name;
private int age;
public People(String name, int age, String sex) {
super(sex);
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
"} " + super.toString();
}
}
如果这个时候,我们对People进行序列化会怎么样呢?会报错!!
Exception in thread "main" java.io.InvalidClassException: People; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2098)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1625)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:465)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:423)
at Main.main(Main.java:38)
如何解决,我们可以在Man中,添加一个无参构造器即可。这是因为当父类不可序列化的时候,需要调用默认无参构造器初始化属性的值。
可自定义的可序列化
我们会有一个疑问,序列化可以将对象保存在磁盘或者网络中,but,我们如何能够保证这个序列化的文件的不会被被人查看到里面的内容。假如我们在进行序列化的时候就像这些属性进行加密不就Ok了吗?(这个仅仅是举一个例子)
可自定义的可序列化有两种情况:
- 某些变量不进行序列化
- 在序列化的时候改变某些变量
在上面我们知道transient和static的变量不会进行序列化,因此我们可以使用transient来标记某一个变量来限制它的序列化。
在第二中情况我们可以通过重写writeObject与readObject方法来选择对属性的操作。(还有writeReplace和readResolve)
在下面的代码中,通过transient来限制name写入,通过writeObject和readObject来对写入的age进行修改。
public class People implements Serializable {
transient private String name;
private int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(age + 1);
}
private void readObject(ObjectInputStream in) throws IOException {
this.age = in.readInt() -1 ;
}
}
至于main函数怎么调用?还是正常的调用:
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
People people = new People("name", 11);
oos.writeObject(people);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
}
}
Externalizable:强制自定义序列化
这个,emm,“强制”两个字都懂吧。让我们来看一看这个接口的源代码:
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
简单点来说,就是类通过implements这个接口,实现这两个方法来进行序列化的自定义。
public class People implements Externalizable {
private String name;
private int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
// 注意必须要一个默认的构造方法
public People() {
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(this.age+1);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.age = in.readInt() - 1;
}
}
两者之间的差异
| 方案 | 实现Serializable接口 | 实现Externalizable接口 |
|---|---|---|
| 方式 | 系统默认决定储存信息 | 程序员决定存储哪些信息 |
| 方法 | 使用简单,implements即可 | 必须实现接口内的两个方法 |
| 性能 | 性能略差 | 性能略好 |
序列化版本号serialVersionUID
我相信很多人都看到过serialVersionUID,随便打开一个类(这里是String类),我么可以看到:
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
使用来自JDK 1.0.2 的serialVersionUID用来保持连贯性
这个serialVersionUID的作用很简单,就是代表一个版本。当进行反序列化的时候,如果class的版本号与序列化的时候不同,则会出现InvalidClassException异常。
版本好可以只有指定,但是有一个点要值得注意,JVM会根据类的信息自动算出一个版本号,如果你更改了类(比如说添加/修改了属性或者方法),则计算出来的版本号就发生了改变。这样也就代表这你无法反序列化你以前的东西。
什么情况下需要修改serialVersionUID呢?分三种情况。
- 修改了方法,这个当然版本好不需要改变
- 修改了静态变量或者transient关键之修饰的变量,同样不需要修改。
- 新增了变量或者删除了变量也不需要修改。如果是新增了变量,则进行反序列化的时候会给新增的变量赋一个默认值。如果是修改了变量,则进行反序列化的时候无需理会被删除的值。
讲完了讲完了,序列化实际上还是挺简单。不过需要注意使用的时候遇到的坑。~~
一文看懂Java序列化的更多相关文章
- 一文看懂Java序列化之serialVersionUID
serialVersionUID适用于Java的序列化机制.简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的.在进行反序列化时,JVM会把传来的字节流中的 ...
- 一文看懂java io系统 (转)
出处: 一文看懂java io系统 学习java IO系统,重点是学会IO模型,了解了各种IO模型之后就可以更好的理解java IO Java IO 是一套Java用来读写数据(输入和输出)的A ...
- 夯实Java基础系列22:一文读懂Java序列化和反序列化
本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...
- 一文看懂java的IO流
废话不多说,直接上代码 import com.fasterxml.jackson.databind.ObjectMapper; import java.io.*; import java.nio.ch ...
- 一文看懂Java Worker 设计模式
Worker模式 想解决的问题 异步执行一些任务,有返回或无返回结果 使用动机 有些时候想执行一些异步任务,如异步网络通信.daemon任务,但又不想去管理这任务的生命周.这个时候可以使用Worker ...
- 一文看懂web服务器、应用服务器、web容器、反向代理服务器区别与联系
我们知道,不同肤色的人外貌差别很大,而双胞胎的辨识很难.有意思的是Web服务器/Web容器/Web应用程序服务器/反向代理有点像四胞胎,在网络上经常一起出现.本文将带读者对这四个相似概念如何区分. 1 ...
- [转帖]一文看懂web服务器、应用服务器、web容器、反向代理服务器区别与联系
一文看懂web服务器.应用服务器.web容器.反向代理服务器区别与联系 https://www.cnblogs.com/vipyoumay/p/7455431.html 我们知道,不同肤色的人外貌差别 ...
- 一文看懂大数据的技术生态圈,Hadoop,hive,spark都有了
一文看懂大数据的技术生态圈,Hadoop,hive,spark都有了 转载: 大数据本身是个很宽泛的概念,Hadoop生态圈(或者泛生态圈)基本上都是为了处理超过单机尺度的数据处理而诞生的.你可以把它 ...
- 转载来自朱小厮博客的 一文看懂Kafka消息格式的演变
转载来自朱小厮博客的 一文看懂Kafka消息格式的演变 ✎摘要 对于一个成熟的消息中间件而言,消息格式不仅关系到功能维度的扩展,还牵涉到性能维度的优化.随着Kafka的迅猛发展,其消息格式也在 ...
随机推荐
- 脚手架搭建vue项目
1.安装安装node.js: 2.cnpm install vue-cli -g (全局安装,需要注意的是我这里是用淘宝镜像安装的,没有安装cnpm的需要先去安装一下) 3.vue --version ...
- linux c 调用 so 库
/***********编译时要链接 -l dl 库************/ #include<stdlib.h> #include<stdio.h> #include< ...
- js中使用EL表达式总结
1.js中使用el表达式要加双引号或单引号:'${list}' 2.js变量获取el表达式中的对象:不能直接获取,直接获取得到的是该对象的toString值. 有两种方法:一:el中直接写对象的属性v ...
- iTOP-4418开发板_重实力_优势突出_有原理图源码开源
核心板参数 尺寸:50mm*60mm 高度:核心板连接器组合高度1.5mm PCB层数:6层PCB沉金设计 4418 CPU:ARM Cortex-A9 四核 S5P4418处理器 1.4GHz 68 ...
- Git内部原理(1)
Git本质上是一套内容寻址文件系统,在此之上提供了VCS的用户界面. Git底层命令(plumbing) vs 高层命令(porcelain) Git的高层命令包括checkout.branch.re ...
- Java基础语法要点
1.Java中byte.short.int.long的取值范围 byte:[-128,127] short:[-32768,32767] int:[-2147483648,2147483647] lo ...
- Outlook邮件的右键菜单中添加自定义按钮
customUI代码如下: <customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui"> ...
- django操作非ORM创建的表
问题:django的ORM怎么连接已存在的表,然后进行增删查改操作? 工作中会遇见很多二次开发的时候,表都是已经创建好的,用django的ORM进行二次开发,怎么操作数据库中的表呢? 下面介绍 ...
- 基于ci框架 修改出来了一个带农历的万年历。
1这里没有写model:代码一看就懂,没什么负杂地方,就是麻烦一点. 直接control模块的代码: <?php if ( ! defined('BASEPATH')) exit('No dir ...
- JarvisOJ level3_x64
这一题是和前面x86的差不多,都是利用了同一个知识点,唯一的区别就是使用的堆栈地址不同,x86是直接使用堆栈来传递参数的,而x64不同 x64的函数调用时整数和指针参数按照从左到右的顺序依次保存在寄存 ...