前言

什么是序列化和反序列化?

  • 序列化:将对象的二进制数据流写入硬盘(或用于网络传输);
  • 反序列化:从硬盘(网络)中将对象的二进制数据流读取出来转化成对象。

简介

实现了java.io.serializable标记性接口的类是可序列化的,可序列化类的所有子类型都是可序列化的。

源码

1
2
public interface Serializable {
}

Java中序列化接口是一个空接口,仅用于标记可序列化的语义。

序列化UID字段:serialVersionUID

实现了java.io.Serializable接口的类,如果未显式进行声明,在编译期JVM将使用自己的算法生成默认的serialVersionUID字段。

默认的serialVersionUID生成算法对类的详细信息非常敏感,因不同的JVM实现而异,并且在反序列化过程中会可能会导致意外的java.io.InvalidClassException异常。

当进行序列化操作时,会将此serialVersionUID序列化进二进制流中;

当进行反序列化操作时,如果用来接收对象的类中的serialVersionUID字段值与序列化时的值不一致,会导致反序列化失败。

代码演示

首先创建一个用来序列化测试的类SerializableClass,并实现序列化接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.sunchaser.javase.base.serizlizable;

import java.io.Serializable;

/**
* 序列化类
* @author sunchaser
* @date 2020/3/19
* @since 1.0
*/
public class SerializableClass implements Serializable {
private static final long serialVersionUID = 5135631042912401553L;
private String name;
private Integer age;

public String getName() {
return name;
}

public SerializableClass setName(String name) {
this.name = name;
return this;
}

public Integer getAge() {
return age;
}

public SerializableClass setAge(Integer age) {
this.age = age;
return this;
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("SerializableClass{");
sb.append("name='").append(name).append('\'');
sb.append(", age=").append(age);
sb.append('}');
return sb.toString();
}
}

接下来创建测试类来进行序列化。首先在当前类所在包中创建serializableClass.txt文件。之后我们序列化写对象写入该文件中,反序列化从该文件中读对象。

使用以下代码即可获取txt文件的绝对路径:

1
2
3
// 获取当前类所在包中的serializableClass.txt文件路径
String path = SerializableTest.class.getResource("").getPath();
path = path.replace("target/classes","src/main/java") + "serializableClass.txt";

接下来创建用来序列化的对象:

1
SerializableClass sc = new SerializableClass().setName("序列化").setAge(10);

然后我们创建序列化的方法writeObject,将对象和txt文件绝对路径传入进行写对象操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static void writeObject(SerializableClass sc,String path) {
FileOutputStream fos = null;
ObjectOutputStream ops = null;
try {
fos = new FileOutputStream(path);
ops = new ObjectOutputStream(fos);
ops.writeObject(sc);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (ops != null) {
ops.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

main方法中调用即可将对象写入文本文件中,可打开文本文件查看(有点乱码)。

下面我们来创建反序列化的方法readObject,将文本文件路径传入进行读对象操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void readObject(String path) {
FileInputStream fis = null;
ObjectInputStream ois = null;
try {
fis = new FileInputStream(path);
ois = new ObjectInputStream(fis);
SerializableClass sc = (SerializableClass) ois.readObject();
System.out.println(sc);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

调用该方法后可在控制台看到对象打印:

1
SerializableClass{name='序列化', age=10}

反序列化失败演示

先调用writeObject方法将对象序列化写入文本文件,然后去修改SerializableClassserialVersionUID字段的值,再去调用readObject方法从文本文件中反序列化读对象。这时反序列化就会失败,并抛出java.io.InvalidClassException异常。

主要异常堆栈信息如下:

1
2
3
java.io.InvalidClassException: com.sunchaser.javase.base.serizlizable.SerializableClass; 

local class incompatible: stream classdesc serialVersionUID = 5135632042912401553, local class serialVersionUID = 5135631042912401553

IDEA中生成serialVersionUID

打开IDEA,选择File->Settings->Editor->Inspections,在搜索框中输入serialVersionUID,找到Serializable class without 'serialVersionUID',进行勾选,点击apply->OK进行保存。

设置之后如果实现了Serializable接口的类未定义serialVersionUID字段,则类名处会有黄色警告,输入光标移动至类名处按下快捷键alt+enter会有生成serialVersionUID的快捷方式。

总结

本文完整代码地址:传送门

Serializable接口实际上就是一个标记,实现了该接口的类才允许被序列化和反序列化,而serialVersionUID字段则像是一个“版本号”,序列化时将该字段一起存至二进制流中,反序列化时用该字段来判断是否是存进去时的状态。它的作用其实就是判断反序列化出来的对象是不是原来序列化的对象。