返回介绍

10.9.2 序列化的控制

发布于 2024-10-15 23:56:24 字数 11359 浏览 0 评论 0 收藏 0

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

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

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

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

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) {
    System.out.println("Constructing objects:");
    Blip1 b1 = new Blip1();
    Blip2 b2 = new Blip2();
    try {
      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();
    } 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 对象必须做的全部事情:

//: Blip3.java
// Reconstructing an externalizable object
import java.io.*;
import java.util.*;

class Blip3 implements Externalizable {
  int i;
  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) {
    System.out.println("Constructing objects:");
    Blip3 b3 = new Blip3("A String ", 47);
    System.out.println(b3.toString());
    try {
      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.toString());
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

其中,字段 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。下面是具体的代码:

//: Logon.java
// Demonstrates the "transient" keyword
import java.io.*;
import java.util.*;

class Logon implements Serializable {
  private Date date = new Date();
  private String username;
  private transient String password;
  Logon(String name, String pwd) {
    username = name;
    password = pwd;
  }
  public String toString() {
    String pwd =
      (password == null) ? "(n/a)" : password;
    return "logon info: \n   " +
      "username: " + username +
      "\n   date: " + date.toString() +
      "\n   password: " + pwd;
  }
  public static void main(String[] args) {
    Logon a = new Logon("Hulk", "myLittlePony");
    System.out.println( "logon a = " + a);
    try {
      ObjectOutputStream o =
        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:
      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);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

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

logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: myLittlePony
Recovering object at Sun Mar 23 18:25:59 PST 1997
logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: (n/a)

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

我们也发现 date 字段被保存到磁盘,并从磁盘恢复,没有重新生成。

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

2. 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 对象的存储与恢复进行控制:

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

public class SerialCtl implements Serializable {
  String a;
  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) {
    SerialCtl sc = 
      new SerialCtl("Test1", "Test2");
    System.out.println("Before:\n" + sc);
    ByteArrayOutputStream buf = 
      new ByteArrayOutputStream();
    try {
      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);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

在这个例子中,一个 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 开发包联机文档的一部分)。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文