[TOC]
<!-- Appendix: Object Serialization -->
# 附錄:對象序列化
當你創建對象時,只要你需要,它就會一直存在,但是在程序終止時,無論如何它都不會繼續存在。盡管這么做肯定是有意義的,但是仍舊存在某些情況,如果對象能夠在程序不運行的情況下仍能存在并保存其信息,那將非常有用。這樣,在下次運行程序時,該對象將被重建并且擁有的信息與在程序上次運行時它所擁有的信息相同。當然,你可以通過將信息寫入文件或數據庫來達到相同的效果,但是在使萬物都成為對象的精神中,如果能夠將一個對象聲明為是“持久性”的,并為我們處理掉所有細節,那將會顯得十分方便。
Java 的對象序列化將那些實現了 Serializable 接口的對象轉換成一個字節序列,并能夠在以后將這個字節序列完全恢復為原來的對象。這一過程甚至可通過網絡進行,這意味著序列化機制能自動彌補不同操作系統之間的差異。也就是說,可以在運行 Windows 系統的計算機上創建一個對象,將其序列化,通過網絡將它發送給一臺運行 Unix 系統的計算機,然后在那里準確地重新組裝,而卻不必擔心數據在不同機器上的表示會不同,也不必關心宇節的順序或者其他任何細節。
就其本身來說,對象序列化可以實現輕量級持久性(lightweight persistence),“持久性”意味著一個對象的生存周期并不取決于程序是否正在執行它可以生存于程序的調用之間。通過將一個序列化對象寫入磁盤,然后在重新調用程序時恢復該對象,就能夠實現持久性的效果。之所以稱其為“輕量級”,是因為不能用某種"persistent"(持久)關鍵字來簡單地定義一個對象,并讓系統自動維護其他細節問題(盡管將來有可能實現)。相反,對象必須在程序中顯式地序列化(serialize)和反序列化還原(deserialize),如果需要個更嚴格的持久性機制,可以考慮像 Hibernate 之類的工具。
對象序列化的概念加入到語言中是為了支持兩種主要特性。一是 Java 的遠程方法調用(Remote Method Invocation,RMI),它使存活于其他計算機上的對象使用起來就像是存活于本機上一樣。當向遠程對象發送消息時,需要通過對象序列化來傳輸參數和返回值。
再者,對 Java Beans 來說,對象的序列化也是必需的(在撰寫本文時被視為失敗的技術),使用一個 Bean 時,一般情況下是在設計階段對它的狀態信息進行配置。這種狀態信息必須保存下來,并在程序啟動時進行后期恢復,這種具體工作就是由對象序列化完成的。
只要對象實現了 Serializable 接口(該接口僅是一個標記接口,不包括任何方法),對象的序列化處理就會非常簡單。當序列化的概念被加入到語言中時,許多標準庫類都發生了改變,以便具備序列化特性-其中包括所有基本數據類型的封裝器、所有容器類以及許多其他的東西。甚至 Class 對象也可以被序列化。
要序列化一個對象,首先要創建某些 OutputStream 對象,然后將其封裝在一個 ObjectOutputStream 對象內。這時,只需調用 writeObject() 即可將對象序列化,并將其發送給 OutputStream(對象化序列是基于字節的,因要使用 InputStream 和 OutputStream 繼承層次結構)。要反向進行該過程(即將一個序列還原為一個對象),需要將一個 InputStream 封裝在 ObjectInputStream 內,然后調用 readObject()。和往常一樣,我們最后獲得的是一個引用,它指向一個向上轉型的 Object,所以必須向下轉型才能直接設置它們。
對象序列化特別“聰明”的一個地方是它不僅保存了對象的“全景圖”,而且能追蹤對象內所包含的所有引用,并保存那些對象;接著又能對對象內包含的每個這樣的引用進行追蹤,依此類推。這種情況有時被稱為“對象網”,單個對象可與之建立連接,而且它還包含了對象的引用數組以及成員對象。如果必須保持一套自己的對象序列化機制,那么維護那些可追蹤到所有鏈接的代碼可能會顯得非常麻煩。然而,由于 Java 的對象序列化似乎找不出什么缺點,所以請盡量不要自己動手,讓它用優化的算法自動維護整個對象網。下面這個例子通過對鏈接的對象生成一個 worm(蠕蟲)對序列化機制進行了測試。每個對象都與 worm 中的下一段鏈接,同時又與屬于不同類(Data)的對象引用數組鏈接:
```java
// serialization/Worm.java
// Demonstrates object serialization
import java.io.*;
import java.util.*;
class Data implements Serializable {
private int n;
Data(int n) { this.n = n; }
@Override
public String toString() {
return Integer.toString(n);
}
}
public class Worm implements Serializable {
private static Random rand = new Random(47);
private Data[] d = {
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10))
};
private Worm next;
private char c;
// Value of i == number of segments
public Worm(int i, char x) {
System.out.println("Worm constructor: " + i);
c = x;
if(--i > 0)
next = new Worm(i, (char)(x + 1));
}
public Worm() {
System.out.println("No-arg constructor");
}
@Override
public String toString() {
StringBuilder result = new StringBuilder(":");
result.append(c);
result.append("(");
for(Data dat : d)
result.append(dat);
result.append(")");
if(next != null)
result.append(next);
return result.toString();
}
public static void
main(String[] args) throws ClassNotFoundException,
IOException {
Worm w = new Worm(6, 'a');
System.out.println("w = " + w);
try(
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("worm.dat"))
) {
out.writeObject("Worm storage\n");
out.writeObject(w);
}
try(
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("worm.dat"))
) {
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
System.out.println(s + "w2 = " + w2);
}
try(
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
ObjectOutputStream out2 =
new ObjectOutputStream(bout)
) {
out2.writeObject("Worm storage\n");
out2.writeObject(w);
out2.flush();
try(
ObjectInputStream in2 = new ObjectInputStream(
new ByteArrayInputStream(
bout.toByteArray()))
) {
String s = (String)in2.readObject();
Worm w3 = (Worm)in2.readObject();
System.out.println(s + "w3 = " + w3);
}
}
}
}
```
輸出為:
```
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w2 = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w3 = :a(853):b(119):c(802):d(788):e(199):f(881)
```
更有趣的是,Worm 內的 Data 對象數組是用隨機數初始化的(這樣就不用懷疑編譯器保留了某種原始信息),每個 Worm 段都用一個 char 加以標記。該 char 是在遞歸生成鏈接的 Worm 列表時自動產生的。要創建一個 Worm,必須告訴構造器你所希望的它的長度。在產生下一個引用時,要調用 Worm 構造器,并將長度減 1,以此類推。最后一個 next 引用則為 null(空),表示已到達 Worm 的尾部
以上這些操作都使得事情變得更加復雜,從而加大了對象序列化的難度。然而,真正的序列化過程卻是非常簡單的。一旦從另外某個流創建了 ObjectOutputstream,writeObject() 就會將對象序列化。注意也可以為一個 String 調用 writeObject() 也可以用與 DataOutputStream 相同的方法寫人所有基本數據類型(它們具有同樣的接口)。
有兩段看起來相似的獨立的代碼。一個讀寫的是文件,而另一個讀寫的是字節數組(ByteArray),可利用序列化將對象讀寫到任何 DatalnputStream 或者 DataOutputStream。
從輸出中可以看出,被還原后的對象確實包含了原對象中的所有鏈接。
注意在對一個 Serializable 對象進行還原的過程中,沒有調用任何構造器,包括默認的構造器。整個對象都是通過從 InputStream 中取得數據恢復而來的。
<!-- Finding the Class -->
## 查找類
你或許會奇怪,將一個對象從它的序列化狀態中恢復出來,有哪些工作是必須的呢?舉個例子來說,假如我們將一個對象序列化,并通過網絡將其作為文件傳送給另一臺計算機,那么,另一臺計算機上的程序可以只利用該文件內容來還原這個對象嗎?
回答這個問題的最好方法就是做一個實驗。下面這個文件位于本章的子目錄下:
```java
// serialization/Alien.java
// A serializable class
import java.io.*;
public class Alien implements Serializable {}
```
而用于創建和序列化一個 Alien 對象的文件也位于相同的目錄下:
```java
// serialization/FreezeAlien.java
// Create a serialized output file
import java.io.*;
public class FreezeAlien {
public static void main(String[] args) throws Exception {
try(
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("X.file"));
) {
Alien quellek = new Alien();
out.writeObject(quellek);
}
}
}
```
一旦該程序被編譯和運行,它就會在 c12 目錄下產生一個名為 X.file 的文件。以下代碼位于一個名為 xiles 的子目錄下:
```java
// serialization/xfiles/ThawAlien.java
// Recover a serialized file
// {java serialization.xfiles.ThawAlien}
// {RunFirst: FreezeAlien}
package serialization.xfiles;
import java.io.*;
public class ThawAlien {
public static void main(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(new File("X.file")));
Object mystery = in.readObject();
System.out.println(mystery.getClass());
}
}
```
輸出為:
```java
class Alien
```
為了正常運行,必須保證 Java 虛擬機能找到相關的.class 文件。
<!-- Controlling Serialization -->
## 控制序列化
正如大家所看到的,默認的序列化機制并不難操縱。然而,如果有特殊的需要那又該怎么辦呢?例如,也許要考慮特殊的安全問題,而且你不希望對象的某一部分被序列化;或者一個對象被還原以后,某子對象需要重新創建,從而不必將該子對象序列化。
在這些特殊情況下,可通過實現 Externalizable 接口——代替實現 Serializable 接口-來對序列化過程進行控制。這個 Externalizable 接口繼承了 Serializable 接口,同時增添了兩個方法:writeExternal0 和 readExternal0。這兩個方法會在序列化和反序列化還原的過程中被自動調用,以便執行一些特殊操作。
下面這個例子展示了 Externalizable 接口方法的簡單實現。注意 Blip1 和 Blip2 除了細微的差別之外,幾乎完全一致(研究一下代碼,看看你能否發現):
```java
// serialization/Blips.java
// Simple use of Externalizable & a pitfall
import java.io.*;
class Blip1 implements Externalizable {
public Blip1() {
System.out.println("Blip1 Constructor");
}
@Override
public void writeExternal(ObjectOutput out)
throws IOException {
System.out.println("Blip1.writeExternal");
}
@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
System.out.println("Blip1.readExternal");
}
}
class Blip2 implements Externalizable {
Blip2() {
System.out.println("Blip2 Constructor");
}
@Override
public void writeExternal(ObjectOutput out)
throws IOException {
System.out.println("Blip2.writeExternal");
}
@Override
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.serialized"))
) {
System.out.println("Saving objects:");
o.writeObject(b1);
o.writeObject(b2);
} catch(IOException e) {
throw new RuntimeException(e);
}
// Now get them back:
System.out.println("Recovering b1:");
try(
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("Blips.serialized"))
) {
b1 = (Blip1)in.readObject();
} catch(IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
// OOPS! Throws an exception:
//- System.out.println("Recovering b2:");
//- b2 = (Blip2)in.readObject();
}
}
```
輸出為:
```
Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Constructor
Blip1.readExternal
```
沒有恢復 Blip2 對象的原因是那樣做會導致一個異常。你找出 Blip1 和 Blip2 之間的區別了嗎?Blipl 的構造器是“公共的”(public),Blip2 的構造器卻不是,這樣就會在恢復時造成異常。試試將 Blip2 的構造器變成 public 的,然后刪除//注釋標記,看看是否能得到正確的結果。
恢復 b1 后,會調用 Blip1 默認構造器。這與恢復一個 Serializable 對象不同。對于 Serializable 對象,對象完全以它存儲的二進制位為基礎來構造,而不調用構造器。而對于一個 Externalizable 對象,所有普通的默認構造器都會被調用(包括在字段定義時的初始化),然后調用 readExternal() 必須注意這一點--所有默認的構造器都會被調用,才能使 Externalizable 對象產生正確的行為。
下面這個例子示范了如何完整保存和恢復一個 Externalizable 對象:
```java
// serialization/Blip3.java
// Reconstructing an externalizable object
import java.io.*;
public class Blip3 implements Externalizable {
private int i;
private 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-no-arg constructor.
}
@Override
public String toString() { return s + i; }
@Override
public void writeExternal(ObjectOutput out)
throws IOException {
System.out.println("Blip3.writeExternal");
// You must do this:
out.writeObject(s);
out.writeInt(i);
}
@Override
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);
try(
ObjectOutputStream o = new ObjectOutputStream(
new FileOutputStream("Blip3.serialized"))
) {
System.out.println("Saving object:");
o.writeObject(b3);
} catch(IOException e) {
throw new RuntimeException(e);
}
// Now get it back:
System.out.println("Recovering b3:");
try(
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("Blip3.serialized"))
) {
b3 = (Blip3)in.readObject();
} catch(IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
System.out.println(b3);
}
}
```
輸出為:
```
Constructing objects:
Blip3(String x, int a)
A String 47
Saving object:
Blip3.writeExternal
Recovering b3:
Blip3 Constructor
Blip3.readExternal
A String 47
```
其中,字段 s 和只在第二個構造器中初始化,而不是在默認的構造器中初始化。這意味著假如不在 readExternal0 中初始化 s 和 i,s 就會為 null,而就會為零(因為在創建對象的第一步中將對象的存儲空間清理為 0)。如果注釋掉跟隨于"You must do this”后面的兩行代碼,然后運行程序,就會發現當對象被還原后,s 是 null,而 i 是零。
我們如果從一個 Externalizable 對象繼承,通常需要調用基類版本的 writeExternal() 和 readExternal() 來為基類組件提供恰當的存儲和恢復功能。
因此,為了正常運行,我們不僅需要在 writeExternal() 方法(沒有任何默認行為來為 Externalizable 對象寫入任何成員對象)中將來自對象的重要信息寫入,還必須在 readExternal() 方法中恢復數據。起先,可能會有一點迷惑,因為 Externalizable 對象的默認構造行為使其看起來似乎像某種自動發生的存儲與恢復操作。但實際上并非如此。
### transient 關鍵字
當我們對序列化進行控制時,可能某個特定子對象不想讓 Java 的序列化機制自動保存與恢復。如果子對象表示的是我們不希望將其序列化的敏感信息(如密碼),通常就會面臨這種情況。即使對象中的這些信息是 private(私有)屬性,一經序列化處理,人們就可以通過讀取文件或者攔截網絡傳輸的方式來訪問到它。
有一種辦法可防止對象的敏感部分被序列化,就是將類實現為 Externalizable,如前面所示。這樣一來,沒有任何東西可以自動序列化,并且可以在 writeExternal() 內部只對所需部分進行顯式的序列化。
然而,如果我們正在操作的是一個 Seralizable 對象,那么所有序列化操作都會自動進行。為了能夠予以控制,可以用 transient(瞬時)關鍵字逐個字段地關閉序列化,它的意思是“不用麻煩你保存或恢復數據——我自己會處理的"。
例如,假設某個 Logon 對象保存某個特定的登錄會話信息,登錄的合法性通過校驗之后,我們想把數據保存下來,但不包括密碼。為做到這一點,最簡單的辦法是實現 Serializable,并將 password 字段標志為 transient,下面是具體的代碼:
```java
// serialization/Logon.java
// Demonstrates the "transient" keyword
import java.util.concurrent.*;
import java.io.*;
import java.util.*;
import onjava.Nap;
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;
}
@Override
public String toString() {
return "logon info: \n username: " +
username + "\n date: " + date +
"\n password: " + password;
}
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.dat"))
) {
o.writeObject(a);
} catch(IOException e) {
throw new RuntimeException(e);
}
new Nap(1);
// Now get them back:
try(
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("Logon.dat"))
) {
System.out.println(
"Recovering object at " + new Date());
a = (Logon)in.readObject();
} catch(IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
System.out.println("logon a = " + a);
}
}
```
輸出為:
```
logon a = logon info:
username: Hulk
date: Tue May 09 06:07:47 MDT 2017
password: myLittlePony
Recovering object at Tue May 09 06:07:49 MDT 2017
logon a = logon info:
username: Hulk
date: Tue May 09 06:07:47 MDT 2017
password: null
```
可以看到,其中的 date 和 username 是一般的(不是 transient 的),所以它們會被自動序列化。而 password 是 transient 的,所以不會被自動保存到磁盤;另外,自動序列化機制也不會嘗試去恢復它。當對象被恢復時,password 就會變成 null。注意,雖然 toString() 是用重載后的+運算符來連接 String 對象,但是 null 引用會被自動轉換成字符串 null。
我們還可以發現:date 字段被存儲了到磁盤并從磁盤上被恢復了出來,而且沒有再重新生成。由于 Externalizable 對象在默認情況下不保存它們的任何字段,所以 transient 關鍵字只能和 Serializable 對象一起使用。
### Externalizable 的替代方法
如果不是特別堅持實現 Externalizable 接口,那么還有另一種方法。我們可以實現 Serializable 接口,并添加(注意我說的是“添加”,而非“覆蓋”或者“實現”)名為 writeObject() 和 readObject() 的方法。這樣一旦對象被序列化或者被反序列化還原,就會自動地分別調用這兩個方法。也就是說,只要我們提供了這兩個方法,就會使用它們而不是默認的序列化機制。
這些方法必須具有準確的方法特征簽名:
```java
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 的,那么它們不會是接口的一部分。因為我們必須要完全遵循其方法特征簽名,所以其效果就和實現了接口一樣。
在調用 ObjectOutputStream.writeObject() 時,會檢查所傳遞的 Serializable 對象,看看是否實現了它自己的 writeObject()。如果是這樣,就跳過正常的序列化過程并調用它的 writeObiect()。readObject() 的情形與此相同。
還有另外一個技巧。在你的 writeObject() 內部,可以調用 defaultWriteObject() 來選擇執行默認的 writeObject()。類似地,在 readObject() 內部,我們可以調用 defaultReadObject(),下面這個簡單的例子演示了如何對一個 Serializable 對象的存儲與恢復進行控制:
```java
// serialization/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;
}
@Override
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);
try (
ByteArrayOutputStream buf =
new ByteArrayOutputStream();
ObjectOutputStream o =
new ObjectOutputStream(buf);
) {
o.writeObject(sc);
// Now get it back:
try (
ObjectInputStream in =
new ObjectInputStream(
new ByteArrayInputStream(
buf.toByteArray()));
) {
SerialCtl sc2 = (SerialCtl)in.readObject();
System.out.println("After:\n" + sc2);
}
} catch(IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
```
輸出為:
```
Before:
Not Transient: Test1
Transient: Test2
After:
Not Transient: Test1
Transient: Test2
```
在這個例子中,有一個 String 字段是普通字段,而另一個是 transient 字段,用來證明非 transient 字段由 defaultWriteObject() 方法保存,而 transient 字段必須在程序中明確保存和恢復。字段是在構造器內部而不是在定義處進行初始化的,以此可以證實它們在反序列化還原期間沒有被一些自動化機制初始化。
如果我們打算使用默認機制寫入對象的非 transient 部分,那么必須調用 defaultwriteObject() 作為 writeObject() 中的第一個操作,并讓 defaultReadObject() 作為 readObject() 中的第一個操作。這些都是奇怪的方法調用。例如,如果我們正在為 ObjectOutputStream 調用 defaultWriteObject() 且沒有傳遞任何參數,然而不知何故它卻可以運行,并且知道對象的引用以及如何寫入非 transient 部分。真是奇怪之極。
對 transient 對象的存儲和恢復使用了我們比較熟悉的代碼。請再考慮一下在這里所發生的事情。在 main0)中,創建 SerialCtl 對象,然后將其序列化到 ObjectOutputStream(注意在這種情況下,使用的是緩沖區而不是文件-這對于 ObjectOutputStream 來說是完全一樣的)。序列化發生在下面這行代碼當中
```java
o.writeObject(sc);
```
writeObject() 方法必須檢查 sc,判斷它是否擁有自己的 writeObject() 方法(不是檢查接口——這里根本就沒有接口,也不是檢查類的類型,而是利用反射來真正地搜索方法)。如果有,那么就會使用它。對 readObject() 也采用了類似的方法。或許這是解決這個問題的唯一切實可行的方法,但它確實有點古怪。
### 版本控制
有時可能想要改變可序列化類的版本(比如源類的對象可能保存在數據庫中)。雖然 Java 支持這種做法,但是你可能只在特殊的情況下才這樣做,此外,還需要對它有相當深程度的了解(在這里我們就不再試圖達到這一點)。從 http://java.oracle.com 下的 JDK 文檔中對這一主題進行了非常徹底的論述。
<!-- Using Persistence -->
## 使用持久化
個比較誘人的使用序列化技術的想法是:存儲程序的一些狀態,以便我們隨后可以很容易地將程序恢復到當前狀態。但是在我們能夠這樣做之前,必須回答幾個問題。如果我們將兩個對象-它們都具有指向第三個對象的引用-進行序列化,會發生什么情況?當我們從它們的序列化狀態恢復這兩個對象時,第三個對象會只出現一次嗎?如果將這兩個對象序列化成獨立的文件,然后在代碼的不同部分對它們進行反序列化還原,又會怎樣呢?
下面這個例子說明了上述問題:
```java
// serialization/MyWorld.java
import java.io.*;
import java.util.*;
class House implements Serializable {}
class Animal implements Serializable {
private String name;
private House preferredHouse;
Animal(String nm, House h) {
name = nm;
preferredHouse = h;
}
@Override
public String toString() {
return name + "[" + super.toString() +
"], " + preferredHouse + "\n";
}
}
public class MyWorld {
public static void main(String[] args) {
House house = new House();
List<Animal> animals = new ArrayList<>();
animals.add(
new Animal("Bosco the dog", house));
animals.add(
new Animal("Ralph the hamster", house));
animals.add(
new Animal("Molly 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:
try(
ByteArrayOutputStream buf2 = new ByteArrayOutputStream();
ObjectOutputStream o2 = new ObjectOutputStream(buf2)
) {
o2.writeObject(animals);
// Now get them back:
try(
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();
System.out.println(
"animals1: " + animals1);
System.out.println(
"animals2: " + animals2);
System.out.println(
"animals3: " + animals3);
}
}
} catch(IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
```
輸出為:
```
animals: [Bosco the dog[Animal@15db9742],
House@6d06d69c
, Ralph the hamster[Animal@7852e922], House@6d06d69c
, Molly the cat[Animal@4e25154f], House@6d06d69c
]
animals1: [Bosco the dog[Animal@7ba4f24f],
House@3b9a45b3
, Ralph the hamster[Animal@7699a589], House@3b9a45b3
, Molly the cat[Animal@58372a00], House@3b9a45b3
]
animals2: [Bosco the dog[Animal@7ba4f24f],
House@3b9a45b3
, Ralph the hamster[Animal@7699a589], House@3b9a45b3
, Molly the cat[Animal@58372a00], House@3b9a45b3
]
animals3: [Bosco the dog[Animal@4dd8dc3],
House@6d03e736
, Ralph the hamster[Animal@568db2f2], House@6d03e736
, Molly the cat[Animal@378bf509], House@6d03e736
]
```
這里有一件有趣的事:我們可以通過一個字節數組來使用對象序列化,從而實現對任何可 Serializable 對象的“深度復制"(deep copy)—— 深度復制意味著我們復制的是整個對象網,而不僅僅是基本對象及其引用。復制對象將在本書的 [附錄:傳遞和返回對象 ]() 一章中進行深入地探討。
在這個例子中,Animal 對象包含有 House 類型的字段。在 main() 方法中,創建了一個 Animal 列表并將其兩次序列化,分別送至不同的流。當其被反序列化還原并被打印時,我們可以看到所示的執行某次運行后的結果(每次運行時,對象將會處在不同的內存地址)。
當然,我們期望這些反序列化還原后的對象地址與原來的地址不同。但請注意,在 animals1 和 animals2 中卻出現了相同的地址,包括二者共享的那個指向 House 對象的引用。另一方面,當恢復 animals3 時,系統無法知道另一個流內的對象是第一個流內的對象的別名,因此它會產生出完全不同的對象網。
只要將任何對象序列化到單一流中,就可以恢復出與我們寫出時一樣的對象網,并且沒有任何意外重復復制出的對象。當然,我們可以在寫出第一個對象和寫出最后一個對象期間改變這些對象的狀態,但是這是我們自己的事,無論對象在被序列化時處于什么狀態(無論它們和其他對象有什么樣的連接關系),它們都可以被寫出。
最安全的做法是將其作為“原子”操作進行序列化。如果我們序列化了某些東西,再去做其他一些工作,再來序列化更多的東西,如此等等,那么將無法安全地保存系統狀態。取而代之的是,將構成系統狀態的所有對象都置入單一容器內,并在一個操作中將該容器直接寫出。然后同樣只需一次方法調用,即可以將其恢復。
下面這個例子是一個想象的計算機輔助設計(CAD)系統,該例演示了這一方法。此外,它還引入了 static 字段的問題:如果我們查看 JDK 文檔,就會發現 Class 是 Serializable 的,因此只需直接對 Class 對象序列化,就可以很容易地保存 static 字段。在任何情況下,這都是一種明智的做法。
```java
// serialization/AStoreCADState.java
// Saving the state of a fictitious CAD system
import java.io.*;
import java.util.*;
import java.util.stream.*;
enum Color { RED, BLUE, GREEN }
abstract class Shape implements Serializable {
private int xPos, yPos, dimension;
private static Random rand = new Random(47);
private static int counter = 0;
public abstract void setColor(Color newColor);
public abstract Color getColor();
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 Color color = Color.RED;
Circle(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(Color newColor) {
color = newColor;
}
public Color getColor() { return color; }
}
class Square extends Shape {
private static Color color = Color.RED;
Square(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(Color newColor) {
color = newColor;
}
public Color getColor() { return color; }
}
class Line extends Shape {
private static Color color = Color.RED;
public static void
serializeStaticState(ObjectOutputStream os)
throws IOException { os.writeObject(color); }
public static void
deserializeStaticState(ObjectInputStream os)
throws IOException, ClassNotFoundException {
color = (Color)os.readObject();
}
Line(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(Color newColor) {
color = newColor;
}
public Color getColor() { return color; }
}
public class AStoreCADState {
public static void main(String[] args) {
List<Class<? extends Shape>> shapeTypes =
Arrays.asList(
Circle.class, Square.class, Line.class);
List<Shape> shapes = IntStream.range(0, 10)
.mapToObj(i -> Shape.randomFactory())
.collect(Collectors.toList());
// Set all the static colors to GREEN:
shapes.forEach(s -> s.setColor(Color.GREEN));
// Save the state vector:
try(
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("CADState.dat"))
) {
out.writeObject(shapeTypes);
Line.serializeStaticState(out);
out.writeObject(shapes);
} catch(IOException e) {
throw new RuntimeException(e);
}
// Display the shapes:
System.out.println(shapes);
}
}
```
輸出為:
```java
[class Circlecolor[GREEN] xPos[58] yPos[55] dim[93]
, class Squarecolor[GREEN] xPos[61] yPos[61] dim[29]
, class Linecolor[GREEN] xPos[68] yPos[0] dim[22]
, class Circlecolor[GREEN] xPos[7] yPos[88] dim[28]
, class Squarecolor[GREEN] xPos[51] yPos[89] dim[9]
, class Linecolor[GREEN] xPos[78] yPos[98] dim[61]
, class Circlecolor[GREEN] xPos[20] yPos[58] dim[16]
, class Squarecolor[GREEN] xPos[40] yPos[11] dim[22]
, class Linecolor[GREEN] xPos[4] yPos[83] dim[6]
, class Circlecolor[GREEN] xPos[75] yPos[10] dim[42]
]
```
Shape 類實現了 Serializable,所以任何自 Shape 繼承的類也都會自動是 Serializable 的。每個 Shape 都含有數據,而且每個派生自 Shape 的類都包含一個 static 字段,用來確定各種 Shape 類型的顏色(如果將 static 字段置入基類,只會產生一個 static 字段,因為 static 字段不能在派生類中復制)。可對基類中的方法進行重載,以便為不同的類型設置顏色(static 方法不會動態綁定,所以這些都是普通的方法)。每次調用 randomFactory() 方法時,它都會使用不同的隨機數作為 Shape 的數據,從而創建不同的 Shape。
在 main() 中,一個 ArrayList 用于保存 Class 對象,而另一個用于保存幾何形狀。
恢復對象相當直觀:
```java
// serialization/RecoverCADState.java
// Restoring the state of the fictitious CAD system
// {RunFirst: AStoreCADState}
import java.io.*;
import java.util.*;
public class RecoverCADState {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try(
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("CADState.dat"))
) {
// Read in the same order they were written:
List<Class<? extends Shape>> shapeTypes =
(List<Class<? extends Shape>>)in.readObject();
Line.deserializeStaticState(in);
List<Shape> shapes =
(List<Shape>)in.readObject();
System.out.println(shapes);
} catch(IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
```
輸出為:
```java
[class Circlecolor[RED] xPos[58] yPos[55] dim[93]
, class Squarecolor[RED] xPos[61] yPos[61] dim[29]
, class Linecolor[GREEN] xPos[68] yPos[0] dim[22]
, class Circlecolor[RED] xPos[7] yPos[88] dim[28]
, class Squarecolor[RED] xPos[51] yPos[89] dim[9]
, class Linecolor[GREEN] xPos[78] yPos[98] dim[61]
, class Circlecolor[RED] xPos[20] yPos[58] dim[16]
, class Squarecolor[RED] xPos[40] yPos[11] dim[22]
, class Linecolor[GREEN] xPos[4] yPos[83] dim[6]
, class Circlecolor[RED] xPos[75] yPos[10] dim[42]
]
```
可以看到,xPos,yPos 以及 dim 的值都被成功地保存和恢復了,但是對 static 信息的讀取卻出現了問題。所有讀回的顏色應該都是“3”,但是真實情況卻并非如此。Circle 的值為 1(定義為 RED),而 Square 的值為 0(記住,它們是在構造器中被初始化的)。看上去似乎 static 數據根本沒有被序列化!確實如此——盡管 Class 類是 Serializable 的,但它卻不能按我們所期望的方式運行。所以假如想序列化 static 值,必須自己動手去實現。
這正是 Line 中的 serializeStaticState() 和 deserializeStaticState() 兩個 static 方法的用途。可以看到,它們是作為存儲和讀取過程的一部分被顯式地調用的。(注意必須維護寫入序列化文件和從該文件中讀回的順序。)因此,為了使 CADStatejava 正確運轉起來,你必須:
1. 為幾何形狀添加 serializeStaticState() 和 deserializeStaticState()
2. 移除 ArrayList shapeTypes 以及與之有關的所有代碼。
3. 在幾何形狀內添加對新的序列化和反序列化還原靜態方法的調用。
另一個要注意的問題是安全,因為序列化也會將 private 數據保存下來。如果你關心安全問題,那么應將其標記成 transient,但是這之后,還必須設計一種安全的保存信息的方法,以便在執行恢復時可以復位那些 private 變量。
## XML
對象序列化的一個重要限制是它只是 Java 的解決方案:只有 Java 程序才能反序列化這種對象。一種更具互操作性的解決方案是將數據轉換為 XML 格式,這可以使其被各種各樣的平臺和語言使用。
因為 XML 十分流行,所以用它來編程時的各種選擇不勝枚舉,包括隨 JDK 發布的 javax.xml.*類庫。我選擇使用 Elliotte Rusty Harold 的開源 XOM 類庫(可從 www.xom.nu 下載并獲得文檔),因為它看起來最簡單,同時也是最直觀的用 Java 產生和修改 XML 的方式。另外,XOM 還強調了 XML 的正確性。
作為一個示例,假設有一個 APerson 對象,它包含姓和名,你想將它們序列化到 XML 中。下面的 APerson 類有一個 getXML() 方法,它使用 XOM 來產生被轉換為 XML 的 Element 對象的 APerson 數據;還有一個構造器,接受 Element 并從中抽取恰當的 APerson 數據(注意,XML 示例都在它們自己的子目錄中):
```java
// serialization/APerson.java
// Use the XOM library to write and read XML
// nu.xom.Node comes from http://www.xom.nu
import nu.xom.*;
import java.io.*;
import java.util.*;
public class APerson {
private String first, last;
public APerson(String first, String last) {
this.first = first;
this.last = last;
}
// Produce an XML Element from this APerson object:
public Element getXML() {
Element person = new Element("person");
Element firstName = new Element("first");
firstName.appendChild(first);
Element lastName = new Element("last");
lastName.appendChild(last);
person.appendChild(firstName);
person.appendChild(lastName);
return person;
}
// Constructor restores a APerson from XML:
public APerson(Element person) {
first = person
.getFirstChildElement("first").getValue();
last = person
.getFirstChildElement("last").getValue();
}
@Override
public String toString() {
return first + " " + last;
}
// Make it human-readable:
public static void
format(OutputStream os, Document doc)
throws Exception {
Serializer serializer =
new Serializer(os,"ISO-8859-1");
serializer.setIndent(4);
serializer.setMaxLength(60);
serializer.write(doc);
serializer.flush();
}
public static void main(String[] args) throws Exception {
List<APerson> people = Arrays.asList(
new APerson("Dr. Bunsen", "Honeydew"),
new APerson("Gonzo", "The Great"),
new APerson("Phillip J.", "Fry"));
System.out.println(people);
Element root = new Element("people");
for(APerson p : people)
root.appendChild(p.getXML());
Document doc = new Document(root);
format(System.out, doc);
format(new BufferedOutputStream(
new FileOutputStream("People.xml")), doc);
}
}
```
輸出為:
```xml
[Dr. Bunsen Honeydew, Gonzo The Great, Phillip J. Fry]
<?xml version="1.0" encoding="ISO-8859-1"?>
<people>
<person>
<first>Dr. Bunsen</first>
<last>Honeydew</last>
</person>
<person>
<first>Gonzo</first>
<last>The Great</last>
</person>
<person>
<first>Phillip J.</first>
<last>Fry</last>
</person>
</people>
```
XOM 的方法都具有相當的自解釋性,可以在 XOM 文檔中找到它們。XOM 還包含一個 Serializer 類,你可以在 format() 方法中看到它被用來將 XML 轉換為更具可讀性的格式。如果只調用 toXML(),那么所有東西都會混在一起,因此 Serializer 是一種便利工具。
從 XML 文件中反序列化 Person 對象也很簡單:
```java
// serialization/People.java
// nu.xom.Node comes from http://www.xom.nu
// {RunFirst: APerson}
import nu.xom.*;
import java.io.File;
import java.util.*;
public class People extends ArrayList<APerson> {
public People(String fileName) throws Exception {
Document doc =
new Builder().build(new File(fileName));
Elements elements =
doc.getRootElement().getChildElements();
for(int i = 0; i < elements.size(); i++)
add(new APerson(elements.get(i)));
}
public static void main(String[] args) throws Exception {
People p = new People("People.xml");
System.out.println(p);
}
}
/* Output:
[Dr. Bunsen Honeydew, Gonzo The Great, Phillip J. Fry]
*/
```
People 構造器使用 XOM 的 Builder.build() 方法打開并讀取一個文件,而 getChildElements() 方法產生了一個 Elements 列表(不是標準的 Java List,只是一個擁有 size() 和 get() 方法的對象,因為 Harold 不想強制人們使用特定版本的 Java,但是仍舊希望使用類型安全的容器)。在這個列表中的每個 Element 都表示一個 Person 對象,因此它可以傳遞給第二個 Person 構造器。注意,這要求你提前知道 XML 文件的確切結構,但是這經常會有些問題。如果文件結構與你預期的結構不匹配,那么 XOM 將拋出異常。對你來說,如果你缺乏有關將來的 XML 結構的信息,那么就有可能會編寫更復雜的代碼去探測 XML 文檔,而不是只對其做出假設。
為了獲取這些示例去編譯它們,你必須將 XOM 發布包中的 JAR 文件放置到你的類路徑中。
這里只給出了用 Java 和 XOM 類庫進行 XML 編程的簡介,更詳細的信息可以瀏覽 www.xom.nu 。
<!-- 分頁 -->
<div style="page-break-after: always;"></div>
- 譯者的話
- 前言
- 簡介
- 第一章 對象的概念
- 抽象
- 接口
- 服務提供
- 封裝
- 復用
- 繼承
- "是一個"與"像是一個"的關系
- 多態
- 單繼承結構
- 集合
- 對象創建與生命周期
- 異常處理
- 本章小結
- 第二章 安裝Java和本書用例
- 編輯器
- Shell
- Java安裝
- 校驗安裝
- 安裝和運行代碼示例
- 第三章 萬物皆對象
- 對象操縱
- 對象創建
- 數據存儲
- 基本類型的存儲
- 高精度數值
- 數組的存儲
- 代碼注釋
- 對象清理
- 作用域
- 對象作用域
- 類的創建
- 類型
- 字段
- 基本類型默認值
- 方法使用
- 返回類型
- 參數列表
- 程序編寫
- 命名可見性
- 使用其他組件
- static關鍵字
- 小試牛刀
- 編譯和運行
- 編碼風格
- 本章小結
- 第四章 運算符
- 開始使用
- 優先級
- 賦值
- 方法調用中的別名現象
- 算術運算符
- 一元加減運算符
- 遞增和遞減
- 關系運算符
- 測試對象等價
- 邏輯運算符
- 短路
- 字面值常量
- 下劃線
- 指數計數法
- 位運算符
- 移位運算符
- 三元運算符
- 字符串運算符
- 常見陷阱
- 類型轉換
- 截斷和舍入
- 類型提升
- Java沒有sizeof
- 運算符總結
- 本章小結
- 第五章 控制流
- true和false
- if-else
- 迭代語句
- while
- do-while
- for
- 逗號操作符
- for-in 語法
- return
- break 和 continue
- 臭名昭著的 goto
- switch
- switch 字符串
- 本章小結
- 第六章 初始化和清理
- 利用構造器保證初始化
- 方法重載
- 區分重載方法
- 重載與基本類型
- 返回值的重載
- 無參構造器
- this關鍵字
- 在構造器中調用構造器
- static 的含義
- 垃圾回收器
- finalize()的用途
- 你必須實施清理
- 終結條件
- 垃圾回收器如何工作
- 成員初始化
- 指定初始化
- 構造器初始化
- 初始化的順序
- 靜態數據的初始化
- 顯式的靜態初始化
- 非靜態實例初始化
- 數組初始化
- 動態數組創建
- 可變參數列表
- 枚舉類型
- 本章小結
- 第七章 封裝
- 包的概念
- 代碼組織
- 創建獨一無二的包名
- 沖突
- 定制工具庫
- 使用 import 改變行為
- 使用包的忠告
- 訪問權限修飾符
- 包訪問權限
- public: 接口訪問權限
- 默認包
- private: 你無法訪問
- protected: 繼承訪問權限
- 包訪問權限 Vs Public 構造器
- 接口和實現
- 類訪問權限
- 本章小結
- 第八章 復用
- 組合語法
- 繼承語法
- 初始化基類
- 帶參數的構造函數
- 委托
- 結合組合與繼承
- 保證適當的清理
- 名稱隱藏
- 組合與繼承的選擇
- protected
- 向上轉型
- 再論組合和繼承
- final關鍵字
- final 數據
- 空白 final
- final 參數
- final 方法
- final 和 private
- final 類
- final 忠告
- 類初始化和加載
- 繼承和初始化
- 本章小結
- 第九章 多態
- 向上轉型回顧
- 忘掉對象類型
- 轉機
- 方法調用綁定
- 產生正確的行為
- 可擴展性
- 陷阱:“重寫”私有方法
- 陷阱:屬性與靜態方法
- 構造器和多態
- 構造器調用順序
- 繼承和清理
- 構造器內部多態方法的行為
- 協變返回類型
- 使用繼承設計
- 替代 vs 擴展
- 向下轉型與運行時類型信息
- 本章小結
- 第十章 接口
- 抽象類和方法
- 接口創建
- 默認方法
- 多繼承
- 接口中的靜態方法
- Instrument 作為接口
- 抽象類和接口
- 完全解耦
- 多接口結合
- 使用繼承擴展接口
- 結合接口時的命名沖突
- 接口適配
- 接口字段
- 初始化接口中的字段
- 接口嵌套
- 接口和工廠方法模式
- 本章小結
- 第十一章 內部類
- 創建內部類
- 鏈接外部類
- 使用 .this 和 .new
- 內部類與向上轉型
- 內部類方法和作用域
- 匿名內部類
- 嵌套類
- 接口內部的類
- 從多層嵌套類中訪問外部類的成員
- 為什么需要內部類
- 閉包與回調
- 內部類與控制框架
- 繼承內部類
- 內部類可以被覆蓋么?
- 局部內部類
- 內部類標識符
- 本章小結
- 第十二章 集合
- 泛型和類型安全的集合
- 基本概念
- 添加元素組
- 集合的打印
- 迭代器Iterators
- ListIterator
- 鏈表LinkedList
- 堆棧Stack
- 集合Set
- 映射Map
- 隊列Queue
- 優先級隊列PriorityQueue
- 集合與迭代器
- for-in和迭代器
- 適配器方法慣用法
- 本章小結
- 簡單集合分類
- 第十三章 函數式編程
- 新舊對比
- Lambda表達式
- 遞歸
- 方法引用
- Runnable接口
- 未綁定的方法引用
- 構造函數引用
- 函數式接口
- 多參數函數式接口
- 缺少基本類型的函數
- 高階函數
- 閉包
- 作為閉包的內部類
- 函數組合
- 柯里化和部分求值
- 純函數式編程
- 本章小結
- 第十四章 流式編程
- 流支持
- 流創建
- 隨機數流
- int 類型的范圍
- generate()
- iterate()
- 流的建造者模式
- Arrays
- 正則表達式
- 中間操作
- 跟蹤和調試
- 流元素排序
- 移除元素
- 應用函數到元素
- 在map()中組合流
- Optional類
- 便利函數
- 創建 Optional
- Optional 對象操作
- Optional 流
- 終端操作
- 數組
- 集合
- 組合
- 匹配
- 查找
- 信息
- 數字流信息
- 本章小結
- 第十五章 異常
- 異常概念
- 基本異常
- 異常參數
- 異常捕獲
- try 語句塊
- 異常處理程序
- 終止與恢復
- 自定義異常
- 異常與記錄日志
- 異常聲明
- 捕獲所有異常
- 多重捕獲
- 棧軌跡
- 重新拋出異常
- 精準的重新拋出異常
- 異常鏈
- Java 標準異常
- 特例:RuntimeException
- 使用 finally 進行清理
- finally 用來做什么?
- 在 return 中使用 finally
- 缺憾:異常丟失
- 異常限制
- 構造器
- Try-With-Resources 用法
- 揭示細節
- 異常匹配
- 其他可選方式
- 歷史
- 觀點
- 把異常傳遞給控制臺
- 把“被檢查的異常”轉換為“不檢查的異常”
- 異常指南
- 本章小結
- 后記:Exception Bizarro World
- 第十六章 代碼校驗
- 測試
- 如果沒有測試過,它就是不能工作的
- 單元測試
- JUnit
- 測試覆蓋率的幻覺
- 前置條件
- 斷言(Assertions)
- Java 斷言語法
- Guava斷言
- 使用斷言進行契約式設計
- 檢查指令
- 前置條件
- 后置條件
- 不變性
- 放松 DbC 檢查或非嚴格的 DbC
- DbC + 單元測試
- 使用Guava前置條件
- 測試驅動開發
- 測試驅動 vs. 測試優先
- 日志
- 日志會給出正在運行的程序的各種信息
- 日志等級
- 調試
- 使用 JDB 調試
- 圖形化調試器
- 基準測試
- 微基準測試
- JMH 的引入
- 剖析和優化
- 優化準則
- 風格檢測
- 靜態錯誤分析
- 代碼重審
- 結對編程
- 重構
- 重構基石
- 持續集成
- 本章小結
- 第十七章 文件
- 文件和目錄路徑
- 選取路徑部分片段
- 路徑分析
- Paths的增減修改
- 目錄
- 文件系統
- 路徑監聽
- 文件查找
- 文件讀寫
- 本章小結
- 第十八章 字符串
- 字符串的不可變
- +的重載與StringBuilder
- 意外遞歸
- 字符串操作
- 格式化輸出
- printf()
- System.out.format()
- Formatter類
- 格式化修飾符
- Formatter轉換
- String.format()
- 一個十六進制轉儲(dump)工具
- 正則表達式
- 基礎
- 創建正則表達式
- 量詞
- CharSequence
- Pattern和Matcher
- find()
- 組(Groups)
- start()和end()
- Pattern標記
- split()
- 替換操作
- 正則表達式與 Java I/O
- 掃描輸入
- Scanner分隔符
- 用正則表達式掃描
- StringTokenizer類
- 本章小結
- 第十九章 類型信息
- 為什么需要 RTTI
- Class對象
- 類字面常量
- 泛化的Class引用
- cast()方法
- 類型轉換檢測
- 使用類字面量
- 遞歸計數
- 一個動態instanceof函數
- 注冊工廠
- 類的等價比較
- 反射:運行時類信息
- 類方法提取器
- 動態代理
- Optional類
- 標記接口
- Mock 對象和樁
- 接口和類型
- 本章小結
- 第二十章 泛型
- 簡單泛型
- 泛型接口
- 泛型方法
- 復雜模型構建
- 泛型擦除
- 補償擦除
- 邊界
- 通配符
- 問題
- 自限定的類型
- 動態類型安全
- 泛型異常
- 混型
- 潛在類型機制
- 對缺乏潛在類型機制的補償
- Java8 中的輔助潛在類型
- 總結:類型轉換真的如此之糟嗎?
- 進階閱讀
- 第二十一章 數組
- 數組特性
- 一等對象
- 返回數組
- 多維數組
- 泛型數組
- Arrays的fill方法
- Arrays的setAll方法
- 增量生成
- 隨機生成
- 泛型和基本數組
- 數組元素修改
- 數組并行
- Arrays工具類
- 數組比較
- 數組拷貝
- 流和數組
- 數組排序
- Arrays.sort()的使用
- 并行排序
- binarySearch二分查找
- parallelPrefix并行前綴
- 本章小結
- 第二十二章 枚舉
- 基本 enum 特性
- 將靜態類型導入用于 enum
- 方法添加
- 覆蓋 enum 的方法
- switch 語句中的 enum
- values 方法的神秘之處
- 實現而非繼承
- 隨機選擇
- 使用接口組織枚舉
- 使用 EnumSet 替代 Flags
- 使用 EnumMap
- 常量特定方法
- 使用 enum 的職責鏈
- 使用 enum 的狀態機
- 多路分發
- 使用 enum 分發
- 使用常量相關的方法
- 使用 EnumMap 進行分發
- 使用二維數組
- 本章小結
- 第二十三章 注解
- 基本語法
- 定義注解
- 元注解
- 編寫注解處理器
- 注解元素
- 默認值限制
- 替代方案
- 注解不支持繼承
- 實現處理器
- 使用javac處理注解
- 最簡單的處理器
- 更復雜的處理器
- 基于注解的單元測試
- 在 @Unit 中使用泛型
- 實現 @Unit
- 本章小結
- 第二十四章 并發編程
- 術語問題
- 并發的新定義
- 并發的超能力
- 并發為速度而生
- 四句格言
- 1.不要這樣做
- 2.沒有什么是真的,一切可能都有問題
- 3.它起作用,并不意味著它沒有問題
- 4.你必須仍然理解
- 殘酷的真相
- 本章其余部分
- 并行流
- 創建和運行任務
- 終止耗時任務
- CompletableFuture類
- 基本用法
- 結合 CompletableFuture
- 模擬
- 異常
- 流異常(Stream Exception)
- 檢查性異常
- 死鎖
- 構造方法非線程安全
- 復雜性和代價
- 本章小結
- 缺點
- 共享內存陷阱
- This Albatross is Big
- 其他類庫
- 考慮為并發設計的語言
- 拓展閱讀
- 第二十五章 設計模式
- 概念
- 單例模式
- 模式分類
- 構建應用程序框架
- 面向實現
- 工廠模式
- 動態工廠
- 多態工廠
- 抽象工廠
- 函數對象
- 命令模式
- 策略模式
- 責任鏈模式
- 改變接口
- 適配器模式(Adapter)
- 外觀模式(Fa?ade)
- 包(Package)作為外觀模式的變體
- 解釋器:運行時的彈性
- 回調
- 多次調度
- 模式重構
- 抽象用法
- 多次派遣
- 訪問者模式
- RTTI的優劣
- 本章小結
- 附錄:補充
- 附錄:編程指南
- 附錄:文檔注釋
- 附錄:對象傳遞和返回
- 附錄:流式IO
- 輸入流類型
- 輸出流類型
- 添加屬性和有用的接口
- 通過FilterInputStream 從 InputStream 讀取
- 通過 FilterOutputStream 向 OutputStream 寫入
- Reader和Writer
- 數據的來源和去處
- 更改流的行為
- 未發生改變的類
- RandomAccessFile類
- IO流典型用途
- 緩沖輸入文件
- 從內存輸入
- 格式化內存輸入
- 基本文件的輸出
- 文本文件輸出快捷方式
- 存儲和恢復數據
- 讀寫隨機訪問文件
- 本章小結
- 附錄:標準IO
- 附錄:新IO
- ByteBuffer
- 數據轉換
- 基本類型獲取
- 視圖緩沖區
- 字節存儲次序
- 緩沖區數據操作
- 緩沖區細節
- 內存映射文件
- 性能
- 文件鎖定
- 映射文件的部分鎖定
- 附錄:理解equals和hashCode方法
- 附錄:集合主題
- 附錄:并發底層原理
- 附錄:數據壓縮
- 附錄:對象序列化
- 附錄:靜態語言類型檢查
- 附錄:C++和Java的優良傳統
- 附錄:成為一名程序員