# 10.9 對象序列化
Java 1.1增添了一種有趣的特性,名為“對象序列化”(Object Serialization)。它面向那些實現了`Serializable`接口的對象,可將它們轉換成一系列字節,并可在以后完全恢復回原來的樣子。這一過程亦可通過網絡進行。這意味著序列化機制能自動補償操作系統間的差異。換句話說,可以先在Windows機器上創建一個對象,對其序列化,然后通過網絡發給一臺Unix機器,然后在那里準確無誤地重新“裝配”。不必關心數據在不同機器上如何表示,也不必關心字節的順序或者其他任何細節。
就其本身來說,對象的序列化是非常有趣的,因為利用它可以實現“有限持久化”。請記住“持久化”意味著對象的“生存時間”并不取決于程序是否正在執行——它存在或“生存”于程序的每一次調用之間。通過序列化一個對象,將其寫入磁盤,以后在程序重新調用時重新恢復那個對象,就能圓滿實現一種“持久”效果。之所以稱其為“有限”,是因為不能用某種`persistent`(持久)關鍵字簡單地地定義一個對象,并讓系統自動照看其他所有細節問題(盡管將來可能成為現實)。相反,必須在自己的程序中明確地序列化和組裝對象。
語言里增加了對象序列化的概念后,可提供對兩種主要特性的支持。Java 1.1的“遠程方法調用”(RMI)使本來存在于其他機器的對象可以表現出好象就在本地機器上的行為。將消息發給遠程對象時,需要通過對象序列化來傳輸參數和返回值。RMI將在第15章作具體討論。
對象的序列化也是Java Beans必需的,后者由Java 1.1引入。使用一個Bean時,它的狀態信息通常在設計期間配置好。程序啟動以后,這種狀態信息必須保存下來,以便程序啟動以后恢復;具體工作由對象序列化完成。
對象的序列化處理非常簡單,只需對象實現了`Serializable`接口即可(該接口僅是一個標記,沒有方法)。在Java 1.1中,許多標準庫類都發生了改變,以便能夠序列化——其中包括用于基本數據類型的全部包裝器、所有集合類以及其他許多東西。甚至`Class`對象也可以序列化(第11章講述了具體實現過程)。
為序列化一個對象,首先要創建某些`OutputStream`對象,然后將其封裝到`ObjectOutputStream`對象內。此時,只需調用`writeObject()`即可完成對象的序列化,并將其發送給`OutputStream`。相反的過程是將一個`InputStream`封裝到`ObjectInputStream`內,然后調用`readObject()`。和往常一樣,我們最后獲得的是指向一個向上轉換`Object`的引用,所以必須向下轉換,以便能夠直接設置。
對象序列化特別“聰明”的一個地方是它不僅保存了對象的“全景圖”,而且能追蹤對象內包含的所有引用并保存那些對象;接著又能對每個對象內包含的引用進行追蹤;以此類推。我們有時將這種情況稱為“對象網”,單個對象可與之建立連接。而且它還包含了對象的引用數組以及成員對象。若必須自行操縱一套對象序列化機制,那么在代碼里追蹤所有這些鏈接時可能會顯得非常麻煩。在另一方面,由于Java對象的序列化似乎找不出什么缺點,所以請盡量不要自己動手,讓它用優化的算法自動維護整個對象網。下面這個例子對序列化機制進行了測試。它建立了許多鏈接對象的一個`Worm`(蠕蟲),每個對象都與`Worm`中的下一段鏈接,同時又與屬于不同類(`Data`)的對象引用數組鏈接:
```
//: Worm.java
// Demonstrates object serialization in Java 1.1
import java.io.*;
class Data implements Serializable {
private int i;
Data(int x) { i = x; }
public String toString() {
return Integer.toString(i);
}
}
public class Worm 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() {
String s = ":" + 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) {
Worm w = new Worm(6, 'a');
System.out.println("w = " + w);
try {
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("worm.out"));
out.writeObject("Worm storage");
out.writeObject(w);
out.close(); // Also flushes output
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("worm.out"));
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
System.out.println(s + ", w2 = " + w2);
} catch(Exception e) {
e.printStackTrace();
}
try {
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
ObjectOutputStream out =
new ObjectOutputStream(bout);
out.writeObject("Worm storage");
out.writeObject(w);
out.flush();
ObjectInputStream in =
new ObjectInputStream(
new ByteArrayInputStream(
bout.toByteArray()));
String s = (String)in.readObject();
Worm w3 = (Worm)in.readObject();
System.out.println(s + ", w3 = " + w3);
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
```
更有趣的是,`Worm`內的`Data`對象數組是用隨機數字初始化的(這樣便不用懷疑編譯器保留了某種原始信息)。每個`Worm`段都用一個`Char`標記。這個`Char`是在重復生成鏈接的`Worm`列表時自動產生的。創建一個`Worm`時,需告訴構造器希望它有多長。為產生下一個引用(`next`),它總是用減去1的長度來調用`Worm`構造器。最后一個`next`引用則保持為`null`(空),表示已抵達`Worm`的尾部。
上面的所有操作都是為了加深事情的復雜程度,加大對象序列化的難度。然而,真正的序列化過程卻是非常簡單的。一旦從另外某個流里創建了`ObjectOutputStream`,`writeObject()`就會序列化對象。注意也可以為一個`String`調用`writeObject()`。亦可使用與`DataOutputStream`相同的方法寫入所有基本數據類型(它們有相同的接口)。
有兩個單獨的`try`塊看起來是類似的。第一個讀寫的是文件,而另一個讀寫的是一個`ByteArray`(字節數組)。可利用對任何`DataInputStream`或者`DataOutputStream`的序列化來讀寫特定的對象;正如在關于連網的那一章會講到的那樣,這些對象甚至包括網絡。一次循環后的輸出結果如下:
```
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)
```
可以看出,裝配回原狀的對象確實包含了原來那個對象里包含的所有鏈接。
注意在對一個`Serializable`(可序列化)對象進行重新裝配的過程中,不會調用任何構造器(甚至默認構造器)。整個對象都是通過從`InputStream`中取得數據恢復的。
作為Java 1.1特性的一種,我們注意到對象的序列化并不屬于新的`Reader`和`Writer`層次結構的一部分,而是沿用老式的`InputStream`和`OutputStream`結構。所以在一些特殊的場合下,不得不混合使用兩種類型的層次結構。
## 10.9.1 尋找類
讀者或許會奇怪為什么需要一個對象從它的序列化狀態中恢復。舉個例子來說,假定我們序列化一個對象,并通過網絡將其作為文件傳送給另一臺機器。此時,位于另一臺機器的程序可以只用文件目錄來重新構造這個對象嗎?
回答這個問題的最好方法就是做一個實驗。下面這個文件位于本章的子目錄下:
```
//: Alien.java
// A serializable class
import java.io.*;
public class Alien implements Serializable {
} ///:~
```
用于創建和序列化一個`Alien`對象的文件位于相同的目錄下:
```
//: FreezeAlien.java
// Create a serialized output file
import java.io.*;
public class FreezeAlien {
public static void main(String[] args)
throws Exception {
ObjectOutput out =
new ObjectOutputStream(
new FileOutputStream("file.x"));
Alien zorcon = new Alien();
out.writeObject(zorcon);
}
} ///:~
```
該程序并不是捕獲和控制異常,而是將異常簡單、直接地傳遞到`main()`外部,這樣便能在命令行報告它們。
程序編譯并運行后,將結果產生的`file.x`復制到名為`xfiles`的子目錄,代碼如下:
```
//: ThawAlien.java
// Try to recover a serialized file without the
// class of object that's stored in that file.
package c10.xfiles;
import java.io.*;
public class ThawAlien {
public static void main(String[] args)
throws Exception {
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("file.x"));
Object mystery = in.readObject();
System.out.println(
mystery.getClass().toString());
}
} ///:~
```
該程序能打開文件,并成功讀取`mystery`對象中的內容。然而,一旦嘗試查找與對象有關的任何資料——這要求`Alien`的`Class`對象——Java虛擬機(JVM)便找不到`Alien.class`(除非它正好在類路徑內,而本例理應相反)。這樣就會得到一個名叫`ClassNotFoundException`的異常(同樣地,若非能夠校驗`Alien`存在的證據,否則它等于消失)。
恢復了一個序列化的對象后,如果想對其做更多的事情,必須保證JVM能在本地類路徑或者因特網的其他什么地方找到相關的`.class`文件。
## 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開發包聯機文檔的一部分)。
## 10.9.3 利用“持久性”
一個比較誘人的想法是用序列化技術保存程序的一些狀態信息,從而將程序方便地恢復到以前的狀態。但在具體實現以前,有些問題是必須解決的。如果兩個對象都有指向第三個對象的引用,該如何對這兩個對象序列化呢?如果從兩個對象序列化后的狀態恢復它們,第三個對象的引用只會出現在一個對象身上嗎?如果將這兩個對象序列化成獨立的文件,然后在代碼的不同部分重新裝配它們,又會得到什么結果呢?
下面這個例子對上述問題進行了很好的說明:
```
//: MyWorld.java
import 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 + "\n";
}
}
public class MyWorld {
public static void main(String[] args) {
House house = new House();
Vector animals = 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 + "]\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 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 argument
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`值,必須親自動手。
這正是`Line`中的`serializeStaticState()`和`deserializeStaticState()`兩個`static`方法的用途。可以看到,這兩個方法都是作為存儲和恢復進程的一部分明確調用的(注意寫入序列化文件和從中讀回的順序不能改變)。所以為了使`CADState.java`正確運行起來,必須采用下述三種方法之一:
(1) 為幾何形狀添加一個`serializeStaticState()`和`deserializeStaticState()`。
(2) 刪除`Vector shapeTypes`以及與之有關的所有代碼
(3) 在幾何形狀內添加對新序列化和撤消序列化靜態方法的調用
要注意的另一個問題是安全,因為序列化處理也會將`private`數據保存下來。若有需要保密的字段,應將其標記成`transient`。但在這之后,必須設計一種安全的信息保存方法。這樣一來,一旦需要恢復,就可以重設那些`private`變量。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物