38.JAVA编程思想——JAVA IO 对象序列化
Java 1.1 增添了一种有趣的特性,名为“对象序列化”(Object Serialization)。它面向那些实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。换句话说,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台 Unix 机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。
就其本身来说,对象的序列化是非常有趣的,因为利用它可以实现“有限持久化”。请记住“持久化”意味着对象的“生存时间”并不取决于程序是否正在执行——它存在或“生存”于程序的每一次调用之间。通过序列化一个对象,将其写入磁盘,以后在程序重新调用时重新恢复那个对象,就能圆满实现一种“持久”效果。之所以称其为“有限”,是因为不能用某种“persistent”(持久)关键字简单地地定义一个对象,并让系统自动照看其他所有细节问题(尽管将来可能成为现实)。相反,必须在自己的程序中明确地序列化和组装对象。
语言里增加了对象序列化的概念后,可提供对两种主要特性的支持。Java 1.1 的“远程方法调用”(RMI)使本来存在于其他机器的对象可以表现出好象就在本地机器上的行为。将消息发给远程对象时,需要通过对象序列化来传输参数和返回值。
对象的序列化也是JavaBeans 必需的,后者由Java 1.1引入。使用一个Bean 时,它的状态信息通常在设计期间配置好。程序启动以后,这种状态信息必须保存下来,以便程序启动以后恢复;具体工作由对象序列化完成。
对象的序列化处理非常简单,只需对象实现了 Serializable接口即可(该接口仅是一个标记,没有方法)。在Java 1.1 中,许多标准库类都发生了改变,以便能够序列化——其中包括用于基本数据类型的全部封装器、所有集合类以及其他许多东西。甚至Class 对象也可以序列化(第 11章讲述了具体实现过程)。
为序列化一个对象,首先要创建某些OutputStream对象,然后将其封装到 ObjectOutputStream 对象内。此时,只需调用writeObject()即可完成对象的序列化,并将其发送给OutputStream。相反的过程是将一个InputStream封装到 ObjectInputStream内,然后调用 readObject()。和往常一样,我们最后获得的是指向一个上溯造型Object 的句柄,所以必须下溯造型,以便能够直接设置。
对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内包含的所有句柄并保存那些对象;接着又能对每个对象内包含的句柄进行追踪;以此类推。我们有时将这种情况称为“对象网”,单个对象可与之建立连接。而且它还包含了对象的句柄数组以及成员对象。若必须自行操纵一套对象序列化机制,那么在代码里追踪所有这些链接时可能会显得非常麻烦。在另一方面,由于Java 对象的序列化似乎找不出什么缺点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。下面这个例子对序列化机制进行了测试。它建立了许多链接对象的一个“Worm”(蠕虫),每个对象都与Worm 中的下一段链接,同时又与属于不同类(Data)的对象句柄数组链接:
import java.io.*;
class Data implements Serializable {
private int i;
Data(int x) {
i = x;
}
public String toString() {
return Integer.toString(i);
}
}
public classWorm implements Serializable {
// Generate a random int value:
private static int r() {
return (int) (Math.random()* 10);
}
private Data[] d = { new Data(r()), new Data(r()), new Data(r()) };
private Worm next;
private char c;
// Value of i == number of segments
Worm(int i, char x) {
System.out.println(" Worm constructor: "+ i);
c = x;
if (--i > 0)
next = new Worm(i, (char) (x + 1));
}
Worm(){
System.out.println("Default constructor");
}
public String toString() {
Strings = ":" + c + "(";
for (int i = 0; i < d.length; i++)
s += d[i].toString();
s += ")";
if (next != null)
s += next.toString();
return s;
}
public static void main(String[] args) {
Wormw = new Worm(6, 'a');
System.out.println("w = " + w);
try {
ObjectOutputStreamout= newObjectOutputStream(new FileOutputStream("worm.out"));
out.writeObject("Worm storage");
out.writeObject(w);
out.close(); // Also flushes output
ObjectInputStreamin= newObjectInputStream(new FileInputStream("worm.out"));
Strings = (String) in.readObject();
Wormw2 = (Worm) in.readObject();
System.out.println(s + ", w2 = "+ w2);
}catch(Exception e) {
e.printStackTrace();
}
try {
ByteArrayOutputStreambout= newByteArrayOutputStream();
ObjectOutputStreamout= newObjectOutputStream(bout);
out.writeObject("Worm storage");
out.writeObject(w);
out.flush();
ObjectInputStreamin = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
Strings = (String) in.readObject();
Wormw3 = (Worm) in.readObject();
System.out.println(s + ", w3 = "+ w3);
}catch(Exception e) {
e.printStackTrace();
}
}
} /// :~
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w =:a(192):b(949):c(591):d(938):e(026):f(116)
Worm storage, w2 = :a(192):b(949):c(591):d(938):e(026):f(116)
Worm storage, w3 =:a(192):b(949):c(591):d(938):e(026):f(116)
Worm 内的Data 对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信息)。每个 Worm 段都用一个 Char 标记。这个Char 是在重复生成链接的 Worm 列表时自动产生的。创建一个Worm 时,需告诉构建器希望它有多长。为产生下一个句柄(next),它总是用减去 1 的长度来调用Worm 构建器。最后一个next 句柄则保持为null(空),表示已抵达 Worm 的尾部。
上面的所有操作都是为了加深事情的复杂程度,加大对象序列化的难度。然而,真正的序列化过程却是非常简单的。一旦从另外某个流里创建了ObjectOutputStream,writeObject()就会序列化对象。注意也可以为一个String调用 writeObject()。亦可使用与DataOutputStream 相同的方法写入所有基本数据类型(它们有相同的接口)。
有两个单独的try块看起来是类似的。第一个读写的是文件,而另一个读写的是一个 ByteArray(字节数组)。可利用对任何DataInputStream 或者DataOutputStream的序列化来读写特定的对象;正如在关于连网的那一章会讲到的那样,这些对象甚至包括网络。
装配回原状的对象确实包含了原来那个对象里包含的所有链接。
注意在对一个Serializable(可序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建器)。整个对象都是通过从InputStream中取得数据恢复的。
作为Java 1.1特性的一种,我们注意到对象的序列化并不属于新的 Reader和 Writer层次结构的一部分,而是沿用老式的InputStream 和OutputStream 结构。所以在一些特殊的场合下,不得不混合使用两种类型的层次结构。
或许会奇怪为什么需要一个对象从它的序列化状态中恢复。举个例子来说,假定我们序列化一个对象,并通过网络将其作为文件传送给另一台机器。此时,位于另一台机器的程序可以只用文件目录来重新构造这个对象吗?
回答这个问题的最好方法就是做一个实验。
创建Alien.java文件如下:
import java.io.*;
public classAlien implements Serializable {
} ///:~
创建FreezeAlien.java如下:
import java.io.*;
public classFreezeAlien {
public static void main(String[] args) throws Exception {
ObjectOutputout= newObjectOutputStream(new FileOutputStream("file.x"));
Alienzorcon= newAlien();
out.writeObject(zorcon);
}
} /// :~
该程序并不是捕获和控制违例,而是将违例简单、直接地传递到main()外部,这样便能在命令行报告它们。
程序编译并运行后,将结果产生的 file.x复制到名为 xfiles 的子目录,创建ThawAlien.java文件如下:
import java.io.*;
public classThawAlien {
public static void main(String[] args) throws Exception {
ObjectInputStreamin= newObjectInputStream(new FileInputStream("file.x"));
Objectmystery= in.readObject();
System.out.println(mystery.getClass().toString());
}
} /// :~
该程序能打开文件,并成功读取mystery对象中的内容。然而,一旦尝试查找与对象有关的任何资料——这要求Alien 的Class 对象——Java 虚拟机(JVM)便找不到 Alien.class(除非它正好在类路径内,而本例理应相反)。这样就会得到一个名叫 ClassNotFoundException 的违例(同样地,若非能够校验Alien 存在的证据,否则它等于消失)。
恢复了一个序列化的对象后,如果想对其做更多的事情,必须保证JVM能在本地类路径或者因特网的其他什么地方找到相关的.class文件。
默认的序列化机制并不难操纵。然而,假若有特殊要求又该怎么办呢?我们可能有特殊的安全问题,不希望对象的某一部分序列化;或者某一个子对象完全不必序列化,因为对象恢复以后,那一部分需要重新创建。
此时,通过实现Externalizable 接口,用它代替Serializable接口,便可控制序列化的具体过程。这个Externalizable 接口扩展了Serializable,并增添了两个方法:writeExternal()和readExternal()。在序列化和重新装配的过程中,会自动调用这两个方法,以便我们执行一些特殊操作。
下面展示了Externalizable 接口方法的简单应用。注意Blip1 和Blip2 几乎完全一致,除了极微小的差别:
import java.io.*;
import java.util.*;
class Blip1 implements Externalizable {
public Blip1() {
System.out.println("Blip1 Constructor");
}
public voidwriteExternal(ObjectOutput out) throwsIOException {
System.out.println("Blip1.writeExternal");
}
public voidreadExternal(ObjectInput in) throwsIOException, ClassNotFoundException {
System.out.println("Blip1.readExternal");
}
}
class Blip2 implements Externalizable {
Blip2(){
System.out.println("Blip2 Constructor");
}
public voidwriteExternal(ObjectOutput out) throwsIOException {
System.out.println("Blip2.writeExternal");
}
public voidreadExternal(ObjectInput in) throwsIOException, ClassNotFoundException {
System.out.println("Blip2.readExternal");
}
}
public classBlips {
public static void main(String[] args) {
System.out.println("Constructing objects:");
Blip1b1 = new Blip1();
Blip2b2 = new Blip2();
try {
ObjectOutputStreamo = new ObjectOutputStream(new FileOutputStream("Blips.out"));
System.out.println("Saving objects:");
o.writeObject(b1);
o.writeObject(b2);
o.close();
// Now get them back:
ObjectInputStreamin= newObjectInputStream(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();
}catch(Exception e) {
e.printStackTrace();
}
}
} /// :~
Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Constructor
Blip1.readExternal
未恢复Blip2 对象的原因是那样做会导致一个违例。你找出了 Blip1和 Blip2 之间的区别吗?Blip1 的构建器是“公共的”(public),Blip2 的构建器则不然,这样便会在恢复时造成违例。试试将 Blip2 的构建器属性变成“public”,然后删除//!注释标记,看看是否能得到正确的结果。
恢复b1后,会调用 Blip1 默认构建器。这与恢复一个Serializable(可序列化)对象不同。在后者的情况下,对象完全以它保存下来的二进制位为基础恢复,不存在构建器调用。而对一个Externalizable 对象,所有普通的默认构建行为都会发生(包括在字段定义时的初始化),而且会调用readExternal()。必须注意这一事实——特别注意所有默认的构建行为都会进行——否则很难在自己的 Externalizable 对象中产生正确的行为。
下面例子揭示了保存和恢复一个Externalizable 对象必须做的全部事情:
import java.io.*;
import java.util.*;
class Blip3 implements Externalizable {
int i;
Strings; // 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 voidwriteExternal(ObjectOutput out) throwsIOException {
System.out.println("Blip3.writeExternal");
// You must do this:
out.writeObject(s);
out.writeInt(i);
}
public voidreadExternal(ObjectInput in) throwsIOException, ClassNotFoundException {
System.out.println("Blip3.readExternal");
// You must do this:
s = (String) in.readObject();
i = in.readInt();
}
public static void main(String[] args) {
System.out.println("Constructing objects:");
Blip3b3 = new Blip3("A String ",47);
System.out.println(b3.toString());
try {
ObjectOutputStreamo = 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.toString());
}catch(Exception e) {
e.printStackTrace();
}
}
} /// :~
Constructing objects:
Blip3(String x, int a)
A String 47
Saving object:
Blip3.writeExternal
Recovering b3:
Blip3 Constructor
Blip3.readExternal
A String 47
其中,字段 s和 i 只在第二个构建器中初始化,不关默认构建器的事。这意味着假如不在readExternal中初始化s 和i,它们就会成为null(因为在对象创建的第一步中已将对象的存储空间清除为 1)。若注释掉跟随于“You must do this”后面的两行代码,并运行程序,就会发现当对象恢复以后,s 是null,而i 是零。
若从一个Externalizable 对象继承,通常需要调用writeExternal()和 readExternal()的基础类版本,以便正确地保存和恢复基础类组件。
所以为了让一切正常运作起来,千万不可仅在 writeExternal()方法执行期间写入对象的重要数据(没有默认的行为可用来为一个 Externalizable对象写入所有成员对象)的,而是必须在 readExternal()方法中也恢复那些数据。初次操作时可能会有些不习惯,因为Externalizable 对象的默认构建行为使其看起来似乎正在进行某种存储与恢复操作。但实情并非如此。
1. transient(临时)关键字控制序列化过程时,可能有一个特定的子对象不愿让Java 的序列化机制自动保存与恢复。一般地,若那个子对象包含了不想序列化的敏感信息(如密码),就会面临这种情况。即使那种信息在对象中具有“private”(私有)属性,但一旦经序列化处理,人们就可以通过读取一个文件,或者拦截网络传输得到它。
为防止对象的敏感部分被序列化,一个办法是将自己的类实现为Externalizable,就象前面展示的那样。这样一来,没有任何东西可以自动序列化,只能在writeExternal()明确序列化那些需要的部分。
然而,若操作的是一个 Serializable对象,所有序列化操作都会自动进行。为解决这个问题,可以用transient(临时)逐个字段地关闭序列化,它的意思是“不要麻烦你(指自动机制)保存或恢复它了——我会自己处理的”。
例如,假设一个Login 对象包含了与一个特定的登录会话有关的信息。校验登录的合法性时,一般都想将数据保存下来,但不包括密码。为做到这一点,最简单的办法是实现Serializable,并将 password 字段设为transient。下面是具体的代码:
import java.io.*;
import java.util.*;
class Logon implements Serializable {
private Date date = new Date();
private String username;
private transient String password;
Logon(Stringname,String pwd){
username = name;
password = pwd;
}
public String toString() {
Stringpwd= (password== null)? "(n/a)": password;
return "logon info: \n "+ "username: " + username+ "\n date: " + date.toString()+ "\n password: " + pwd;
}
public static void main(String[] args) {
Logona = new Logon("Hulk", "myLittlePony");
System.out.println("logon a = "+ a);
try {
ObjectOutputStreamo = new ObjectOutputStream(new FileOutputStream("Logon.out"));
o.writeObject(a);
o.close();
// Delay:
int seconds = 5;
long t = System.currentTimeMillis()+ seconds* 1000;
while (System.currentTimeMillis()< t)
;
// Now get them back:
ObjectInputStreamin= newObjectInputStream(new FileInputStream("Logon.out"));
System.out.println("Recovering object at "+ newDate());
a = (Logon) in.readObject();
System.out.println("logon a = "+ a);
}catch(Exception e) {
e.printStackTrace();
}
}
} /// :~
logon a = logon info:
username: Hulk
date: Sun Apr 17 22:43:58 CST 2016
password: myLittlePony
可以看到,其中的date 和 username 字段保持原始状态(未设成transient),所以会自动序列化。然而,password 被设为 transient,所以不会自动保存到磁盘;另外,自动序列化机制也不会作恢复它的尝试。一旦对象恢复成原来的样子,password 字段就会变成null。注意必须用 toString()检查password 是否为null,因为若用过载的“+”运算符来装配一个String 对象,而且那个运算符遇到一个null 句柄,就会造成一个名为NullPointerException 的违例(新版Java可能会提供避免这个问题的代码)。
我们也发现 date 字段被保存到磁盘,并从磁盘恢复,没有重新生成。由于Externalizable 对象默认时不保存它的任何字段,所以transient 关键字只能伴随Serializable使用。
Externalizable的替代方法若不是特别在意要实现 Externalizable接口,还有另一种方法可供选用。我们可以实现 Serializable接口,并添加(注意是“添加”,而非“覆盖”或者“实现”)名为writeObject()和 readObject()的方法。
一旦对象被序列化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑默认的序列化机制。
这些方法必须含有下列准确的签名:
private void
writeObject(ObjectOutputStream stream)
throws IOException;
private 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 对象的存储与恢复进行控制:
import java.io.*;
public classSerialCtl implements Serializable {
Stringa;
transient String b;
public SerialCtl(String aa, String bb) {
a = "Not Transient: "+ aa;
b = "Transient: "+ bb;
}
public String toString() {
return a + "\n" + b;
}
private voidwriteObject(ObjectOutputStream stream) throwsIOException {
stream.defaultWriteObject();
stream.writeObject(b);
}
private voidreadObject(ObjectInputStream stream) throwsIOException, ClassNotFoundException {
stream.defaultReadObject();
b = (String) stream.readObject();
}
public static void main(String[] args) {
SerialCtlsc = new SerialCtl("Test1","Test2");
System.out.println("Before:\n"+ sc);
ByteArrayOutputStreambuf= newByteArrayOutputStream();
try {
ObjectOutputStreamo = new ObjectOutputStream(buf);
o.writeObject(sc);
// Now get it back:
ObjectInputStreamin = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
SerialCtlsc2= (SerialCtl) in.readObject();
System.out.println("After:\n"+ sc2);
}catch(Exception e) {
e.printStackTrace();
}
}
} /// :~
Before:
Not Transient: Test1
Transient: Test2
After:
Not Transient: Test1
Transient: Test2
在这个例子中,一个String 保持原始状态,其他设为transient(临时),以便证明非临时字段会被defaultWriteObject()方法自动保存,而 transient 字段必须在程序中明确保存和恢复。字段是在构建器内部初始化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化。
若准备通过默认机制写入对象的非 transient 部分,那么必须调用defaultWriteObject(),令其作为writeObject()中的第一个操作;并调用defaultReadObject(),令其作为 readObject()的第一个操作。这些都是不常见的调用方法。举个例子来说,当我们为一个ObjectOutputStream 调用defaultWriteObject()的时候,而且没有为其传递参数,就需要采取这种操作,使其知道对象的句柄以及如何写入所有非transient的部分。这种做法非常不便。
transient 对象的存储与恢复采用了我们更熟悉的代码。现在考虑一下会发生一些什么事情。在 main()中会创建一个SerialCtl 对象,随后会序列化到一个ObjectOutputStream 里(注意这种情况下使用的是一个缓冲区,而非文件——与ObjectOutputStream 完全一致)。正式的序列化操作是在下面这行代码里发生的:
o.writeObject(sc);
其中,writeObject()方法必须核查sc,判断它是否有自己的 writeObject()方法(不是检查它的接口——它根本就没有,也不是检查类的类型,而是利用反射方法实际搜索方法)。若答案是肯定的,就使用那个方法。类似的情况也会在 readObject()上发生。或许这是解决问题唯一实际的方法,但确实显得有些古怪。
3. 版本问题
有时候可能想改变一个可序列化的类的版本(比如原始类的对象可能保存在数据库中)。尽管这种做法得到了支持,但一般只应在非常特殊的情况下才用它。此外,它要求操作者对背后的原理有一个比较深的认识,而我们在这里还不想达到这种深度。JDK 1.1 的HTML 文档对这一主题进行了非常全面的论述(可从Sun公司下载,但可能也成了Java 开发包联机文档的一部分)。
一个比较诱人的想法是用序列化技术保存程序的一些状态信息,从而将程序方便地恢复到以前的状态。但在具体实现以前,有些问题是必须解决的。如果两个对象都有指向第三个对象的句柄,该如何对这两个对象序列化呢?如果从两个对象序列化后的状态恢复它们,第三个对象的句柄只会出现在一个对象身上吗?如果将这两个对象序列化成独立的文件,然后在代码的不同部分重新装配它们,又会得到什么结果呢?
下面例子对上述问题进行了很好的说明:
import java.io.*;
import java.util.*;
class House implements Serializable {
}
class Animal implements Serializable {
Stringname;
HousepreferredHouse;
Animal(Stringnm, House h) {
name = nm;
preferredHouse = h;
}
public String toString() {
return name + "[" + super.toString() + "], " + preferredHouse + "\n";
}
}
public classMyWorld {
public static void main(String[] args) {
Househouse= newHouse();
Vectoranimals= newVector();
animals.addElement(new Animal("Bosco the dog",house));
animals.addElement(new Animal("Ralph the hamster", house));
animals.addElement(new Animal("Fronk the cat",house));
System.out.println("animals: "+ animals);
try {
ByteArrayOutputStreambuf1= newByteArrayOutputStream();
ObjectOutputStreamo1 = new ObjectOutputStream(buf1);
o1.writeObject(animals);
o1.writeObject(animals); // Write a 2nd set
// Write to a different stream:
ByteArrayOutputStreambuf2= newByteArrayOutputStream();
ObjectOutputStreamo2 = new ObjectOutputStream(buf2);
o2.writeObject(animals);
// Now get them back:
ObjectInputStreamin1= newObjectInputStream(new ByteArrayInputStream(buf1.toByteArray()));
ObjectInputStreamin2= newObjectInputStream(new ByteArrayInputStream(buf2.toByteArray()));
Vectoranimals1= (Vector) in1.readObject();
Vectoranimals2= (Vector) in1.readObject();
Vectoranimals3= (Vector) in2.readObject();
System.out.println("animals1: "+ animals1);
System.out.println("animals2: "+ animals2);
System.out.println("animals3: "+ animals3);
}catch(Exception e) {
e.printStackTrace();
}
}
} /// :~
animals: [Bosco thedog[Animal@15db9742], House@6d06d69c
, Ralph thehamster[Animal@7852e922], House@6d06d69c
, Fronk the cat[Animal@4e25154f],House@6d06d69c
]
animals1: [Bosco thedog[Animal@29453f44], House@5cad8086
, Ralph thehamster[Animal@6e0be858], House@5cad8086
, Fronk the cat[Animal@61bbe9ba],House@5cad8086
]
animals2: [Bosco thedog[Animal@29453f44], House@5cad8086
, Ralph thehamster[Animal@6e0be858], House@5cad8086
, Fronk the cat[Animal@61bbe9ba],House@5cad8086
]
animals3: [Bosco thedog[Animal@610455d6], House@511d50c0
, Ralph thehamster[Animal@60e53b93], House@511d50c0
, Fronk the cat[Animal@5e2de80c],House@511d50c0
]
这里一件有趣的事情是也许是能针对一个字节数组应用对象的序列化,从而实现对任何Serializable(可序列化)对象的一个“全面复制”(全面复制意味着复制的是整个对象网,而不仅是基本对象和它的句柄)。
Animal对象包含了类型为 House 的字段。在 main()中,会创建这些Animal的一个 Vector,并对其序列化两次,分别送入两个不同的数据流内。这些数据重新装配并打印出来后,可看到下面这样的结果(对象在每次运行时都会处在不同的内存位置,所以每次运行的结果有区别):当然,我们希望装配好的对象有与原来不同的地址。但注意在 animals1 和animals2 中出现了相同的地址,其中包括共享的、对House 对象的引用。在另一方面,当animals3 恢复以后,系统没有办法知道另一个流内的对象是第一个流内对象的化身,所以会产生一个完全不同的对象网。只要将所有东西都序列化到单独一个数据流里,就能恢复获得与以前写入时完全一样的对象网,不会不慎造成对象的重复。当然,在写第一个和最后一个对象的时间之间,可改变对象的状态,但那必须由我们明确采取操作——序列化时,对象会采用它们当时的任何状态(包括它们与其他对象的连接关系)写入。
若想保存系统状态,最安全的做法是当作一种“微观”操作序列化。如果序列化了某些东西,再去做其他一些工作,再来序列化更多的东西,以此类推,那么最终将无法安全地保存系统状态。相反,应将构成系统状态的所有对象都置入单个集合内,并在一次操作里完成那个集合的写入。这样一来,同样只需一次方法调用,即可成功恢复之。
下面这个例子是一套假想的计算机辅助设计(CAD)系统,对这一方法进行了很好的演示。此外,它还为我们引入了static 字段的问题——如留意联机文档,就会发现Class 是“Serializable”(可序列化)的,所以只需简单地序列化Class 对象,就能实现static 字段的保存。这无论如何都是一种明智的做法。
import java.io.*;
import java.util.*;
abstract classShape implements Serializable {
public static final int RED = 1, BLUE = 2, GREEN = 3;
private int xPos, yPos, dimension;
private static Random r = new Random();
private static int counter = 0;
abstract public void setColor(int newColor);
abstract public int getColor();
public Shape(int xVal, int yVal, int dim) {
xPos = xVal;
yPos = yVal;
dimension = dim;
}
public String toString() {
return getClass().toString() +" color["+ getColor() + "] xPos[" + xPos+ "] yPos["+ yPos+ "] dim["
+dimension+ "]\n";
}
public static Shape randomFactory() {
int xVal = r.nextInt() % 100;
int yVal = r.nextInt() % 100;
int dim = r.nextInt() % 100;
switch (counter++ % 3) {
default:
case 0:
return new Circle(xVal, yVal, dim);
case 1:
return new Square(xVal, yVal, dim);
case 2:
return new Line(xVal, yVal, dim);
}
}
}
class Circle extends Shape {
private static int color = RED;
public Circle(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(int newColor) {
color = newColor;
}
public int getColor() {
return color;
}
}
class Square extends Shape {
private static int color;
public Square(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
color = RED;
}
public void setColor(int newColor) {
color = newColor;
}
public int getColor() {
return color;
}
}
class Line extends Shape {
private static int color = RED;
public static voidserializeStaticState(ObjectOutputStream os) throws IOException {
os.writeInt(color);
}
public static voiddeserializeStaticState(ObjectInputStream os) throws IOException {
color = os.readInt();
}
public Line(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(int newColor) {
color = newColor;
}
public int getColor() {
return color;
}
}
public classCADState {
public static void main(String[] args) throws Exception {
VectorshapeTypes,shapes;
if (args.length == 0) {
shapeTypes = new Vector();
shapes = new Vector();
// Add handles to the class objects:
shapeTypes.addElement(Circle.class);
shapeTypes.addElement(Square.class);
shapeTypes.addElement(Line.class);
// Make some shapes:
for (int i = 0; i < 10; i++)
shapes.addElement(Shape.randomFactory());
// Set all the static colors to GREEN:
for (int i = 0; i < 10; i++)
((Shape)shapes.elementAt(i)).setColor(Shape.GREEN);
// Save the state vector:
ObjectOutputStreamout= newObjectOutputStream(new FileOutputStream("CADState.out"));
out.writeObject(shapeTypes);
Line.serializeStaticState(out);
out.writeObject(shapes);
}else{ // There's a command-line argument
ObjectInputStreamin = new ObjectInputStream(new FileInputStream(args[0]));
// Read in the same order they were written:
shapeTypes = (Vector) in.readObject();
Line.deserializeStaticState(in);
shapes = (Vector) in.readObject();
}
// Display the shapes:
System.out.println(shapes);
}
} /// :~
[class Circle color[3] xPos[-95]yPos[-45] dim[11]
, class Square color[3] xPos[45]yPos[24] dim[-90]
, class Line color[3] xPos[25]yPos[-62] dim[-29]
, class Circle color[3] xPos[23]yPos[5] dim[-83]
, class Square color[3] xPos[-29]yPos[-1] dim[20]
, class Line color[3] xPos[70]yPos[70] dim[34]
, class Circle color[3] xPos[22]yPos[55] dim[-43]
, class Square color[3] xPos[89]yPos[48] dim[-55]
, class Line color[3] xPos[-75]yPos[-55] dim[59]
, class Circle color[3] xPos[-70]yPos[19] dim[-70]
]
Shape(几何形状)类“实现了可序列化”(implementsSerializable),所以从 Shape继承的任何东西也都会自动“可序列化”。每个Shape 都包含了数据,而且每个衍生的Shape 类都包含了一个特殊的 static 字段,用于决定所有那些类型的Shape 的颜色(如将一个 static字段置入基础类,结果只会产生一个字段,因为static 字段未在衍生类中复制)。可对基础类中的方法进行覆盖处理,以便为不同的类型设置颜色(static 方法不会动态绑定,所以这些都是普通的方法)。每次调用randomFactory()方法时,它都会创建一个不同的 Shape(Shape 值采用随机值)。
Circle(圆)和 Square(矩形)属于对Shape 的直接扩展;唯一的差别是Circle在定义时会初始化颜色,而Square 在构建器中初始化。Line(直线)的问题将留到以后讨论。
在main()中,一个Vector 用于容纳Class 对象,而另一个用于容纳形状。若不提供相应的命令行参数,就会创建shapeTypes Vector,并添加Class 对象。然后创建shapes Vector,并添加Shape 对象。接下来,所有static color 值都会设成GREEN,而且所有东西都会序列化到文件 CADState.out。
若提供了一个命令行参数(假设CADState.out),便会打开那个文件,并用它恢复程序的状态。无论在哪种情况下,结果产生的Shape 的Vector 都会打印出来。
从中可以看出,xPos,yPos 以及dim的值都已成功保存和恢复出来。但在获取 static信息时却出现了问题。所有“3”都已进入,但没有正常地出来。Circle 有一个 1值(定义为 RED),而Square 有一个 0值(记住,它们是在构建器里初始化的)。看上去似乎static 根本没有得到初始化!实情正是如此——尽管类Class 是“可以序列化的”,但却不能按我们希望的工作。所以假如想序列化static值,必须亲自动手。
这正是Line 中的 serializeStaticState()和deserializeStaticState()两个static 方法的用途。可以看到,这两个方法都是作为存储和恢复进程的一部分明确调用的(注意写入序列化文件和从中读回的顺序不能改变)。所以为了使CADState.java 正确运行起来,必须采用下述三种方法之一:
(1) 为几何形状添加一个serializeStaticState()和deserializeStaticState()。
(2) 删除Vector shapeTypes以及与之有关的所有代码
(3) 在几何形状内添加对新序列化和撤消序列化静态方法的调用
要注意的另一个问题是安全,因为序列化处理也会将private 数据保存下来。若有需要保密的字段,应将其标记成transient。但在这之后,必须设计一种安全的信息保存方法。这样一来,一旦需要恢复,就可以重设那些private 变量。
Java IO流库能满足我们的许多基本要求:可以通过控制台、文件、内存块甚至因特网进行
读写。可以创建新的输入和输出对象类型(通过从 InputStream和 OutputStream继承)。向一个本来预期为收到字串的方法传递一个对象时,由于Java 已限制了“自动类型转换”,所以会自动调用toString()方法。而我们可以重新定义这个toString(),扩展一个数据流能接纳的对象种类。
在IO 数据流库的联机文档和设计过程中,仍有些问题没有解决。比如当我们打开一个文件以便输出时,完全可以指定一旦有人试图覆盖该文件就“掷”出一个违例——有的编程系统允许我们自行指定想打开一个输出文件,但唯一的前提是它尚不存在。但在Java 中,似乎必须用一个 File 对象来判断某个文件是否存在,因为假如将其作为FileOutputStream或者FileWriter 打开,那么肯定会被覆盖。若同时指定文件和目录路径,File 类设计上的一个缺陷就会暴露出来,因为它会说“不要试图在单个类里做太多的事情”!
IO流库易使我们混淆一些概念。它确实能做许多事情,而且也可以移植。但假如假如事先没有吃透装饰器方案的概念,那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它,都要特别花一些功夫才行。而且它并不完整:没有提供对输出格式化的支持,而其他几乎所有语言的 IO包都提供了这方面的支持(这一点没有在Java 1.1 里得以纠正,它完全错失了改变库设计方案的机会,反而增添了更特殊的一些情况,使复杂程度进一步提高)。Java 1.1 转到那些尚未替换的IO 库,而不是增加新库。而且库的设计人员似乎没有很好地指出哪些特性是不赞成的,哪些是首选的,造成库设计中经常都会出现一些令人恼火的反对消息。然而,一旦掌握了装饰器方案,并开始在一些较为灵活的环境使用库,就会认识到这种设计的好处。到那个时候,为此多付出的代码行应该不至于使你觉得太生气。