10.6.2 自定义对象序列化
默认的对象序列化机制虽然使用简单,但是存在的问题也比较多。其中最重要的问题是默认的序列化机制依赖于Java类的内部实现,而不是公开接口。随着程序的版本更新,公开接口基本上不会发生变化,而内部的实现可能发生很多变化。内部实现的变化会导致旧版本的Java对象序列化之后的字节流无法被重新转换成新版本的Java对象。这与通常的“面向接口编程”的实践方式是相背离的。
比如,代码清单10-14中的User类需要增加获取用户年龄的功能,从公开API的角度来说,需要增加一个getAge方法获取用户的年龄。早期版本的实现是用一个int类型的域age来直接保存用户的年龄数据,在构造方法中设置该域的值。在后来的版本更新中,发现直接保存年龄的做法并不合适,改为根据出生日期自动计算出年龄。于是把原来实现中的age域删除,增加一个java.util.Date类型的域birthDate,并修改getAge方法的内部实现。在这个版本更新过程中,类的公开接口并没有变化,但内部实现发生了改变。如果某个文件中保存的是旧版本的对象序列化之后的字节流,那么在通过ObjectInputStream类的readObject方法读取之后得到的新版本的对象中,域birthDate的值是null。这是因为旧版本的序列化结果中只有age域的值,域birthDate的值被设为默认值null。默认的序列化机制并不理解age域和birthDate域之间的关系,只会根据域的名称和类型来进行赋值。
为了解决版本更新带来的序列化格式不兼容的问题,需要为Java类定义自己的序列化格式,而不是简单地使用默认格式。这就要求对Java类的职责及可能会发生变化的地方进行缜密的思考与设计,定义好序列化后的格式中要包含的数据。这个格式在之后的版本更新中应该是相对稳定的。完成设计之后,通过序列化机制提供的扩展方式来编写相关的逻辑。
通过在Java类中添加特定的方法来编写自定义的序列化实现逻辑。自定义的序列化逻辑由两个配对的方法来实现,分别是writeObject和readObject方法。当使用ObjectOutputStream类的对象进行序列化时,如果Java类中定义了writeObject方法,那么会调用该方法来完成实际的写入操作;当使用ObjectInputStream类的对象进行反序列化时,如果Java类中定义了readObject方法,那么会调用该方法进行实际的读取操作。这两个方法一般同时出现,但是所包含的逻辑正好相反。对writeObject和readObject方法的类型声明有严格的要求。不满足要求的方法不会在序列化时被调用。
以之前提到的在User类中添加表示年龄的域的需求为例来说明自定义序列化格式的用法。经过设计,在序列化后的格式中,只需要包含年龄的值即可。代码清单10-18给出了新的NewUser类的代码。NewUser类中的writeObject方法中包含的是序列化时的逻辑。在writeObject方法被调用时,当前进行对象写入操作的ObjectOutputStream类的对象会作为参数传入。使用该ObjectOutputStream类的对象中的方法可以写入任何所需的内容。在writeObject方法中一般先使用defaultWriteObject方法来执行默认的写入操作,即写入非静态和非瞬时的域。这样可以提高代码的灵活性。接着把age域的值通过writeInt方法写入流中。与writeObject方法相对应的readObject方法中包含的是反序列化时的逻辑。在readObject方法被调用时,当前进行读取操作的ObjectInputStream类的对象会作为参数传入。使用该ObjectInputStream类的对象来读取流中的内容,并初始化对象中的对应实例域。在readObject方法中一般先使用defaultReadObject方法来读取非静态和非瞬时域。在writeObject方法和readObject方法中的操作是相对应的,按照写入时的顺序来进行读取。
代码清单10-18 自定义序列化机制
public class NewUser implements Serializable{
private static final long serialVersionUID=1L;
private transient int age;
public NewUser(int age){
this.age=age;
}
public int getAge(){
return this.age;
}
private void writeObject(ObjectOutputStream output)throws IOException{
output.defaultWriteObject();
output.writeInt(getAge());
}
private void readObject(ObjectInputStream input)throws IOException, ClassNotFoundException{
input.defaultReadObject();
int age=input.readInt();
this.age=age;
}
}
代码清单10-18中的writeObject方法和readObject方法的实现都比较简单。实际上,可以在writeObject方法和readObject方法中添加比较复杂的逻辑,包括修改域的值或添加额外的数据等。如果在自定义的writeObject方法和readObject方法中对某个域的数据进行了处理,一般把该域声明为transient,这样defaultWriteObject方法和defaultReadObject方法就不会处理这个域,避免默认实现带来的不兼容性问题。
使用自定义序列化格式可以解决之前提到的由于版本更新造成的序列化内容不兼容的问题。代码清单10-19给出了新版本的NewUser类。新版本的NewUser类使用了Date类型表示出生日期。为了保持序列化格式的兼容性,在序列化之前需要把birthDate域转换成年龄,在反序列化之后要进行相反的操作。
代码清单10-19 版本更新后的序列化机制的实现
public class NewUser implements Serializable{
private static final long serialVersionUID=1L;
private transient Date birthDate;
public NewUser(Date birthDate){
this.birthDate=birthDate;
}
public int getAge(){
return dateToAge(birthDate);
}
private Date ageToDate(int age){
//年龄转换成日期
}
private int dateToAge(Date date){
//日期转换成年龄
}
private void writeObject(ObjectOutputStream output)throws IOException{
output.defaultWriteObject();
int age=dateToAge(birthDate);
output.writeInt(age);
}
private void readObject(ObjectInputStream input)throws IOException,
ClassNotFoundException{
input.defaultReadObject();
int age=input.readInt();
this.birthDate=ageToDate(age);
}
}
通过ObjectInputStream类的readObject方法可以从字节流中得到一个Java对象。不过对象的创建并不是通过一般的方式来完成的,对应的Java类的构造方法没有被调用,而不少Java类在其构造方法中有相关的对象初始化的逻辑。通过反序列化得到的对象由于没有调用构造方法,其内部的完整性约束可能被破坏,因此在readObject方法中需要确保对象在返回之前经过了完整的初始化。