对象序列化

  当你创建对象时,只要你需要,它就会一直存在,但是在程序终止时,无论如何它都不会继续存在。尽管这么做肯定是有意义的,但是仍旧存在某些情况,如果对象能够在程序不运行的情况下仍能存在并保存其信息,那将非常有用。这样,在下次运行程序时,该对象将被重建并且拥有的信息与在程序上次运行时它所拥有的信息相同。当然,你可以通过将信息写入文件或数据库来达到相同的效果,但是在使万物都成为对象的精神中,如果能够将一个对象声明为“持久性”的,并为我们处理掉所有的细节,那将会显得十分的方便。

  Java对象序列化将那些实现了Serializable接口的对象转化成一个字节序列,并能够在以后将这个字节序列化完全恢复为原来的对象。这一过程甚至可以通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。换句话说,可以先在Windows 机器上创建一个对象,对其序列化,然后通过网络发给一台Unix 机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

  就其本身来说,对象的序列化是非常有趣的,因为利用它可以实现“轻量级持久化”。请记住“持久化”意味着对象的“生存时间”并不取决于程序是否正在执行——它存在或“生存”于程序的每一次调用之间。通过序列化一个对象,将其写入磁盘,以后在程序重新调用时重新恢复那个对象,就能圆满实现一种“持久”效果。之所以称其为“轻量级”,是因为不能用某种“persistent”(持久)关键字简单地地定义一个对象,并让系统自动照看其他所有细节问题(尽管将来可能成为现实)。相反,必须在自己的程序中显示地序列化(serialize)和反序列化还原(deserialize)。

  Java语言里增加了对象序列化的概念后,可提供对两种主要特性的支持。Java 的“远程方法调用”(RMI)使本来存在于其他机器的对象可以表现出好像就在本地机器上的行为。将消息发给远程对象时,需要通过对象序列化来传输参数和返回值。

  对象的序列化也是Java Beans 必需的,后者由Java 引入。使用一个Bean 时,它的状态信息通常在设计期间配置好。程序启动以后,这种状态信息必须保存下来,以便程序启动以后恢复;具体工作由对象序列化完成。

  对象的序列化处理非常简单,只需对象实现了Serializable 接口即可(该接口仅是一个标记,没有方法)。在Java中,许多标准库类都发生了改变,以便能够序列化——其中包括用于基本数据类型的全部封装器、所有集合类以及其他许多东西。甚至Class 对象也可以序列化。

  为序列化一个对象,首先要创建某些OutputStream 对象,然后将其封装到ObjectOutputStream 对象内。此时,只需调用writeObject()即可完成对象的序列化,并将其发送给OutputStream。相反的过程是将一个InputStream 封装到ObjectInputStream 内,然后调用readObject()。和往常一样,我们最后获得的是指向一个上溯造型Object 的句柄,所以必须下溯造型,以便能够直接设置。

  对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内包含的所有句柄并保存那些对象;接着又能对每个对象内包含的句柄进行追踪;以此类推。我们有时将这种情况称为“对象网”,单个对象可与之建立连接。而且它还包含了对象的句柄数组以及成员对象。若必须自行操纵一套对象序列化机制,那么在代码里追踪所有这些链接时可能会显得非常麻烦。在另一方面,由于Java 对象的序列化似乎找不出什么缺点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。

1.序列化的控制

  正如大家看到的那样,默认的序列化机制并不难操纵。然而,假若有特殊要求又该怎么办呢?我们可能有特殊的安全问题,不希望对象的某一部分序列化;或者某一个子对象完全不必序列化,因为对象恢复以后,那一部分需要重新创建。

  此时,通过实现Externalizable 接口,用它代替Serializable 接口,便可控制序列化的具体过程。这个Externalizable 接口扩展了Serializable,并增添了两个方法:writeExternal()和readExternal()。在序列化和反序列化过程中,会自动调用这两个方法,以便我们执行一些特殊操作。

下面这个例子展示了Externalizable 接口方法的简单应用。注意Blip1 和Blip2 几乎完全一致,除了极微小的差别(自己研究一下代码,看看是否能发现):

//: io/Blips.java
// Simple use of Externalizable & a pitfall.
import java.io.*;
import static net.mindview.util.Print.*;

class Blip1 implements Externalizable {
  public Blip1() {
    print("Blip1 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    print("Blip1.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    print("Blip1.readExternal");
  }
}

class Blip2 implements Externalizable {
  Blip2() {
    print("Blip2 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    print("Blip2.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    print("Blip2.readExternal");
  }
}

public class Blips {
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    print("Constructing objects:");
    Blip1 b1 = new Blip1();
    Blip2 b2 = new Blip2();
    ObjectOutputStream o = new ObjectOutputStream(
      new FileOutputStream("Blips.out"));
    print("Saving objects:");
    o.writeObject(b1);
    o.writeObject(b2);
    o.close();
    // Now get them back:
    ObjectInputStream in = new ObjectInputStream(
      new FileInputStream("Blips.out"));
    print("Recovering b1:");
    b1 = (Blip1)in.readObject();
    // OOPS! Throws an exception:
//! print("Recovering b2:");
//! b2 = (Blip2)in.readObject();
  }
} /* Output:
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 对象必须做的全部事情:

//: io/Blip3.java
// Reconstructing an externalizable object.
import java.io.*;
import static net.mindview.util.Print.*;

public class Blip3 implements Externalizable {
  private int i;
  private String s; // No initialization
  public Blip3() {
    print("Blip3 Constructor");
    // s, i not initialized
  }
  public Blip3(String x, int a) {
    print("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 {
    print("Blip3.writeExternal");
    // You must do this:
    out.writeObject(s);
    out.writeInt(i);
  }
  public void readExternal(ObjectInput in)
  throws IOException, ClassNotFoundException {
    print("Blip3.readExternal");
    // You must do this:
    s = (String)in.readObject();
    i = in.readInt();
  }
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    print("Constructing objects:");
    Blip3 b3 = new Blip3("A String ", 47);
    print(b3);
    ObjectOutputStream o = new ObjectOutputStream(
      new FileOutputStream("Blip3.out"));
    print("Saving object:");
    o.writeObject(b3);
    o.close();
    // Now get it back:
    ObjectInputStream in = new ObjectInputStream(
      new FileInputStream("Blip3.out"));
    print("Recovering b3:");
    b3 = (Blip3)in.readObject();
    print(b3);
  }
} /* Output:
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 对象的默认构建行为使其看起来似乎正在进行某种存储与恢复操作。但实情并非如此。

transient(瞬时)关键字

  控制序列化过程时,可能有一个特定的子对象不愿让Java 的序列化机制自动保存与恢复。一般地,若那个子对象包含了不想序列化的敏感信息(如密码),就会面临这种情况。即使那种信息在对象中具有“private”(私有)属性,但一旦经序列化处理,人们就可以通过读取一个文件,或者拦截网络传输得到它。

  为防止对象的敏感部分被序列化,一个办法是将自己的类实现为Externalizable,就象前面展示的那样。这样一来,没有任何东西可以自动序列化,只能在writeExternal()明确序列化那些需要的部分。

  然而,若操作的是一个Serializable 对象,所有序列化操作都会自动进行。为解决这个问题,可以用transient(临时)逐个字段地关闭序列化,它的意思是“不要麻烦你(指自动机制)保存或恢复它了——我会自己处理的”。

  例如,假设一个Login 对象包含了与一个特定的登录会话有关的信息。校验登录的合法性时,一般都想将数据保存下来,但不包括密码。为做到这一点,最简单的办法是实现Serializable,并将password 字段设为transient。下面是具体的代码:

//: io/Logon.java
// Demonstrates the "transient" keyword.
import java.util.concurrent.*;
import java.io.*;
import java.util.*;
import static net.mindview.util.Print.*;

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");
    print("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"));
    print("Recovering object at " + new Date());
    a = (Logon)in.readObject();
    print("logon a = " + a);
  }
} /* Output: (Sample)
logon a = logon info:
   username: Hulk
   date: Sat Nov 19 15:03:26 MST 2005
   password: myLittlePony
Recovering object at Sat Nov 19 15:03:28 MST 2005
logon a = logon info:
   username: Hulk
   date: Sat Nov 19 15:03:26 MST 2005
   password: null
*///:~

  可以看到,其中的date 和username 字段保持原始状态(未设成transient),所以会自动序列化。然而,password 被设为transient,所以不会自动保存到磁盘;另外,自动序列化机制也不会作恢复它的尝试。

  一旦对象恢复成原来的样子,password 字段就会变成null。注意必须用toString()检查password 是否为null,因为若用过载的“+”运算符来装配一个String 对象,而且那个运算符遇到一个null 句柄,就会造成一个名为NullPointerException 的违例(新版Java 可能会提供避免这个问题的代码)。我们也发现date 字段被保存到磁盘,并从磁盘恢复,没有重新生成。

由于Externalizable 对象默认时不保存它的任何字段,所以transient 关键字只能伴随Serializable 使用。

transient使用小结

1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

Externalizable 的替代方法

  若不是特别在意要实现Externalizable 接口,还有另一种方法可供选用。我们可以实现Serializable 接口,并添加(注意是“添加”,而非“覆盖”或者“实现”)名为writeObject()和readObject()的方法。一旦对象被序列化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑默认的序列化机制。这些方法必须含有下列准确的签名:

private void writeObject(ObjectOutputStream stream) 
throws IOException;

private void readObject(ObjectInputStream stream) 
throws IOException, ClassNotFoundException

  从设计的角度出发,情况变得有些扑朔迷离。首先,大家可能认为这些方法不属于基础类或者Serializable接口的一部分,它们应该在自己的接口中得到定义。但请注意它们被定义成“private”,这意味着它们只能由这个类的其他成员调用。然而,我们实际并不从这个类的其他成员中调用它们,而是由ObjectOutputStreamObjectInputStreamwriteObject()readObject()方法来调用我们对象的writeObject()readObject()方法(注意我在这里用了很大的抑制力来避免使用相同的方法名——因为怕混淆)。大家可能奇怪ObjectOutputStreamObjectInputStream 如何有权访问我们的类的private方法——只能认为这是序列化机制玩的一个把戏。

  在任何情况下,接口中的定义的任何东西都会自动具有public 属性,所以假若writeObject()readObject()必须为private,那么它们不能成为接(interface)的一部分。但由于我们准确地加上了签名,所以最终的效果实际与实现一个接口是相同的。看起来似乎我们调用ObjectOutputStream.writeObject()的时候,我们传递给它的Serializable 对象似乎会被检查是否实现了自己writeObject()。若答案是肯定的是,便会跳过常规的序列化过程,并调用writeObject()readObject()也会遇到同样的情况。

  还存在另一个问题。在我们的writeObject() 内部,可以调用defaultWriteObject(),从而决定采取默认的writeObject()行动。类似地,在readObject()内部,可以调用defaultReadObject()。下面这个简单的例子演示了如何对一个Serializable 对象的存储与恢复进行控制:

//: io/SerialCtl.java
// Controlling serialization by adding your own
// writeObject() and readObject() methods.
import java.io.*;

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);   // 如果此行代码不存在,则transizent起作用
  }
  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);
  }
} /* Output:
Before:
Not Transient: Test1
Transient: Test2
After:
Not Transient: Test1
Transient: Test2
*///:~

  在这个例子中,一个String 保持原始状态,其他设为transient(临时),以便证明非临时字段会被defaultWriteObject()方法自动保存,而transient 字段必须在程序中明确保存和恢复。字段是在构建器内部初始化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化(因为通过Serialize序列化的对象完全以它保存下来的二进制位为基础恢复,不存在构建器调用)。

  若准备通过默认机制写入对象的非transient 部分,那么必须调用defaultWriteObject(),令其作为writeObject()中的第一个操作;并调用defaultReadObject(),令其作为readObject()的第一个操作。这些都是不常见的调用方法。举个例子来说,当我们为一个ObjectOutputStream 调用defaultWriteObject()的时候,而且没有为其传递参数,就需要采取这种操作,使其知道对象的句柄以及如何写入所有非transient的部分。这种做法非常不便。

  transient 对象的存储与恢复采用了我们更熟悉的代码。现在考虑一下会发生一些什么事情。在main()中会创建一个SerialCtl 对象,随后会序列化到一个ObjectOutputStream 里(注意这种情况下使用的是一个缓冲区,而非文件——与ObjectOutputStream 完全一致)。正式的序列化操作是在下面这行代码里发生的:

o.writeObject(sc);

  其中,writeObject()方法必须核查sc,判断它是否有自己的writeObject()方法(不是检查它的接口——它根本就没有,也不是检查类的类型,而是利用反射方法实际搜索方法)。若答案是肯定的,就使用那个方法。类似的情况也会在readObject()上发生。或许这是解决问题唯一实际的方法,但确实显得有些古怪。

利用“持久性”

  一个比较诱人的想法是用序列化技术保存程序的一些状态信息,从而将程序方便地恢复到以前的状态。但在具体实现以前,有些问题是必须解决的。如果两个对象都有指向第三个对象的句柄,该如何对这两个对象序列化呢?如果从两个对象序列化后的状态恢复它们,第三个对象的句柄只会出现在一个对象身上吗?如果将这两个对象序列化成独立的文件,然后在代码的不同部分重新装配它们,又会得到什么结果呢?
下面这个例子对上述问题进行了很好的说明:

import java.io.*;
import java.util.*;
import static net.mindview.util.Print.*;

class House implements Serializable {}

class Animal implements Serializable {
  private String name;
  private House preferredHouse;
  Animal(String nm, House h) {
    name = nm;
    preferredHouse = h;
  }
  public String toString() {
    return name + "[" + super.toString() +
      "], " + preferredHouse + "\n";
  }
}

public class MyWorld {
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    House house = new House();
    List<Animal> animals = new ArrayList<Animal>();
    animals.add(new Animal("Bosco the dog", house));
    animals.add(new Animal("Ralph the hamster", house));
    animals.add(new Animal("Molly the cat", house));
    print("animals: " + animals);
    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()));
    List
      animals1 = (List)in1.readObject(),
      animals2 = (List)in1.readObject(),
      animals3 = (List)in2.readObject();
    print("animals1: " + animals1);
    print("animals2: " + animals2);
    print("animals3: " + animals3);
  }
}

  这里一件有趣的事情是也许是能针对一个字节数组应用对象的序列化,从而实现对任何Serializable(可序列化)对象的一个“全面复制”(全面复制意味着复制的是整个对象网,而不仅是基本对象和它的句柄)。

  Animal 对象包含了类型为House 的字段。在main()中,会创建这些Animal 的一个Vector,并对其序列化两次,分别送入两个不同的数据流内。这些数据重新装配并打印出来后,可看到下面这样的结果(对象在每次运行时都会处在不同的内存位置,所以每次运行的结果有区别):

animals: [Bosco the dog[Animal@addbf1], House@42e816
, Ralph the hamster[Animal@9304b1], House@42e816
, Molly the cat[Animal@190d11], House@42e816
]
animals1: [Bosco the dog[Animal@de6f34], House@156ee8e
, Ralph the hamster[Animal@47b480], House@156ee8e
, Molly the cat[Animal@19b49e6], House@156ee8e
]
animals2: [Bosco the dog[Animal@de6f34], House@156ee8e
, Ralph the hamster[Animal@47b480], House@156ee8e
, Molly the cat[Animal@19b49e6], House@156ee8e
]
animals3: [Bosco the dog[Animal@10d448], House@e0e1c6
, Ralph the hamster[Animal@6ca1c], House@e0e1c6
, Molly the cat[Animal@1bf216a], House@e0e1c6
]

  当然,我们希望装配好的对象有与原来不同的地址。但注意在animals1 和animals2 中出现了相同的地址,其中包括共享的、对House 对象的引用。在另一方面,当animals3 恢复以后,系统没有办法知道另一个流内的对象是第一个流内对象的化身,所以会产生一个完全不同的对象网。

  只要将所有东西都序列化到单独一个数据流里,就能恢复获得与以前写入时完全一样的对象网,不会不慎造成对象的重复。当然,在写第一个和最后一个对象的时间之间,可改变对象的状态,但那必须由我们明确采取操作——序列化时,对象会采用它们当时的任何状态(包括它们与其他对象的连接关系)写入。若想保存系统状态,最安全的做法是当作一种“微观”操作序列化。如果序列化了某些东西,再去做其他一些工作,再来序列化更多的东西,以此类推,那么最终将无法安全地保存系统状态。相反,应将构成系统状态的所有对象都置入单个集合内,并在一次操作里完成那个集合的写入。这样一来,同样只需一次方法调用,即可成功恢复之。

  下面这个例子是一套假想的计算机辅助设计(CAD)系统,对这一方法进行了很好的演示。此外,它还为我们引入了static 字段的问题——如留意联机文档,就会发现Class 是“Serializable”(可序列化)的,所以只需简单地序列化Class 对象,就能实现static 字段的保存。这无论如何都是一种明智的做法。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Random;
import java.util.Vector;

abstract class Shape implements Serializable {
    public static final int RED     = 1, BLUE = 2, GREEN = 3;
    private int             xPos, yPos, dimension;
    private static Random   rand    = new Random(47);
    private static int      counter = 0;

    public abstract void setColor(int newColor);

    public abstract int getColor();

    public Shape(int xVal, int yVal, int dim) {
        xPos = xVal;
        yPos = yVal;
        dimension = dim;
    }

    public String toString() {
        return getClass() + "color[" + getColor() + "] xPos[" + xPos + "] yPos[" + yPos + "] dim["
               + dimension + "]\n";
    }

    public static Shape randomFactory() {
        int xVal = rand.nextInt(100);
        int yVal = rand.nextInt(100);
        int dim = rand.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 StoreCADState {

    @SuppressWarnings({ "rawtypes", "unchecked" })
    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 argument
        // 重启机器后jvm中的方法区中的static变量被初始化为初始值
            ObjectInputStream 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 值,必须亲自动手(static 修饰的变量本身是不会被序列化的我们读取的值是当前jvm中的方法区对应此变量的值,所以最后输出的值为我们对static 变量后赋的值)。这正是Line 中的serializeStaticState()和deserializeStaticState()两个static 方法的用途。可以看到,这两个方法都是作为存储和恢复进程的一部分明确调用的(注意写入序列化文件和从中读回的顺序不能改变)。所以为了使CADState.java 正确运行起来,必须采用下述三种方法之一:
(1) 为几何形状添加一个serializeStaticState()和deserializeStaticState()。
(2) 删除Vector shapeTypes 以及与之有关的所有代码
(3) 在几何形状内添加对新序列化和撤消序列化静态方法的调用
要注意的另一个问题是安全,因为序列化处理也会将private 数据保存下来。若有需要保密的字段,应将其标记成transient。但在这之后,必须设计一种安全的信息保存方法。这样一来,一旦需要恢复,就可以重设那些private 变量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值