当你创建对象时,只要你需要,它就会一直存在,但是在程序终止时,无论如何它都不会继续存在。如果对象能
够在程序不运行的情况下仍然存在并保存其信息,那将非常有用。这样,在下一次运行程序时,该对象将被重建
并且拥有的信息与在程序上次运行时它所拥有的信息相同。当然,可以通过将对象信息写入文件或是数据库来达
到相同的效果,但是在使万物都成为对象的精神中,如果能够将一个对象声明为是持久性的,并为我们处理掉所
有细节,那将会显得非常方便。
java的对象序列化将那些实现了Serializable接口的对象转换成为一个字节学列,并能够在以后将这个字节序列
完全恢复为原来的样子。这一过程甚至可以通过网络进行,可以在windows上创建对象并将其序列化,然后通过网
络传递给unix机子,然后在那里重新组装该对象。
对象的序列化处理非常简单,只需对象实现了Serializable接口即可(该接口仅是一个标记,没有方法)。在
Java1.1中,许多标准库类都发生了改变,以便能够序列化——其中包括用于基本数据类型的全部封装器、所有集
合类以及其他许多东西。甚至Class对象也可以序列化
为序列化一个对象,首先要创建某些OutputStream对象,然后将其封装到ObjectOutputStream对象内。此时,只
需调用 writeObject()即可完成对象的序列化,并将其发送给OutputStream。相反的过程是将一个InputStream封
装到 ObjectInputStream内,然后调用readObject()。和往常一样,我们最后获得的是指向一个上溯造型Object
的句柄,所以必须下溯造型,以便能够直接设置。
InputStream在read的时候,都是即时读取的,执行完马上就执行IO操作并返回数据。
而OutputStream为了提高效率都不是即时输出的,write会把数据先写到缓存里面,积累够了再一次性做IO输出,
所以你虽然write了,但数据还没传出去,flush方法就是强制对buffer里面的内容做IO输出。
java的io流相当于一个数据流通的管道,这里面存在一个默认的数据缓冲区,大小是8k。当我们在进行io操作的
时候,数据会经过这个缓冲区,然后保存到硬盘文件或其它存储设备文件中。如果保存过程中的数据少于8k之后,
那么这时,它就有可能把少于8k的这部分数据缓存在缓冲区中,而不写进存储设备里面,因此,我们调用当前io
流的flush()方法,清空当前缓冲区,即把数据完整的存储到文件中。
对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内包含的所有句柄并保
存那些对象;接着又能对每个对象内包含的句柄进行追踪;以此类推。我们有时将这种情况称为“对象网”,单
个对象可与之建立连接。而且它还包含了对象的句柄数组以及成员对象。若必须自行操纵一套对象序列化机制,
那么在代码里追踪所有这些链接时可能会显得非常麻烦。在另一方面,由于Java对象的序列化似乎找不出什么缺
点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。下面这个例子对序列化机制进行了测试
。它建立了许多链接对象的一个“Worm”(蠕虫),每个对象都与Worm中的下一段链接,同时又与属于不同类
(Data)的对象句柄数组链接:
import java.io.*;
import java.util.*;
class Data implements Serializable {
private int n;
public Data(int n) {
this.n = n;
}
public String toString() {
return Integer.toString(n);
}
}
public class Worm implements Serializable {
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;
// Value of i == number of segments
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 ClassNotFoundException,
IOException {
Worm w = new Worm(6, 'a');
System.out.println("w = " + w);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(
"worm.out"));
out.writeObject("Worm storage\n");
out.writeObject(w);
out.close(); // Also flushes output
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
"worm.out"));
String s = (String) in.readObject();
Worm w2 = (Worm) in.readObject();
System.out.println(s + "w2 = " + w2);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out2 = new ObjectOutputStream(bout);
out2.writeObject("Worm storage\n");
out2.writeObject(w);
out2.flush();
ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(
bout.toByteArray()));
s = (String) in2.readObject();
Worm w3 = (Worm) in2.readObject();
System.out.println(s + "w3 = " + w3);
}
}
更有趣的是,Worm内的Data对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信息)
。每个Worm段都用一个Char标记。这个 Char是在重复生成链接的Worm列表时自动产生的。创建一个Worm时,需告
诉构建器希望它有多长。为产生下一个句柄(next),它总是用减去1的长度来调用Worm构建器。最后一个next句
柄则保持为null(空),表示已抵达Worm的尾部。
上面的所有操作都是为了加深事情的复杂程度,加大对象序列化的难度。然而,真正的序列化过程却是非常
简单的。一旦从另外某个流里创建了 ObjectOutputStream,writeObject()就会序列化对象。注意也可以为一个
String调用writeObject()。亦可使用与DataOutputStream相同的方法写入所有基本数据类型(它们有相同的接口
)。
有两个单独的try块看起来是类似的。第一个读写的是文件,而另一个读写的是一个ByteArray(字节数组)
。可利用对任何DataInputStream或者DataOutputStream的序列化来读写特定的对象;正如在关于连网的那一章会
讲到的那样,这些对象甚至包括网络。
可以看出,装配回原状的对象确实包含了原来那个对象里包含的所有链接。注意在对一个Serializable(可
序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建器)。整个对象都是通过从
InputStream中取得数据恢复的。
该程序能打开文件,并成功读取mystery对象中的内容。然而,一旦尝试查找与对象有关的任何资料——这要求
Alien的Class对象——Java虚拟机(JVM)便找不到Alien.class(除非它正好在类路径内,而本例理应相反)。
这样就会得到一个名叫 ClassNotFoundException的违例(同样地,若非能够校验Alien存在的证据,否则它等于
消失)。
恢复了一个序列化的对象后,如果想对其做更多的事情,必须保证JVM能在本地类路径或者因特网的其他什么地方
找到相关的.class文件。
例如:
public class ThawAlien {
public static void main(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
new File("..", "X.file")));
Object mystery = in.readObject();
System.out.println(mystery.getClass());
}
}
正如大家看到的那样,默认的序列化机制并不难操纵。然而,假若有特殊要求又该怎么办呢?我们可能有特殊的
安全问题,不希望对象的某一部分序列化;或者某一个子对象完全不必序列化,因为对象恢复以后,那一部分需
要重新创建。
此时,通过实现Externalizable接口,用它代替Serializable接口,便可控制序列化的具体过程。这个
Externalizable接口扩展了Serializable,并增添了两个方法:writeExternal()和readExternal()。在序列化和
重新装配的过程中,会自动调用这两个方法,以便我们执行一些特殊操作。
下面这个例子展示了Externalizable接口方法的简单应用。注意Blip1和Blip2几乎完全一致,除了极微小的差别
(自己研究一下代码,看看是否能发现):
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;
class Blip1 implements Externalizable {
public Blip1() {
System.out.println("Blip1 Constructor");
}
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip1.writeExternal");
}
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
System.out.println("Blip1.readExternal");
}
}
class Blip2 implements Externalizable {
Blip2() {
System.out.println("Blip2 Constructor");
}
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip2.writeExternal");
}
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
System.out.println("Blip2.readExternal");
}
}
public class Blips {
public static void main(String[] args) throws IOException,
ClassNotFoundException {
System.out.println("Constructing objects:");
Blip1 b1 = new Blip1();
Blip2 b2 = new Blip2();
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
"Blips.out"));
System.out.println("Saving objects:");
o.writeObject(b1);
o.writeObject(b2);
o.close();
// Now get them back:
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
"Blips.out"));
System.out.println("Recovering b1:");
b1 = (Blip1) in.readObject();
// OOPS! Throws an exception:
// ! System.out.println("Recovering b2:");
// ! b2 = (Blip2)in.readObject();
}
}
未恢复Blip2对象的原因是那样做会导致一个违例。你找出了Blip1和Blip2之间的区别吗?Blip1的构建器是“公
共的” (public),Blip2的构建器则不然,这样便会在恢复时造成违例。试试将Blip2的构建器属性变成
“public”,然后删除//!注释标记,看看是否能得到正确的结果。
恢复b1后,会调用Blip1默认构建器。这与恢复一个Serializable(可序列化)对象不同。在后者的情况下,对象
完全以它保存下来的二进制位为基础恢复,不存在构建器调用。而对一个Externalizable对象,所有普通的默认
构建行为都会发生(包括在字段定义时的初始化),而且会调用readExternal()。必须注意这一事实——特别注
意所有默认的构建行为都会进行——否则很难在自己的 Externalizable对象中产生正确的行为。
下面这个例子揭示了保存和恢复一个Externalizable对象必须做的全部事情:
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 Blip3 implements Externalizable {
private int i;
private String s; // No initialization
public Blip3() {
System.out.println("Blip3 Constructor");
// s, i not initialized
}
public Blip3(String x, int a) {
System.out.println("Blip3(String x, int a)");
s = x;
i = a;
// s & i initialized only in non-default constructor.
}
public String toString() {
return s + i;
}
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip3.writeExternal");
// You must do this:
out.writeObject(s);
out.writeInt(i);
}
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
System.out.println("Blip3.readExternal");
// You must do this:
s = (String) in.readObject();
i = in.readInt();
}
public static void main(String[] args) throws IOException,
ClassNotFoundException {
System.out.println("Constructing objects:");
Blip3 b3 = new Blip3("A String ", 47);
System.out.println(b3);
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
"Blip3.out"));
System.out.println("Saving object:");
o.writeObject(b3);
o.close();
// Now get it back:
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
"Blip3.out"));
System.out.println("Recovering b3:");
b3 = (Blip3) in.readObject();
System.out.println(b3);
}
}
其中,字段s和i只在第二个构建器中初始化,不关默认构建器的事。这意味着假如不在readExternal中初始化s和
i,它们就会成为null(因为在对象创建的第一步中已将对象的存储空间清除为1)。若注释掉跟随于
“Youmustdothis”后面的两行代码,并运行程序,就会发现当对象恢复以后,s是null,而i是零。
若从一个Externalizable对象继承,通常需要调用writeExternal()和readExternal()的基础类版本,以便正确地
保存和恢复基础类组件。
所以为了让一切正常运作起来,千万不可仅在writeExternal()方法执行期间写入对象的重要数据(没有默认的行
为可用来为一个 Externalizable对象写入所有成员对象)的,而是必须在readExternal()方法中也恢复那些数据
。初次操作时可能会有些不习惯,因为Externalizable对象的默认构建行为使其看起来似乎正在进行某种存储与
恢复操作。但实情并非如此。
1.transient(临时)关键字
控制序列化过程时,可能有一个特定的子对象不愿让Java的序列化机制自动保存与恢复。一般地,若那个子对象
包含了不想序列化的敏感信息(如密码),就会面临这种情况。即使那种信息在对象中具有“private”(私有)
属性,但一旦经序列化处理,人们就可以通过读取一个文件,或者拦截网络传输得到它。
为防止对象的敏感部分被序列化,一个办法是将自己的类实现为Externalizable,就象前面展示的那样。这样一
来,没有任何东西可以自动序列化,只能在writeExternal()明确序列化那些需要的部分。
然而,若操作的是一个Serializable对象,所有序列化操作都会自动进行。为解决这个问题,可以用transient(
临时)逐个字段地关闭序列化,它的意思是“不要麻烦你(指自动机制)保存或恢复它了——我会自己处理的”
。
例如,假设一个Login对象包含了与一个特定的登录会话有关的信息。校验登录的合法性时,一般都想将数据保存
下来,但不包括密码。为做到这一点,最简单的办法是实现Serializable,并将password字段设为transie
nt。下面是具体的代码:
public class Logon implements Serializable {
private Date date = new Date();
private String username;
private transient String password;
public Logon(String name, String pwd) {
username = name;
password = pwd;
}
public String toString() {
return "logon info: \n username: " + username + "\n date: " + date
+ "\n password: " + password;
}
public static void main(String[] args) throws Exception {
Logon a = new Logon("Hulk", "myLittlePony");
System.out.println("logon a = " + a);
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
"Logon.out"));
o.writeObject(a);
o.close();
TimeUnit.SECONDS.sleep(1); // Delay
// Now get them back:
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
"Logon.out"));
System.out.println("Recovering object at " + new Date());
a = (Logon) in.readObject();
System.out.println("logon a = " + a);
}
}
可以看到,其中的date和username字段保持原始状态(未设成transient),所以会自动序列化。然而,password
被设为transient,所以不会自动保存到磁盘;另外,自动序列化机制也不会作恢复它的尝试。
由于Externalizable对象默认时不保存它的任何字段,所以transient关键字只能伴随Serializable使用。
Externalizable的替代方法
若不是特别在意要实现Externalizable接口,还有另一种方法可供选用。我们可以实现 Serializable接口,并添
加(注意是“添加”,而非“覆盖”或者“实现”)名为writeObject()和readObject()的方法。一旦对象被序列
化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑
默认的序列化机制。
这些方法必须含有下列准确的签名:
pirvate void writeObject(ObjectOutputStream stream)throws IOException;
pirvate void readObject(ObjectInputStream stream)throws IOException,ClassNotFoundException;
从设计的角度出发,情况变得有些扑朔迷离。首先,大家可能认为这些方法不属于基础类或者Serializable接口
的一部分,它们应该在自己的接口中得到定义。但请注意它们被定义成“private”,这意味着它们只能由这个类
的其他成员调用。然而,我们实际并不从这个类的其他成员中调用它们,而是由 ObjectOutputStream和
ObjectInputStream的writeObject()及readObject()方法来调用我们对象的writeObject()和readObject()方法(
注意我在这里用了很大的抑制力来避免使用相同的方法名——因为怕混淆)。大家可能奇怪 ObjectOutputStream
和ObjectInputStream如何有权访问我们的类的private方法——只能认为这是序列化机制玩的一个把戏。
在任何情况下,接口中的定义的任何东西都会自动具有public属性,所以假若writeObject()和readObject()必须
为private,那么它们不能成为接口(interface)的一部分。但由于我们准确地加上了签名,所以最终的效果实
际与实现一个接口是相同的。
看起来似乎我们调用ObjectOutputStream.writeObject()的时候,我们传递给它的Serializable对象似乎会被检
查是否实现了自己的writeObject()。若答案是肯定的是,便会跳过常规的序列化过程,并调用writeObject()。
readObject() 也会遇到同样的情况。
还存在另一个问题。在我们的writeObject()内部,可以调用defaultWriteObject(),从而决定采取默认的
writeObject()行动。类似地,在readObject()内部,可以调用defaultReadObject()。下面这个简单的例子演示
了如何对一个Serializable对象的存储与恢复进行控制:
public class SerialCtl implements Serializable {
private String a;
private transient String b;
public SerialCtl(String aa, String bb) {
a = "Not Transient: " + aa;
b = "Transient: " + bb;
}
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 IOException, ClassNotFoundException {
stream.defaultReadObject();
b = (String)stream.readObject();
}
public static void main(String[] args)
throws IOException, ClassNotFoundException {
SerialCtl sc = new SerialCtl("Test1", "Test2");
System.out.println("Before:\n" + sc);
ByteArrayOutputStream buf= new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(buf);
o.writeObject(sc);
// Now get it back:
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(buf.toByteArray()));
SerialCtl sc2 = (SerialCtl)in.readObject();
System.out.println("After:\n" + sc2);
}
}
在这个例子中,一个String保持原始状态,其他设为transient(临时),以便证明非临时字段会被
defaultWriteObject()方法自动保存,而transient字段必须在程序中明确保存和恢复。字段是在构建器内部初始
化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化。
若准备通过默认机制写入对象的非transient部分,那么必须调用defaultWriteObject(),令其作为writeObject
()中的第一个操作;并调用defaultReadObject(),令其作为readObject()的第一个操作。这些都是不常见的调用
方法。举个例子来说,当我们为一个ObjectOutputStream调用defaultWriteObject()的时候,而且没有为其传递
参数,就需要采取这种操作,使其知道对象的句柄以及如何写入所有非transient的部分。这种做法非常不便。
transient对象的存储与恢复采用了我们更熟悉的代码。现在考虑一下会发生一些什么事情。在main()中会创建一
个SerialCtl对象,随后会序列化到一个ObjectOutputStream里(注意这种情况下使用的是一个缓冲区,而非文件
——与ObjectOutputStream完全一致)。正式的序列化操作是在下面这行代码里发生的:
o.writeObject(sc);
其中,writeObject()方法必须核查sc,判断它是否有自己的writeObject()方法(不是检查它的接口——它根本
就没有,也不是检查类的类型,而是利用反射方法实际搜索方法)。若答案是肯定的,就使用那个方法。类似的
情况也会在readObject()上发生。或许这是解决问题唯一实际的方法,但确实显得有些古怪。