序列化与反序列化
# 12.序列化与反序列化
序列化是指把一个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()
方法
参考: