序列化与反序列化
# 序列化与反序列化
序列化是指把一个 Java 对象变成二进制内容,本质上就是一个 byte[]
数组。在 Java 中, 序列化是 JDK 1.1 时引入的一组开创性的特性。
# 为什么需要序列化
为什么要把 Java 对象序列化呢?便于存储和传输。因为序列化后可以把 byte[]
保存到文件中,或者把 byte[]
通过网络传输到远程,这样,就相当于把 Java 对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是 byte[]
数组)变回 Java 对象。有了反序列化,保存到文件中的 byte[]
数组又可以“变回”Java 对象,或者从网络上读取 byte[]
并把它“变回”Java 对象。
# 如何序列化一个对象
一个 Java 对象要能序列化,必须实现一个特殊的 java.io.Serializable
接口,它的定义如下:
public interface Serializable {}
Serializable
接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
我们定义一个类,并实现该接口:
import java.io.Serializable;
class Person implements Serializable{
private String name;
public Person(String name){
this.name = name;
}
@Override
public String toString(){
return this.name;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
把一个 Java 对象变为 byte[]
数组,需要使用 ObjectOutputStream
。ObjectOutputStream
既可以写入基本类型,如 int
,boolean
,也可以写入 String
(以 UTF-8 编码),还可以写入实现了 Serializable
接口的 Object
:
Person JXL = new Person("JXL");
try (
FileOutputStream fop = new FileOutputStream("objectFile");
ObjectOutputStream oos = new ObjectOutputStream(fop);
) {
oos.writeInt(114514);
oos.writeUTF("Hello World!");
oos.writeObject(JXL);
} catch (Exception e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
9
10
11
12
# 反序列化一个对象
相应的, ObjectInputStream
能读取基本类型和 String
类型,还可以调用 readObject()
读取一个 Object
对象。要把它变成一个特定类型,必须强制转型:
try (
FileInputStream fis = new FileInputStream("objectFile");
ObjectInputStream ois = new ObjectInputStream(fis);
) {
int n = ois.readInt();
String s = ois.readUTF();
Person JXL2 = (Person)ois.readObject();
System.out.println("n: " + n);
System.out.println("s: " + s);
System.out.println("JXL2: " + JXL2);
} catch (Exception e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
readObject()
可能抛出的异常有:
ClassNotFoundException
:没有找到对应的 Class;InvalidClassException
:Class 不匹配。
我们来讲解下什么时候会出现异常。
# 兼容性问题
对于 ClassNotFoundException
,这种情况常见于一台电脑上的 Java 程序把一个 Java 对象,例如,Person
对象序列化以后,通过网络传给另一台电脑上的另一个 Java 程序,但是这台电脑的 Java 程序并没有定义 Person
类,所以无法反序列化。
对于 InvalidClassException
,这种情况常见于序列化的 Person
对象定义了一个 int
类型的 age
字段,然后序列化的时候存储到了磁盘上。过了一段时间后,在反序列化时,Person
类经过更新,定义的 age
字段改成了 long
类型,所以导致 class 不兼容,反序列化会失败,这里就不演示了。
为了避免这种 class 定义变动导致的不兼容,Java 的序列化允许 class 定义一个特殊的 serialVersionUID
静态变量,用于标识 Java 类的序列化“版本”,通常可以由 IDE 自动生成。如果增加或修改了字段,可以改变 serialVersionUID
的值,这样就能自动阻止不匹配的 class 版本:
public class Person implements Serializable {
private static final long serialVersionUID = 2709425275741743919L;
}
2
3
如果不添加这个 serialVersionUID,是会有警告的,一般 IDE 也会有贴心的提示:
我们可以使用 @SuppressWarnings("serial")
来不显示这个警告。
如果读者有看过 String 类的源码,可以看到也有一个 serialVersionUID:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
}
2
3
4
5
6
由于存在兼容性问题,更好的序列化方法是通过 JSON 这样的通用数据结构来实现,只输出基本类型(包括 String)的内容,而不存储任何与代码相关的信息
# 安全性问题
反序列化时,由 JVM 直接构造出 Java 对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。因此,Java 的序列化机制可以导致一个实例能不经过构造方法,直接从 byte[]
数组创建一个对象,它存在一定的安全隐患:一个精心构造的 byte[]
数组被反序列化后可以执行特定的 Java 代码,从而导致严重的安全漏洞!
# 什么东西能被序列化?
在 Java 中,什么东西能被序列化?首先基本数据类型都可以被序列化,还有字符串、数组和枚举,或者实现了 Serializable
接口的类,否则不能被序列化,我们可以看看 ObjectOutputStream
的 writeObject0()
方法部分源码(writeObject
会调用 writeObject0
方法):
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());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectOutputStream
在序列化的时候,会判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 Serializable
,如果全都不是的话,抛出 NotSerializableException
。
因此,如果我们自定义的类实现了 Serializable
接口,就可以序列化和反序列化了。
需要注意的是,static
和 transient
修饰的字段是不会被序列化的。
序列化保存的是对象的状态,而 static
修饰的字段属于类的状态;而 transient
的中文字义为“临时的”,它可以阻止字段被序列化到文件中,在被反序列化后,transient
字段的值被设为初始值,比如 int
型的初始值为 0,对象型的初始值为 null
。
如果想要深究源码的话,你可以在 ObjectStreamClass
中发现下面这样的代码:
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}
2
3
4
5
6
7
8
9
# 怎么反序列化呢?
以 ObjectInputStream
为例,它在反序列化的时候会依次调用 readObject()
→readObject0()
→readOrdinaryObject()
→readSerialData()
→defaultReadFields()
。
贴出 defaultReadFields()
的部分源码:
private void defaultReadFields(Object obj, ObjectStreamClass desc)
throws IOException
{
//....................
for (int i = 0; i < objVals.length; i++) {
ObjectStreamField f = fields[numPrimFields + i];
objVals[i] = readObject0(Object.class, f.isUnshared());
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
if (obj != null) {
desc.setObjFieldValues(obj, objVals);
}
passHandle = objHandle;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
序列化同理,是由 defaultWriteFields
方法来实现的。
由此可知,序列化和反序列化的具体操作并不是 Serializable
接口来实现的,Serializable
接口只起到了一个标识的作用,因此标识为空完全没问题。
# 自定义序列化
有时候,我们想自己决定哪些字段被保存,哪些不用,此时我们可以改为实现 Externalizable
接口,并实现其 writeExternal()
和 readExternal()
方法,决定哪些字段被序列化,反序列化时如何做等等。
另外,我们还需要定义一个无参的构造方法,因为使用 Externalizable
进行反序列化的时候,会调用被序列化类的无参构造方法去创建一个新的对象,然后再将被保存对象的字段值复制过去。
我们定义要序列化的类:
class Person implements Externalizable{
private String name;
public Person(){}
public Person(String name){
this.name = name;
}
@Override
public String toString(){
return this.name;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String)in.readObject();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
然后测试能否正常序列化和反序列化:
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class IODemo10Seri2 {
public static void main(String[] args) {
Person p = new Person("jxl");
// output
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("objectFile2"))) {
oos.writeObject(p);
} catch (Exception e) {
e.printStackTrace();
}
//input
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("objectFile2"))) {
Person p2 = (Person)ois.readObject();
System.out.println(p2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
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
# 小结
什么是序列化:将一个对象转为 byte[]
数组,反序列化则是将 byte[]
数组转换为对象。
为什么需要序列化:方便传输和存储。
怎么序列化:实现 java.io.Serializable
接口
Serializable
这样的空接口被称为“标记接口”(Marker Interface),真正序列化和反序列化的操作在其他类里,例如 ObjectInputStream
。
最佳实践:
- 要不要用序列化:Java 的序列化机制仅适用于 Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如 JSON。
- 需要注意安全问题:反序列化时不调用构造方法,存在一定的安全隐患
- 需要注意兼容性问题,最好设置
serialVersionUID
作为版本号。 - 要想自定义序列化操作,可实现
Externalizable
接口,并实现其writeExternal()
和readExternal()
方法
参考: