对象序列化

序列化

第一节 概述

1.1 什么是序列化?

序列化指将一个Java对象转化为二进制序列,可以看作一个 byte[] 字节数组。

1.2 为什么要序列化?

需要把对象等数据转为字节序列进行传输。序列化后方便把对象存入文件或是通过网络传输到远程服务器。

1.3 使用场景

  • 持久保存:保存对象的字节序列到本地文件或数据库
  • 网络传输:序列化后写入字节流使对象得以在网络中传输
  • 传递对象:通过序列化在进程间传递对象

1.4 序列化与反序列化

把对象等数据转为字节序列的这个过程就是序列化,还原的过程就是反序列化。

注意:反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。


第二节 Java序列化

Java支持对象序列化机制可以将任何对象写出到流中,并在之后读回。其实现机制主要基于两个接口 SerializableExternalizable

实现 Serializable 接口表示此类的对象是可序列化的。如果类中的成员变量是引用类型,那么它也必须实现Serializable 接口才能保证序列化成功。

2.1 ObjectInputStream与ObjectOutputStream

可以通过库提供的 ObjectInputStreamObjectOutputStream 类来进行对象的序列化。

以下示例通过 ObjectOutputStream 保存对象数据,ObjectInputStream 读回对象数据。

1
2
3
4
5
6
7
8
9
Employee employee = new Employee("张三",3000,1995,8,1);
Manager manager = new Manager("李四",8000,1986,12,15);
oos.writeObject(employee);
oos.writeObject(manager);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.dat"));
Employee e1 = (Employee)in.readObject();
Employee e2 = (Employee)in.readObject();
System.out.println(e1.toString());
System.out.println(e2.toString());

结果如下:

1
2
name[张三] pay[3000] year[1995] month[8] day[1]
name[李四] pay[8000] year[1986] month[12] day[15]

writeObject()readObject() 方法只能写出对象,如果想使用基本数据类型需要实现 DataInputDataOutput 定义的相对方法,对象流已经都实现了所需方法:readXX()writeXX() 系列方法支持各种数据类型的字节读写(包括各种基本类型、UTF-8编码的String和实现了Serializable接口的Object等)。

ObjectOutputStream(序列化)可以把一个Java对象变为 byte[] 数组,它负责把一个Java对象写入一个字节流。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public class ObjectOutputStream
extends OutputStream implements ObjectOutput, ObjectStreamConstants{

public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}

/**
* Underlying writeObject/writeUnshared implementation.
*/
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}

// check for replacement object
Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}

// if object replaced, run through original checks a second time
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}

// remaining cases 当对象非字符串、数组、枚举类以及Serializable时会抛出NotSerializableException
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) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}

public void writeInt(int val) throws IOException {
bout.writeInt(val);
}
...
public void writeInt(int v) throws IOException {
if (pos + 4 <= MAX_BLOCK_SIZE) {
Bits.putInt(buf, pos, v);
pos += 4;
} else {
dout.writeInt(v);
}
}
...
}

ObjectInputStream(反序列化)则负责从一个字节流读取Java对象。

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
43
44
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants{

public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}

// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}

public boolean readBoolean() throws IOException {
return bin.readBoolean();
}
...
public boolean readBoolean() throws IOException {
int v = read();
if (v < 0) {
throw new EOFException();
}
return (v != 0);
}
...
}

ObjectInputStream 可能抛出的异常包括:

  • ClassNotFoundException :没有找到对应的Class,如Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。
  • InvalidClassException :Class不匹配,如序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。

为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的 serialVersionUID 静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。

2.2 序列化ID

序列化ID(serialVersionUID)可以看作版本号,序列化对象的唯一标识,如果序列化ID不同(比如新版本不兼容旧版本)或修改了对象的信息,反序列化时就会抛出 InvalidClassException 异常。

序列化ID可以通过以下两种方式生成:

1
2
3
4
// 默认1L
private static final long serialVersionUID = 1L;
// 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段
private static final long serialVersionUID = xxxL;

2.3 安全性

因为Java的序列化机制可以导致一个实例能直接从 byte[] 数组创建,而不经过构造方法,因此,它存在一定的安全隐患。一个精心构造的 byte[] 数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。

实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。

2.4 Transient

某些域是不可被序列化的或着说没有序列化的意义,Java可以通过transient关键字来标记这类域,详细可看这篇static,final,transient,volatile等关键字原理作用

2.5 自定义序列化

对于一个对象只要实现 Serializable 接口,就会可以对 非transient 以及 非static 修饰属性进行序列化工作(静态属性属于类信息,所以不会被序列化)。

如果想要自定义序列化的属性,除了transient也可以通过重写 readObject()writeObject() 方法来代替默认的自动序列化或通过实现Externalizable接口。

部分属性序列化(自定义序列化)的方法:

  • transient
  • 重写 readObject()writeObject()
  • 实现 Externalizable 接口

2.6 writeReplace() 和 readResolve()

  • writeReplace() :在序列化时,会先调用此方法,再调用 writeObject() ,此方法可将任意对象代替目标序列化对象。
  • readResolve() :反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃,此方法在 readObject() 后调用。readResolve常用来反序列单例类,保证单例类的唯一性。

注意:readResolve()writeReplace() 的访问修饰符可以是private、protected、public,如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。通常建议对于final修饰的类重写 readResolve() 方法没有问题;否则,重写 readResolve() 使用private修饰。

2.7 Externalizable

Externalizable 接口继承自 Serializable ,声明了两个方法 writeExternal()readExternal()

1
2
3
4
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

这两个方法可以对包含超类在内的对象所有数据的存储和恢复进行管理,序列化机制在流中仅仅记录对象所属的类。在读入可外部化的类时,对象流将调用无参构造器创建一个新的对象(所以实现Externalizable接口一定要声明无参构造器),然后调用 readExternal() 方法,将字段值分别填充到新的对象中。

readObject()writeObject()私有的,只能被序列化机制调用,而 readExternal()writeExternal()公共的。

Serializable和Externalizable的区别:

  • Serializable没有声明方法,只是一个标识接口。
  • Serializable实现时不需要重写 readObject()writeObject(),有默认实现。
  • 实现Externalizable接口必须声明无参构造器,Serializable则采用反射机制完成内容恢复,没有这个限制。
  • Externalizable方式不需要序列化ID,而Serializable接口则需要。
  • 相比Serializable,Externalizable序列化、反序列更加快速,占用相比较小的内存。

虽然Externalizable接口带来了一定的性能提升,但相应的复杂度也提高了,所以一般还是会通过实现Serializable接口进行序列化。


第三节 原理

3.1 序列化流程

Java序列化同一对象,多次的序列化只会得到相同的结果。可以编程试验一下,原因就在于序列化时会对是否序列化过进行检测。

Java序列化算法简单描述:

  1. 所有保存到磁盘的对象都有一个序列化编码号。
  2. 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
  3. 如果此对象已经序列化过,则直接输出编号即可。

3.2 序列化后内容改变

当一个对象在序列化后,内容发生了修改,再次序列化会如何?

答案是仍然不会再次进行序列化转换,只是保存一下序列化编号。

什么情况下会导致serialVersionUID修改呢?

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号。
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号。
  • 如果修改了非瞬态变量,则可能导致反序列化失败。
  • 如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。
  • 如果只是新增了实例变量,则反序列化回来新增的是默认值;
  • 如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

3.3 被多个对象共享

当一个对象被多个对象共享时,序列化过程是怎么样的呢?

每个对象都是用一个序列号保存,第一次调用时,将对象数据保存到流中,如果此对象已被保存过,则只写出”与以保存序列号x对象相同”。

对于流中的对象,第一次遇到其序列号时,构建对象,并使用流中的数据来初始化,再记录此序号和新对象之间的关联,当遇到”与以保存序列号x对象相同”此标记时,获取与序号关联的对象引用。

对象序列化以特殊的文件格式来存储对象数据,通过对类的域进行排序,然后通过 SHA安全散列算法 得到SHA码,只取前8个字节作为类的指纹(指纹即其身份证明)。可以通过对象的指纹和其所属类的指纹进行比对,当类信息改变时,指纹大概率会不同(注意是类的结构信息,而非对象数据),所以当两者不同时就抛出一个异常。

具体如何进行编码和排布对于当前的学习不是那么重要,只需要知道对象流输出中包含所有对象的类型和数据域,每个对象都被赋予一个序列号,相同对象的重复出现将被存储为对这个对象序列号的引用。

3.4 序列化单例和类型安全的枚举

序列化枚举类,当枚举类或使用单例模式进行序列化化时目标对象是唯一的,这其中隐藏着一个问题:即序列化后的对象和原对象是相等吗?

我们实现单例模式时,期望得到的实例应该是唯一的,而序列化的对象却是一个全新的对象,所以其它方式实现单例模式时,只要实现了Serializable接口,目标对象就不唯一了,所以需要加如 readResolve()方法来避免这种情况,此方法会直接返回单例对象。

而使用Java中的枚举类时不会出现此问题,因为Jvm限制枚举类型及其枚举常量是唯一的,在序列化时只会将枚举对象的name属性输出到流中,反序列化时在通过 Enum.valueOf() 方法根据名字来查找枚举对象。同时Jvm禁止了枚举类的 readObject()writeObject()readResolve() 等方法,避免上述规范被破坏。

1
2
3
4
5
protected Object readResolve() throws ObjectStreamException {//定义了此方法后,对象被序列化时会被调用,其返回的对象在之后会成为readObject()的返回值。
if(filed == A) return Demo.A;
else if(filed == B) return Demo.B;
return null;
}

3.5 克隆和序列化

Java对象克隆这篇博客中有谈到用序列化去实现克隆,相比重载 equals() 还要更为简单。只要声明对象为可序列化的,将其序列化到流中再读回,即可得到一个深拷贝的结果,但序列化的性能相比复制数据域来说是要慢很多的,所以个人觉得非深克隆的情况还是避免使用好一点。


参考:

🔗 《Java核心技术 卷Ⅱ》

🔗 序列化

🔗 java序列化