Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可以通过网络进行,这意味着序列化机制能自动补弥不同操作系统之间的差异。“持久化”意味着一个对象的生存周期并不取决于程序是否正在执行,它可以生存于程序的调用之间。
对象序列化的概念加入到语言中是为了支持两种主要特性。一是Java的远程方法调用(Remote Method Invocation,RMI),它使存活于其他计算机的对象使用起来就像是存活于本机上一样。二是对JavaBean对象序列化的支持。只要对象实现了Serializable接口(该接口仅是一个标记接口,不包含任何方法),就可以进行对象序列化,当前的标准库中,包括基本数据类型的封装器,所有容器类以及许多其他的东西,甚至Class对象也可以被序列化。
要序列化一个对象,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内。这时,只需调用writeObject()即可调用对象序列化,并将其发送给OutputStream(基于字节流)。要执行反序列化只需执行相反的过程,将一个InputStream封装在ObjectInputStream内,调用readObject。
对象序列化不仅仅保存对象的“全景图”,还能跟踪对象内包含的所有引用,并保存那些对象;接着又能对对象内包含的每个这样的引用进行跟踪;依次类推。这种情况称为对象网,单个对象可与之建立连接,而且它还包含了对象的引用数组以及成员对象。
public class Worm implements Serializable{
private static final long serialVersionUID = -1;
private static class Data implements Serializable {
private static final long serialVersionUID = 1L;
private int n;
public Data(int n) {
this.n = n;
}
public String toString() {
return Integer.toString(n);
}
}
private static Random rand = new Random(47);
private Data[] d = { new Data(rand.nextInt(10)),
new Data(rand.nextInt(10)), new Data(rand.nextInt(10)) };
private Worm next;
private char c;
public Worm(int i, char x) {
System.out.println("Worm constructor: " + i);
c = x;
if (--i > 0) {
next = new Worm(i, (char) (x + 1));
}
}
public Worm() {
System.out.println("Default constructor");
}
public String toString() {
StringBuilder result = new StringBuilder(":");
result.append(c);
result.append("(");
for (Data dat : d) {
result.append(dat);
}
result.append(")");
if (next != null) {
result.append(next);
}
return result.toString();
}
public static void main(String[] args) throws Exception {
String filename = "worm.out";
Worm w = new Worm(6, 'a');
System.out.println("w = " + w);
ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(filename)));
out.writeObject("Worm storage\n");
out.writeObject(w);
out.close();
ObjectInputStream in = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(filename)));
String s = (String) in.readObject();
Worm w2 = (Worm) in.readObject();
System.out.println(s + "w2 = " + w2);
in.close();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out2 = new ObjectOutputStream(bout);
out2.writeObject("Worm storage\n");
out2.writeObject(w);
out2.flush();
ObjectInputStream in2 = new ObjectInputStream(
new BufferedInputStream(new ByteArrayInputStream(bout.toByteArray())));
s = (String) in2.readObject();
Worm w3 = (Worm) in2.readObject();
System.out.println(s + "w3 = " + w3);
}
}
对对象进行反序列化时,不仅要提供保存对象时的数据还要提供对象的.class文件,以便Java虚拟机能够找到它。
序列化的控制(Externalizable)
如果不希望某个对象的一部分被序列化,或者对象被还原后,某子对象需要重新创建,从而不必将该子对象序列化时可以使用Externalizable接口替代Serializable。Externalizable包含两个方法writeExternal和readExternal方法。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊操作。
public class Blip implements Externalizable {
private int i;
private String s;
public Blip() {
System.out.println("Blip Constructor");
}
public Blip(String x, int a) {
System.out.println("Blip(String x, int a)");
s = x;
i = a;
}
public String toString() {
return s + i;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip.writeExternal");
// you must do this
out.writeObject(s);
out.writeInt(i);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
System.out.println("Blip.readExternal");
// you must do this
s = (String) in.readObject();
i = in.readInt();
}
public static void main(String[] args) throws Exception {
String filename = "blip.out";
System.out.println("Constructing objects.");
Blip b = new Blip("A String ", 47);
System.out.println(b);
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
filename));
System.out.println("Saving object:");
o.writeObject(b);
o.close();
// now get it back
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
filename));
System.out.println("Recovering b:");
b = (Blip) in.readObject();
System.out.println(b);
in.close();
}
}
如果注释掉“you must do this”后的代码得到的对象就是未被初始化的对象,即s是null,i是0。因为对于Externalizable对象来说,所有普通的默认构造器都会被调用(包括字段定义时的初始化),然后调用readExternal()方法。而对于Serializable对象,它完全以存储的二进制位为基础来构造,而不调用构造器。以上代码中,s 和 i 在默认构造方法中未初始化,故得到的是类初始化时的值。
序列化的控制(transient(瞬时)关键字)
在进行序列化控制时,可能某个特定子对象不想让Java的序列化机制自动保存与恢复,第一种办法是将类实现为Externalizable,另一种办法是,假如我们使用的是Serializable对象,那么所有的序列化都是自动进行的,为了能够控制,可以使用transient关键字关闭特定字段的序列化。
public class Logon implements Serializable {
private static final long serialVersionUID = 1L;
private Date date = new Date();
private String username;
private transient String password;
public Logon(String username, String password) {
this.username = username;
this.password = password;
}
public String toString() {
return "object\n usename: " + username + "\n password: " + password
+ "\n date" + date;
}
public static void main(String[] args) throws Exception {
Logon a = new Logon("Hulk", "mypassword");
System.out.println(a);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(out);
o.writeObject(a);
TimeUnit.SECONDS.sleep(1);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(
out.toByteArray()));
System.out.println("Recovering object at: " + new Date());
Logon b = (Logon) in.readObject();
System.out.println(b);
}
}
以上使用了transient对password字段进行了保护,使得它不会自动被序列化存储,因为在反序列化时它的值为null。
序列化的控制(Externalizable的替代方法——readObject/writeObject)
如果不是特别坚持实现Externalizable接口,还可以通过实现Serializable接口,并添加(不是覆盖或者实现)名为writeObject和readObject的方法。
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException
当使用ObjectOutputStream和ObjectInputStream的writeObject和readObject方法时,它会自动调用你的对象的writeObject和readObject方法。但是它们不是Serializable接口的一部分,Serializable接口只是序列化类的标记。
具体的过程是这样的,在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看是否实现了自己的writeObject()。如果是这样,就跳过正常的序列化过程,并调用它的writeObject()。readObject()的情况类似。在readObject/writeObject方法中还可以调用defaultReadObject/defaultWriteObject方法实现默认的序列化操作。
public class SerialCtl implements Serializable {
private static final long serialVersionUID = 1L;
private String a;
private transient String b;
public SerialCtl(String a, String b) {
this.a = "Not Transient: " + a;
this.b = "Not Transient: " + b;
}
public String toString() {
return a + "\n" + b;
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
stream.writeObject(b);
}
private void readObject(ObjectInputStream stream)
throws ClassNotFoundException, IOException {
stream.defaultReadObject();
b = (String) stream.readObject();
}
public static void main(String[] args) throws Exception {
SerialCtl sc = new SerialCtl("Test1", "Test2");
System.out.println("Before:\n" + sc);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(buf);
o.writeObject(sc);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(
buf.toByteArray()));
SerialCtl sc2 = (SerialCtl) in.readObject();
System.out.println("After:\n" + sc2);
}
}
这里的b字段是transient类型的,defaultWriteObject不会保存它,于是在这里使用了writeObject方法保存了它。
只要将任何对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,并且没有任何意外重复复制的对象,无论对象处于在被序列化的状态如何,它们都能够被写出。
另外,static的数据是不会被序列化的,故想要序列化static值时,必须自己动手实现。而且private的字段也能够被序列化保存,所以需要考虑好如何设计private变量的值的安全恢复过程。
serialVersionUID的意义
最后需要注意到序列化时定义了一个字段:
private static final long serialVersionUID = 1L;
它是序列化的版本号,如果没有设置这个值,在序列化一个对象之后,然后改动这个类的字段、方法名等等,那么在反序列化时取出之前存储的对象可能会抛出异常,因为serialVersionUID是根据类名,接口名,成员方法名、属性生成的hash值(可能还与编译器有关),修改了这个类,然后反序列化时serialVersionUID就变得和之前存储的结构不一致了,故会抛出异常。如果显式设置了serialVersionUID值,当类变动时,它反序列化时,若出现新增值则会设置为null,删除的值就不会显示。
Java API中是这样描述的:
序列化运行时使用一个称为serialVersionUID的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的serialVersionUID与对应的发送者的类的版本号不同,则反序列化将会导致InvalidClassException。可序列化类可以通过声明名为 "serialVersionUID"的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)显式声明其自己的 serialVersionUID:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
如果可序列化类未显式声明serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认serialVersionUID值,如“Java(TM) 对象序列化规范”中所述。不过,强烈建议所有可序列化类都显式声明serialVersionUID值,原因是计算默认的serialVersionUID对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的InvalidClassException。因此,为保证 serialVersionUID 值跨不同java编译器实现的一致性,序列化类必须声明一个明确的serialVersionUID值。还强烈建议使用 private修饰符显示声明serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类——serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配serialVersionUID值的要求。
参考资料:《Java编程思想》