利用“持久性”2007-05-29 yycnet.yeah.net yyc译一个比较诱人的想法是用序列化技术保存程序的一些状态信息,从而将程序方便地恢复到以前的状态。但在具体实现以前,有些问题是必须解决的。如果两个对象都有指向第三个对象的句柄,该如何对这两个对象序列化呢?如果从两个对象序列化后的状态恢复它们,第三个对象的句柄只会出现在一个对象身上吗?如果将这两个对象序列化成独立的文件,然后在代码的不同部分重新装配它们,又会得到什么结果呢?
下面这个例子对上述问题进行了很好的说明:
//: MyWorld.javaimport java.io.*;import java.util.*;class House implements Serializable {}class Animal implements Serializable {String name;House preferredHouse;Animal(String nm, House h) { name = nm; preferredHouse = h;}public String toString() {return name + "[" + super.toString() + "], " + preferredHouse + "
";}}public class MyWorld {public static void main(String[] args) {House house = new House();Vectoranimals = new Vector();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 {ByteArrayOutputStream buf1 = new ByteArrayOutputStream();ObjectOutputStream o1 =new ObjectOutputStream(buf1);o1.writeObject(animals);o1.writeObject(animals); // Write a 2nd set// Write to a different stream:ByteArrayOutputStream buf2 = new ByteArrayOutputStream();ObjectOutputStream o2 =new ObjectOutputStream(buf2);o2.writeObject(animals);// Now get them back:ObjectInputStream in1 =new ObjectInputStream(new ByteArrayInputStream(buf1.toByteArray()));ObjectInputStream in2 =new ObjectInputStream(new ByteArrayInputStream(buf2.toByteArray()));Vector animals1 = (Vector)in1.readObject();Vector animals2 = (Vector)in1.readObject();Vector animals3 = (Vector)in2.readObject();System.out.println("animals1: " + animals1);System.out.println("animals2: " + animals2);System.out.println("animals3: " + animals3);} catch(Exception e) {e.printStackTrace();}}} ///:~
这里一件有趣的事情是也许是能针对一个字节数组应用对象的序列化,从而实现对任何Serializable(可序列化)对象的一个“全面复制”(全面复制意味着复制的是整个对象网,而不仅是基本对象和它的句柄)。复制问题将在第12章进行全面讲述。
Animal对象包含了类型为House的字段。在main()中,会创建这些Animal的一个Vector,并对其序列化两次,分别送入两个不同的数据流内。这些数据重新装配并打印出来后,可看到下面这样的结果(对象在每次运行时都会处在不同的内存位置,所以每次运行的结果有区别):
animals: [Bosco the dog[Animal@1cc76c], House@1cc769, Ralph the hamster[Animal@1cc76d], House@1cc769, Fronk the cat[Animal@1cc76e], House@1cc769]animals1: [Bosco the dog[Animal@1cca0c], House@1cca16, Ralph the hamster[Animal@1cca17], House@1cca16, Fronk the cat[Animal@1cca1b], House@1cca16]animals2: [Bosco the dog[Animal@1cca0c], House@1cca16, Ralph the hamster[Animal@1cca17], House@1cca16, Fronk the cat[Animal@1cca1b], House@1cca16]animals3: [Bosco the dog[Animal@1cca52], House@1cca5c, Ralph the hamster[Animal@1cca5d], House@1cca5c, Fronk the cat[Animal@1cca61], House@1cca5c]
当然,我们希望装配好的对象有与原来不同的地址。但注意在animals1和animals2中出现了相同的地址,其中包括共享的、对House对象的引用。在另一方面,当animals3恢复以后,系统没有办法知道另一个流内的对象是第一个流内对象的化身,所以会产生一个完全不同的对象网。
只要将所有东西都序列化到单独一个数据流里,就能恢复获得与以前写入时完全一样的对象网,不会不慎造成对象的重复。当然,在写第一个和最后一个对象的时间之间,可改变对象的状态,但那必须由我们明确采取操作——序列化时,对象会采用它们当时的任何状态(包括它们与其他对象的连接关系)写入。
若想保存系统状态,最安全的做法是当作一种“微观”操作序列化。如果序列化了某些东西,再去做其他一些工作,再来序列化更多的东西,以此类推,那么最终将无法安全地保存系统状态。相反,应将构成系统状态的所有对象都置入单个集合内,并在一次操作里完成那个集合的写入。这样一来,同样只需一次方法调用,即可成功恢复之。
下面这个例子是一套假想的计算机辅助设计(CAD)系统,对这一方法进行了很好的演示。此外,它还为我们引入了static字段的问题——如留意联机文档,就会发现Class是“Serializable”(可序列化)的,所以只需简单地序列化Class对象,就能实现static字段的保存。这无论如何都是一种明智的做法。
//: CADState.java// Saving and restoring the state of a // pretend CAD system.import java.io.*;import java.util.*;abstract class Shape 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 + "]
";}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 void serializeStaticState(ObjectOutputStream os)throws IOException {os.writeInt(color);}public static void deserializeStaticState(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 class CADState {public static void main(String[] args) throws Exception {Vector shapeTypes, 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:ObjectOutputStream out =new ObjectOutputStream(new FileOutputStream("CADState.out"));out.writeObject(shapeTypes);Line.serializeStaticState(out);out.writeObject(shapes);} else { // There"s a command-line argumentObjectInputStream in =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);}} ///:~
Shape(几何形状)类“实现了可序列化”(implements Serializable),所以从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都会打印出来。下面列出它某一次运行的结果:
>java CADState[class Circle color[3] xPos[-51] yPos[-99] dim[38], class Square color[3] xPos[2] yPos[61] dim[-46], class Line color[3] xPos[51] yPos[73] dim[64], class Circle color[3] xPos[-70] yPos[1] dim[16], class Square color[3] xPos[3] yPos[94] dim[-36], class Line color[3] xPos[-84] yPos[-21] dim[-35], class Circle color[3] xPos[-75] yPos[-43] dim[22], class Square color[3] xPos[81] yPos[30] dim[-45], class Line color[3] xPos[-29] yPos[92] dim[17], class Circle color[3] xPos[17] yPos[90] dim[-76]]>java CADState CADState.out[class Circle color[1] xPos[-51] yPos[-99] dim[38], class Square color[0] xPos[2] yPos[61] dim[-46], class Line color[3] xPos[51] yPos[73] dim[64], class Circle color[1] xPos[-70] yPos[1] dim[16], class Square color[0] xPos[3] yPos[94] dim[-36], class Line color[3] xPos[-84] yPos[-21] dim[-35], class Circle color[1] xPos[-75] yPos[-43] dim[22], class Square color[0] xPos[81] yPos[30] dim[-45], class Line color[3] xPos[-29] yPos[92] dim[17], class Circle color[1] xPos[17] yPos[90] dim[-76]]
从中可以看出,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变量。