# 5.1 包:庫單元
我們用`import`關鍵字導入一個完整的庫時,就會獲得“包”(Package)。例如:
```
import java.util.*;
```
它的作用是導入完整的實用工具(Utility)庫,該庫屬于標準Java開發工具包的一部分。由于`Vector`位于`java.util`里,所以現在要么指定完整名稱`java.util.Vector`(可省略`import`語句),要么簡單地指定一個`Vector`(因為`import`是默認的)。
若想導入單獨一個類,可在`import`語句里指定那個類的名字:
```
import java.util.Vector;
```
現在,我們可以自由地使用`Vector`。然而,`java.util`中的其他任何類仍是不可使用的。
之所以要進行這樣的導入,是為了提供一種特殊的機制,以便管理“命名空間”(Name Space)。我們所有類成員的名字相互間都會隔離起來。位于類`A`內的一個方法`f()`不會與位于類`B`內的、擁有相同“簽名”(參數列表)的`f()`發生沖突。但類名會不會沖突呢?假設創建一個`stack`類,將它安裝到已有一個`stack`類(由其他人編寫)的機器上,這時會出現什么情況呢?對于因特網中的Java應用,這種情況會在用戶毫不知曉的時候發生,因為類會在運行一個Java程序的時候自動下載。
正是由于存在名字潛在的沖突,所以特別有必要對Java中的命名空間進行完整的控制,而且需要創建一個完全獨一無二的名字,無論因特網存在什么樣的限制。
迄今為止,本書的大多數例子都僅存在于單個文件中,而且設計成局部(本地)使用,沒有同包名發生沖突(在這種情況下,類名置于“默認包”內)。這是一種有效的做法,而且考慮到問題的簡化,本書剩下的部分也將盡可能地采用它。然而,若計劃創建一個“對因特網友好”或者說“適合在因特網使用”的程序,必須考慮如何防止類名的重復。
為Java創建一個源碼文件的時候,它通常叫作一個“編輯單元”(有時也叫作“翻譯單元”)。每個編譯單元都必須有一個以`.java`結尾的名字。而且在編譯單元的內部,可以有一個公共(`public`)類,它必須擁有與文件相同的名字(包括大小寫形式,但排除`.java`文件擴展名)。如果不這樣做,編譯器就會報告出錯。每個編譯單元內都只能有一個`public`類(同樣地,否則編譯器會報告出錯)。那個編譯單元剩下的類(如果有的話)可在那個包外面的世界面前隱藏起來,因為它們并非“公共”的(非`public`),而且它們由用于主`public`類的“支撐”類組成。
編譯一個`.java`文件時,我們會獲得一個名字完全相同的輸出文件;但對于`.java`文件中的每個類,它們都有一個`.class`擴展名。因此,我們最終從少量的`.java`文件里有可能獲得數量眾多的`.class`文件。如以前用一種匯編語言寫過程序,那么可能已習慣編譯器先分割出一種過渡形式(通常是一個`.obj`文件),再用一個鏈接器將其與其他東西封裝到一起(生成一個可執行文件),或者與一個庫封裝到一起(生成一個庫)。但那并不是Java的工作方式。一個有效的程序就是一系列`.class`文件,它們可以封裝和壓縮到一個JAR文件里(使用Java 1.1提供的`jar`工具)。Java解釋器負責對這些文件的尋找、裝載和解釋(注釋①)。
①:Java并沒有強制一定要使用解釋器。一些固有代碼的Java編譯器可生成單獨的可執行文件。
“庫”也由一系列類文件構成。每個文件都有一個`public`類(并沒強迫使用一個`public`類,但這種情況最很典型的),所以每個文件都有一個組件。如果想將所有這些組件(它們在各自獨立的`.java`和`.class`文件里)都歸納到一起,那么`package`關鍵字就可以發揮作用)。
若在一個文件的開頭使用下述代碼:
```
package mypackage;
```
那么`package`語句必須作為文件的第一個非注釋語句出現。該語句的作用是指出這個編譯單元屬于名為`mypackage`的一個庫的一部分。或者換句話說,它表明這個編譯單元內的`public`類名位于`mypackage`這個名字的下面。如果其他人想使用這個名字,要么指出完整的名字,要么與`mypackage`聯合使用`import`關鍵字(使用前面給出的選項)。注意根據Java包(封裝)的約定,名字內的所有字母都應小寫,甚至那些中間單詞亦要如此。
例如,假定文件名是`MyClass.java`。它意味著在那個文件有一個、而且只能有一個`public`類。而且那個類的名字必須是`MyClass`(包括大小寫形式):
```
package mypackage;
public class MyClass {
// . . .
```
現在,如果有人想使用`MyClass`,或者想使用`mypackage`內的其他任何`public`類,他們必須用`import`關鍵字激活`mypackage`內的名字,使它們能夠使用。另一個辦法則是指定完整的名稱:
```
mypackage.MyClass m = new mypackage.MyClass();
```
`import`關鍵字則可將其變得簡潔得多:
```
import mypackage.*;
// . . .
MyClass m = new MyClass();
```
作為一名庫設計者,一定要記住`package`和`import`關鍵字允許我們做的事情就是分割單個全局命名空間,保證我們不會遇到名字的沖突——無論有多少人使用因特網,也無論多少人用Java編寫自己的類。
## 5.1.1 創建獨一無二的包名
大家或許已注意到這樣一個事實:由于一個包永遠不會真的“封裝”到單獨一個文件里面,它可由多個`.class`文件構成,所以局面可能稍微有些混亂。為避免這個問題,最合理的一種做法就是將某個特定包使用的所有`.class`文件都置入單個目錄里。也就是說,我們要利用操作系統的分級文件結構避免出現混亂局面。這正是Java所采取的方法。
它同時也解決了另兩個問題:創建獨一無二的包名以及找出那些可能深藏于目錄結構某處的類。正如我們在第2章講述的那樣,為達到這個目的,需要將`.class`文件的位置路徑編碼到`package`的名字里。但根據約定,編譯器強迫`package`名的第一部分是類創建者的因特網域名。由于因特網域名肯定是獨一無二的(由InterNIC保證——注釋②,它控制著域名的分配),所以假如按這一約定行事,`package`的名稱就肯定不會重復,所以永遠不會遇到名稱沖突的問題。換句話說,除非將自己的域名轉讓給其他人,而且對方也按照相同的路徑名編寫Java代碼,否則名字的沖突是永遠不會出現的。當然,如果你沒有自己的域名,那么必須創造一個非常生僻的包名(例如自己的英文姓名),以便盡最大可能創建一個獨一無二的包名。如決定發行自己的Java代碼,那么強烈推薦去申請自己的域名,它所需的費用是非常低廉的。
②:ftp://ftp.internic.net
這個技巧的另一部分是將`package`名解析成自己機器上的一個目錄。這樣一來,Java程序運行并需要裝載`.class`文件的時候(這是動態進行的,在程序需要創建屬于那個類的一個對象,或者首次訪問那個類的一個`static`成員時),它就可以找到`.class`文件駐留的那個目錄。
Java解釋器的工作程序如下:首先,它找到環境變量`CLASSPATH`(將Java或者具有Java解釋能力的工具——如瀏覽器——安裝到機器中時,通過操作系統進行設定)。`CLASSPATH`包含了一個或多個目錄,它們作為一種特殊的“根”使用,從這里展開對`.class`文件的搜索。從那個根開始,解釋器會尋找包名,并將每個點號(句點)替換成一個斜杠,從而生成從`CLASSPATH`根開始的一個路徑名(所以`package foo.bar.baz`會變成`foo\bar\baz`或者`foo/bar/baz`;具體是正斜杠還是反斜杠由操作系統決定)。隨后將它們連接到一起,成為`CLASSPATH`內的各個條目(入口)。以后搜索`.class`文件時,就可從這些地方開始查找與準備創建的類名對應的名字。此外,它也會搜索一些標準目錄——這些目錄與Java解釋器駐留的地方有關。
為進一步理解這個問題,下面以我自己的域名為例,它是`bruceeckel.com`。將其反轉過來后,`com.bruceeckel`就為我的類創建了獨一無二的全局名稱(`com`,`edu`,`org`,`net`等擴展名以前在Java包中都是大寫的,但自Java 1.2以來,這種情況已發生了變化。現在整個包名都是小寫的)。由于決定創建一個名為`util`的庫,我可以進一步地分割它,所以最后得到的包名如下:
```
package com.bruceeckel.util;
```
現在,可將這個包名作為下述兩個文件的“命名空間”使用:
```
//: Vector.java
// Creating a package
package com.bruceeckel.util;
public class Vector {
public Vector() {
System.out.println(
"com.bruceeckel.util.Vector");
}
} ///:~
```
創建自己的包時,要求`package`語句必須是文件中的第一個“非注釋”代碼。第二個文件表面看起來是類似的:
```
//: List.java
// Creating a package
package com.bruceeckel.util;
public class List {
public List() {
System.out.println(
"com.bruceeckel.util.List");
}
} ///:~
```
這兩個文件都置于我自己系統的一個子目錄中:
```
C:\DOC\JavaT\com\bruceeckel\util
```
若通過它往回走,就會發現包名`com.bruceeckel.uti`l,但路徑的第一部分又是什么呢?這是由`CLASSPATH`環境變量決定的。在我的機器上,它是:
```
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT
```
可以看出,`CLASSPATH`里能包含大量備用的搜索路徑。然而,使用JAR文件時要注意一個問題:必須將JAR文件的名字置于類路徑里,而不僅僅是它所在的路徑。所以對一個名為`grape.jar`的JAR文件來說,我們的類路徑需要包括:
```
CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar
```
正確設置好類路徑后,可將下面這個文件置于任何目錄里(若在執行該程序時遇到麻煩,請參見第3章的3.1.2小節“賦值”):
```
//: LibTest.java
// Uses the library
package c05;
import com.bruceeckel.util.*;
public class LibTest {
public static void main(String[] args) {
Vector v = new Vector();
List l = new List();
}
} ///:~
```
編譯器遇到`import`語句后,它會搜索由`CLASSPATH`指定的目錄,查找子目錄`com\bruceeckel\util`,然后查找名稱適當的已編譯文件(對于`Vector`是`Vector.class`,對于`List`則是`List.class`)。注意`Vector`和`List`內無論類還是需要的方法都必須設為`public`。
(1) 自動編譯
為導入的類首次創建一個對象時(或者訪問一個類的`static`成員時),編譯器會在適當的目錄里尋找同名的`.class`文件(所以如果創建類X的一個對象,就應該是`X.class`)。若只發現`X.class`,它就是必須使用的那一個類。然而,如果它在相同的目錄中還發現了一個`X.java`,編譯器就會比較兩個文件的日期標記。如果`X.java`比`X.class`新,就會自動編譯`X.java`,生成一個最新的`X.class`。
對于一個特定的類,或在與它同名的`.java`文件中沒有找到它,就會對那個類采取上述的處理。
(2) 沖突
若通過 `*` 導入了兩個庫,而且它們包括相同的名字,這時會出現什么情況呢?例如,假定一個程序使用了下述導入語句:
```
import com.bruceeckel.util.*;
import java.util.*;
```
由于 `java.util.*` 也包含了一個`Vector`類,所以這會造成潛在的沖突。然而,只要沖突并不真的發生,那么就不會產生任何問題——這當然是最理想的情況,因為否則的話,就需要進行大量編程工作,防范那些可能可能永遠也不會發生的沖突。
如現在試著生成一個`Vector`,就肯定會發生沖突。如下所示:
```
Vector v = new Vector();
```
它引用的到底是哪個`Vector`類呢?編譯器對這個問題沒有答案,讀者也不可能知道。所以編譯器會報告一個錯誤,強迫我們進行明確的說明。例如,假設我想使用標準的Java `Vector`,那么必須象下面這樣編程:
```
java.util.Vector v = new java.util.Vector();
```
由于它(與`CLASSPATH`一起)完整指定了那個Vector的位置,所以不再需要 `import java.util.*` 語句,除非還想使用來自`java.util`的其他東西。
## 5.1.2 自定義工具庫
掌握前述的知識后,接下來就可以開始創建自己的工具庫,以便減少或者完全消除重復的代碼。例如,可為`System.out.println()`創建一個別名,減少重復鍵入的代碼量。它可以是名為`tools`的一個包(`package`)的一部分:
```
//: P.java
// The P.rint & P.rintln shorthand
package com.bruceeckel.tools;
public class P {
public static void rint(Object obj) {
System.out.print(obj);
}
public static void rint(String s) {
System.out.print(s);
}
public static void rint(char[] s) {
System.out.print(s);
}
public static void rint(char c) {
System.out.print(c);
}
public static void rint(int i) {
System.out.print(i);
}
public static void rint(long l) {
System.out.print(l);
}
public static void rint(float f) {
System.out.print(f);
}
public static void rint(double d) {
System.out.print(d);
}
public static void rint(boolean b) {
System.out.print(b);
}
public static void rintln() {
System.out.println();
}
public static void rintln(Object obj) {
System.out.println(obj);
}
public static void rintln(String s) {
System.out.println(s);
}
public static void rintln(char[] s) {
System.out.println(s);
}
public static void rintln(char c) {
System.out.println(c);
}
public static void rintln(int i) {
System.out.println(i);
}
public static void rintln(long l) {
System.out.println(l);
}
public static void rintln(float f) {
System.out.println(f);
}
public static void rintln(double d) {
System.out.println(d);
}
public static void rintln(boolean b) {
System.out.println(b);
}
} ///:~
```
所有不同的數據類型現在都可以在一個新行輸出(`P.rintln()`),或者不在一個新行輸出(`P.rint()`)。
大家可能會猜想這個文件所在的目錄必須從某個`CLASSPATH`位置開始,然后繼續`com/bruceeckel/tools`。編譯完畢后,利用一個`import`語句,即可在自己系統的任何地方使用`P.class`文件。如下所示:
```
ToolTest.java
```
所以從現在開始,無論什么時候只要做出了一個有用的新工具,就可將其加入`tools`目錄(或者自己的個人`util`或`tools`目錄)。
(1) `CLASSPATH`的陷阱
`P.java`文件存在一個非常有趣的陷阱。特別是對于早期的Java實現方案來說,類路徑的正確設定通常都是很困難的一項工作。編寫這本書的時候,我引入了`P.java`文件,它最初看起來似乎工作很正常。但在某些情況下,卻開始出現中斷。在很長的時間里,我都確信這是Java或其他什么在實現時一個錯誤。但最后,我終于發現在一個地方引入了一個程序(即第17章要說明的`CodePackager.java`),它使用了一個不同的類`P`。由于它作為一個工具使用,所以有時候會進入類路徑里;另一些時候則不會這樣。但只要它進入類路徑,那么假若執行的程序需要尋找`com.bruceeckel.tools`中的類,Java首先發現的就是`CodePackager.java`中的`P`。此時,編譯器會報告一個特定的方法沒有找到。這當然是非常令人頭疼的,因為我們在前面的類`P`里明明看到了這個方法,而且根本沒有更多的診斷報告可為我們提供一條線索,讓我們知道找到的是一個完全不同的類(那甚至不是`public`的)。
乍一看來,這似乎是編譯器的一個錯誤,但假若考察`import`語句,就會發現它只是說:“在這里可能發現了`P`”。然而,我們假定的是編譯器搜索自己類路徑的任何地方,所以一旦它發現一個`P`,就會使用它;若在搜索過程中發現了“錯誤的”一個,它就會停止搜索。這與我們在前面表述的稍微有些區別,因為存在一些討厭的類,它們都位于包內。而這里有一個不在包內的P,但仍可在常規的類路徑搜索過程中找到。
如果您遇到象這樣的情況,請務必保證對于類路徑的每個地方,每個名字都僅存在一個類。
## 5.1.3 利用導入改變行為
Java已取消的一種特性是C的“條件編譯”,它允許我們改變參數,獲得不同的行為,同時不改變其他任何代碼。Java之所以拋棄了這一特性,可能是由于該特性經常在C里用于解決跨平臺問題:代碼的不同部分根據具體的平臺進行編譯,否則不能在特定的平臺上運行。由于Java的設計思想是成為一種自動跨平臺的語言,所以這種特性是沒有必要的。
然而,條件編譯還有另一些非常有價值的用途。一種很常見的用途就是調試代碼。調試特性可在開發過程中使用,但在發行的產品中卻無此功能。Alen Holub(`www.holub.com`)提出了利用包(`package`)來模仿條件編譯的概念。根據這一概念,它創建了C“斷定機制”一個非常有用的Java版本。之所以叫作“斷定機制”,是由于我們可以說“它應該為真”或者“它應該為假”。如果語句不同意你的斷定,就可以發現相關的情況。這種工具在調試過程中是特別有用的。
可用下面這個類進行程序調試:
```
//: Assert.java
// Assertion tool for debugging
package com.bruceeckel.tools.debug;
public class Assert {
private static void perr(String msg) {
System.err.println(msg);
}
public final static void is_true(boolean exp) {
if(!exp) perr("Assertion failed");
}
public final static void is_false(boolean exp){
if(exp) perr("Assertion failed");
}
public final static void
is_true(boolean exp, String msg) {
if(!exp) perr("Assertion failed: " + msg);
}
public final static void
is_false(boolean exp, String msg) {
if(exp) perr("Assertion failed: " + msg);
}
} ///:~
```
這個類只是簡單地封裝了布爾測試。如果失敗,就顯示出出錯消息。在第9章,大家還會學習一個更高級的錯誤控制工具,名為“異常控制”。但在目前這種情況下,`perr()`方法已經可以很好地工作。
如果想使用這個類,可在自己的程序中加入下面這一行:
```
import com.bruceeckel.tools.debug.*;
```
如欲清除斷定機制,以便自己能發行最終的代碼,我們創建了第二個`Assert`類,但卻是在一個不同的包里:
```
//: Assert.java
// Turning off the assertion output
// so you can ship the program.
package com.bruceeckel.tools;
public class Assert {
public final static void is_true(boolean exp){}
public final static void is_false(boolean exp){}
public final static void
is_true(boolean exp, String msg) {}
public final static void
is_false(boolean exp, String msg) {}
} ///:~
```
現在,假如將前一個`import`語句變成下面這個樣子:
```
import com.bruceeckel.tools.*;
```
程序便不再顯示出斷言。下面是個例子:
```
//: TestAssert.java
// Demonstrating the assertion tool
package c05;
// Comment the following, and uncomment the
// subsequent line to change assertion behavior:
import com.bruceeckel.tools.debug.*;
// import com.bruceeckel.tools.*;
public class TestAssert {
public static void main(String[] args) {
Assert.is_true((2 + 2) == 5);
Assert.is_false((1 + 1) == 2);
Assert.is_true((2 + 2) == 5, "2 + 2 == 5");
Assert.is_false((1 + 1) == 2, "1 +1 != 2");
}
} ///:~
```
通過改變導入的`package`,我們可將自己的代碼從調試版本變成最終的發行版本。這種技術可應用于任何種類的條件代碼。
## 5.1.4 包的停用
大家應注意這樣一個問題:每次創建一個包后,都在為包取名時間接地指定了一個目錄結構。這個包必須存在(駐留)于由它的名字規定的目錄內。而且這個目錄必須能從`CLASSPATH`開始搜索并發現。最開始的時候,`package`關鍵字的運用可能會令人迷惑,因為除非堅持遵守根據目錄路徑指定包名的規則,否則就會在運行期獲得大量莫名其妙的消息,指出找不到一個特定的類——即使那個類明明就在相同的目錄中。若得到象這樣的一條消息,請試著將`package`語句作為注釋標記出去。如果這樣做行得通,就可知道問題到底出在哪兒。
- 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 推薦讀物