- 写在前面的话
- 引言
- 第 1 章 对象入门
- 第 2 章 一切都是对象
- 第 3 章 控制程序流程
- 第 4 章 初始化和清除
- 第 5 章 隐藏实施过程
- 第 6 章 类再生
- 第 7 章 多形性
- 第 8 章 对象的容纳
- 第 9 章 违例差错控制
- 第 10 章 Java IO 系统
- 第 11 章 运行期类型鉴定
- 第 12 章 传递和返回对象
- 第 十三 章 创建窗口和程序片
- 第 14 章 多线程
- 第 15 章 网络编程
- 第 16 章 设计范式
- 第 17 章 项目
- 附录 A 使用非 JAVA 代码
- 附录 B 对比 C++和 Java
- 附录 C Java 编程规则
- 附录 D 性能
- 附录 E 关于垃圾收集的一些话
- 附录 F 推荐读物
10.9.2 序列化的控制
正如大家看到的那样,默认的序列化机制并不难操纵。然而,假若有特殊要求又该怎么办呢?我们可能有特殊的安全问题,不希望对象的某一部分序列化;或者某一个子对象完全不必序列化,因为对象恢复以后,那一部分需要重新创建。
此时,通过实现 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论