15.8.5 自定义序列化

15.8.5 自定义序列化

递归序列化

  • 当对某个对象进行序列化时,系统会自动把该对象的所有实例变量依次进行序列化,
    • 如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;
      • 如果被引用的对象的实例变量也引用了其他对象,则被引用的对象也会被序列化

这种情况被称为递归序列化

transient关键字

不希望序列化的实例变量的情况

在一些特殊的场景下,如果一个类里包含的某些实例变量是敏感信息,例如银行账户信息等,这时不希望系统将该实例变量值进行序列化;
或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归序列化,以避免引发java.io.NotSerializableException异常。
通过在实例变量前面使用transient关键字修饰,可以指定Java序列化时无须理会该实例变量
transient关键字只能用于修饰实例变量,不可修饰Java程序中的其他成分

程序 transient关键字修饰实例变量

如下Person类与前面的Person类几乎完全一样,只是它的age使用了transient关键字修饰。

1
2
3
4
5
6
7
8
9
10
11
12
public class Person implements java.io.Serializable {
private static final long serialVersionUID = -2595800114629327570L;
private String name;
private transient int age;
// 注意此处没有提供无参数的构造器!
public Person(String name, int age) {
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 此处省略getter和setter方法,请自己补上
}

程序 序列化后反序列化 再输出transient修饰的实例变量

下面程序先序列化一个Person对象,然后再反序列化该Person对象,得到反序列化的Person对象后程序输出该对象的age实例变量值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.*;

public class TransientTest {
public static void main(String[] args) {
try (
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("transient.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("transient.txt"))) {
Person per = new Person("孙悟空", 500);// 代码1
// 系统会per对象转换字节序列并输出
oos.writeObject(per);// 代码2
// 反序列化
Person p = (Person) ois.readObject();// 代码3
System.out.println(p.getAge());//代码4
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

上面程序中的
代码1创建了一个Person对象,并为它的nameage两个实例变量指定了值;
代码2将该Person对象序列化后输出;
代码3从序列化文件中读取该Person对象;
代码4输出该Person对象的age实例变量值。

由于本程序中的Person类的age实例变量使用transient关键字修饰,所以p.getAge()将得到0,而不是500

transient关键字的问题

使用transient关键字修饰实例变量虽然简单、方便,但transient修饰的实例变量将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java对象时无法取得该实例变量值

自定义序列化机制

Java还提供了一种自定义序列化机制,**通过这种自定义序列化机制可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量(与使用transient关键字的效果相同)**。

在类中添加特殊的方法

在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化。

1
2
3
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException
private void readObjectNoData()throws ObjectStreamException

writeObject方法

writeObject方法负责写入特定类的实例状态,以便相应的readObject方法可以恢复它。通过重写该writeObject方法,程序员可以完全获得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需要怎样序列化。在默认情况下,该方法会调用out.defaultObject来保存Java对象的各实例变量,从而可以实现序列化Java对象状态的目的

readObject方法

readObject方法负责从流中读取并恢复对象实例变量,通过重写该readObject方法,程序员可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些实例变量,以及如何进行反序列化。在默认情况下,该方法会调用in default Readobject来恢复Java对象的非瞬态实例变量。在通常情况下,readObject方法与writeObject方法对应,如果writeObject方法中对Java对象的实例变量进行了一些处理,则应该在readObject方法中对其实例变量进行相应的反处理,以便正确恢复该对象。

readObjectNoData方法

当序列化流不完整时,readObjectNoData方法可以用来正确地初始化反序列化的对象。例如,接收方使用的反序列化类的版本不同于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改时,系统都会调用readObjectNoData方法来初始化反序列化的对象。

程序 自定义序列化

下面的Person类提供了writeObjectreadObject两个方法,其中
writeObject方法在保存Person对象时将其name实例变量包装成StringBuffer,并将其字符序列反转后写入;
readObject方法中处理name的策略与此对应——先将读取的数据强制类型转换成StringBuffer,再将其反转后赋给nane实例变量

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
import java.io.*;
public class Person implements java.io.Serializable {
private static final long serialVersionUID = 3069227031912694124L;
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name, int age) {
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 此处省略getter和setter方法,请自己补上
// 序列化方法
private void writeObject(java.io.ObjectOutputStream out)
throws IOException {
// 将name实例变量的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
// 反序列化方法
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
// 将读取的字符串反转后赋给name实例变量
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
}

上面程序中提供了writeObjectreadObject用以实现自定义序列化,对于这个Person类而言,序列化、反序列化Person实例并没有任何区别——区别在于序列化后的对象流,即使有Cracker截获到Person对象流,他看到的name也是加密后的name值,这样就提高了序列化的安全性。
writeObject方法存储实例变量的顺序应该和readObject方法中恢复实例变量的顺序一致,否则将不能正常恢复该Java对象。
Person对象进行序列化和反序列化的程序与前面程序没有任何区别,故此处不再赘述。

更彻底的自定义序列化方式

writeReplace方法

还有一种更彻底的自定义机制,它甚至可以在序列化对象时将该对象替换成其他对象。如果需要实现序列化某个对象时替换该对象,则应为序列化类提供如下特殊方法。

1
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException

writeReplace方法将由序列化机制调用,只要该方法存在。因为该方法可以拥有私有(private)受保护的(protected)和包私有(package-private)等访问权限,所以其子类有可能获得该方法。

程序 使用writeReplace方法

例如下面的Person类提供了writeReplace方法,这样可以在写入Person对象时将该对象替换成ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;
import java.io.*;
public class Person implements java.io.Serializable {
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name, int age) {
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 此处省略getter和setter方法,请自己补上
// 重写writeReplace方法,程序在序列化该对象之前,先调用该方法
private Object writeReplace() throws ObjectStreamException {
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}

Java的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace方法,如果该方法返回另一个Java对象,则系统转为序列化另一个对象。如下程序表面上是序列化Person对象,但实际上序列化的是ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*;
import java.util.*;

public class ReplaceTest {
public static void main(String[] args) {
try (
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("replace.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("replace.txt"))) {
Person per = new Person("孙悟空", 500);
// 系统将per对象转换字节序列并输出
oos.writeObject(per);//代码一
// 反序列化读取得到的是ArrayList
ArrayList list = (ArrayList) ois.readObject();//代码二
System.out.println(list);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

上面程序中代码一使用writeObject写入了一个Person对象,但代码二使用readObject方法返回的实际上是一个ArayList对象,这是因为Person类的writeReplace方法返回了个ArrayList对象,所以序列化机制在序列化Person对象时,实际上是转为序列化ArrayList对象。

先调用writeReplace方法 再调用writeObject方法

根据上面的介绍,可以知道系统在序列化某个对象之前,会先调用该对象的writeReplacewriteObject两个方法,系统总是先调用被序列化对象的writeReplace方法,如果该方法返回另一个对象,系统将再次调用另一个对象的writeReplace方法…直到该方法不再返回另一个对象为止,程序最后将调用该对象的writeObject方法来保存该对象的状态

readResolve方法

writeReplace()方法相对的是,序列化机制里还有一个特殊的方法,它可以实现保护性复制整个对象。这个方法就是:

1
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

readResolve方法会紧接着readObject之后被调用,readResolve方法的返回值将会代替原来反序列化的对象,而原来readObject反序列化的对象将会被立即丢弃。

readResolve()方法在序列化单例类、枚举类时尤其有用
当然,如果使用Java5提供的enum来定义枚举类,则完全不用担心,程序没有任何问题。

早期枚举类问题

但如果应用中有早期遗留下来的枚举类,例如下面的Orientation类就是一个枚举类。

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
import java.io.*;

public class Orientation
implements java.io.Serializable
{
public static final Orientation HORIZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
private int value;
private Orientation(int value)
{
this.value = value;
}
// // 为枚举类增加readResolve()方法
// private Object readResolve()throws ObjectStreamException
// {
// if (value == 1)
// {
// return HORIZONTAL;
// }
// if (value == 2)
// {
// return VERTICAL;
// }
// return null;
// }
}

Java5以前,这种代码是很常见的。Orientation类的构造器私有,程序只有两个Orientation对象,分别通过OrientationHORIZONTALVERTICAL两个常量来引用。但如果让该类实现Serializable接口,则会引发一个问题,如果将一个Orientation. HORIZONTAL值序列化后再读出,如下代码片段所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;

public class ResolveTest {
public static void main(String[] args) {
try (
// 创建一个ObjectOutputStream输入流
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("transient.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("transient.txt"))) {
oos.writeObject(Orientation.HORIZONTAL);
Orientation ori = (Orientation) ois.readObject();
System.out.println(ori == Orientation.HORIZONTAL);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

**如果立即拿oriOrientation.HORIZONTAL值进行比较,将会发现返回false**。也就是说,ori是个新的Orientation对象,而不等于Orientation类中的任何枚举值——虽然Orientation的构造器是private的,但反序列化依然可以创建Orientation对象。

前面已经指出,反序列化机制在恢复Java对象时无须调用构造器来初始化Java对象从这个意义上来看,序列化机制可以用来“克隆”对象
在这种情况下,可以通过为Orientation类提供一个readResolve()方法来解决该问题,readResolve()方法的返回值将会代替原来反序列化的对象,也就是让反序列化得到的Orientation对象被直接丢弃。
下面是为Orientation类提供的readResolve()方法(程序清单同上)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 为枚举类增加readResolve()方法
private Object readResolve()throws ObjectStreamException
{
if (value == 1)
{
return HORIZONTAL;
}
if (value == 2)
{
return VERTICAL;
}
return null;
}

通过重写readResolve()方法可以保证反序列化得到的依然是OrientationHORIZONTALVERTICAL两个枚举值之一。

单例类 枚举类都应该提供readResolve方法

所有的单例类、枚举类在实现序列化时都应该提供readResolve()方法,这样才可以保证反序列化的对象依然正常

readResolve方法需要注意的问题

writeReplace()方法类似的是,readResolve()方法也可以使用任意的访问控制符,因此父类的readResolve()方法可能被其子类继承。这样利用readResolve()方法时就会存在一个明显的缺点,就是当父类已经实现了readResolve()方法后,子类将变得无从下手。如果父类包含一个protectedpublicreadResolve方法,而且子类也没有重写该方法,将会使得子类反序列化时得到一个父类的对象——这显然不是程序要的结果,而且也不容易发现这种错误。
总是让子类重写readResolve()方法无疑是一个负担,因此对于要被作为父类继承的类而言,实现readResolve(()方法可能有一些潜在的危险。
通常的建议是:

  • 对于final类,重写readResolve()方法不会有任何问题;
  • 否则,重写readResolve()方法时应尽量使用private修饰该方法。