序列化与反序列化

序列化:将Java对象转换为字节序列,以便持久化到磁盘或者网络传输。

反序列化:将磁盘文件或者网络文件中的字节序列恢复为原先的Java对象。

Java对象的序列化的方式有两种:

  • 实现Serializable接口,比较方便。
  • 实现Exteranlizable接口,需要重写writeExternalreadExternal方法。

定义User:

1
2
3
4
5
6
7
8
public class User implements Serializable {
private Integer id;
private String username;
private Date birth;

// AllArgsConstructor
// ToString
}

序列化:

1
2
3
4
5
6
public static void serialize(Object obj) throws IOException {
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(
new File(System.getProperty("user.dir") + "/src/main/resources/public/" + "user.txt")));
output.writeObject(obj);
output.close();
}

反序列化:

1
2
3
4
5
public static User deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream input = new ObjectInputStream(new FileInputStream(
new File(System.getProperty("user.dir") + "/src/main/resources/public/" + "user.txt")));
return (User) input.readObject();
}

测试:

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
User user = new User(1, "Khighness", new Date());
serialize(user);
User des = deserialize();
System.out.println(des);
}

控制台输出:

1
User[id=1, username='Khighness', birth=Thu Apr 22 12:12:48 CST 2021]

Serializable

看一下源代码:

1
2
public interface Serializable {
}

发现只是个空接口,因此这只是个标示性接口。
那我们点进刚才使用的writeObject方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final void writeObject(Object obj) throws IOException {
if (enableOverride) { // 表示是否可覆写,默认false
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false); // 主要执行的方法
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}

可以看到它调用的是writeObject0方法,再进入这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// remaining cases 判断对象
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);
} else if (obj instanceof Serializable) { // 是否实现了Serializable接口
writeOrdinaryObject(obj, desc, unshared);
} else { // 四非则抛出异常
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}

serialVersionUID

继续测试,给User类添加字段:

1
private String email;

将测试代码改为:

1
2
3
4
public static void main(String[] args) throws Exception {
User des = deserialize();
System.out.println(des);
}

运行结果:

1
Exception in thread "main" java.io.InvalidClassException: top.parak.entity.User; local class incompatible: stream classdesc serialVersionUID = -5676367428859465228, local class serialVersionUID = -1182591043963610802

发现报错,并且抛出InvalidClassException异常,提示信息:本地类不兼容,序列化前后的serialVersionUID不同。
因此,有两个重要结论:

  • serialVersionUID是序列化前后的唯一标识符。
  • 默认如果没有显示定义serialVersionUID,则编译器会为它自动声明一个。

扩展:

  • serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的暗号(连上彼此的讯号才有个依靠),在反序列化时,JVM会把字节流中的序列化ID和被序列号类中的序列化ID做对比,只有两者一致,才能重新反序列化,否则抛出异常终止反序列化的过程。
  • 如果在定义一个可序列化的类时,没有人为显示地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID,一旦更改了类的结构或者信息,则类的serialVersionUID也会跟着变化。

static&transient

继续测试,将User类的email字段添加修饰符static或者transient,发现测试成功。
于是可以得出结论,对于Serilizable接口:

  1. 凡是被static修饰的字段是不会被序列化的
  2. 凡是被transient修饰的字段是不会被序列化的

transient

transient关键字的作用就是把被修饰的字段的生命周期仅存于调用者的内存而不会持久化到磁盘中。

注意

实现Exteranlizable接口时,transient是无效的。

单例模式增强

单例模式下,对象经过序列化和反序列化之后还是一个对象吗?

可以做一个实验。

静态内部类的单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton implements Serializable {
private static final long serialVersionUID = 3992737853791586260L;

private Singleton() { }

private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}

public static synchronized Singleton getInstance() {
return SingletonHolder.singleton;
}
}

然后写一个测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 序列化
Singleton instance = Singleton.getInstance();
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(
new File(System.getProperty("user.dir") + "/src/main/resources/public/" + "singleton.txt")));
output.writeObject(instance);
output.close();
// 反序列化
ObjectInputStream input = new ObjectInputStream(new FileInputStream(
new File(System.getProperty("user.dir") + "/src/main/resources/public/" + "singleton.txt")));
Singleton singleton = (Singleton) input.readObject();
// 输出结果
System.out.println(instance == singleton);
}

运行之后,发现控制台打印的是false

解决方法:在单例中重写readResolve函数,直接返回单例对象,来规避之。

1
2
3
public Object readResolve() {
return SingletonHolder.singleton;
}