# 8.2 集合
現在總結一下我們前面學過的東西:為容納一組對象,最適宜的選擇應當是數組。而且假如容納的是一系列基本數據類型,更是必須采用數組。在本章剩下的部分,大家將接觸到一些更常規的情況。當我們編寫程序時,通常并不能確切地知道最終需要多少個對象。有些時候甚至想用更復雜的方式來保存對象。為解決這個問題,Java提供了四種類型的“集合類”:`Vector`(向量)、`BitSet`(位集)、`Stack`(棧)以及`Hashtable`(散列表)。與擁有集合功能的其他語言相比,盡管這兒的數量顯得相當少,但仍然能用它們解決數量驚人的實際問題。
這些集合類具有形形色色的特征。例如,`Stack`實現了一個LIFO(先入先出)序列,而`Hashtable`是一種“關聯數組”,允許我們將任何對象關聯起來。除此以外,所有Java集合類都能自動改變自身的大小。所以,我們在編程時可使用數量眾多的對象,同時不必擔心會將集合弄得有多大。
## 8.2.1 缺點:類型未知
使用Java集合的“缺點”是在將對象置入一個集合時丟失了類型信息。之所以會發生這種情況,是由于當初編寫集合時,那個集合的程序員根本不知道用戶到底想把什么類型置入集合。若指示某個集合只允許特定的類型,會妨礙它成為一個“常規用途”的工具,為用戶帶來麻煩。為解決這個問題,集合實際容納的是類型為`Object`的一些對象的引用。這種類型當然代表Java中的所有對象,因為它是所有類的根。當然,也要注意這并不包括基本數據類型,因為它們并不是從“任何東西”繼承來的。這是一個很好的方案,只是不適用下述場合:
(1) 將一個對象引用置入集合時,由于類型信息會被拋棄,所以任何類型的對象都可進入我們的集合——即便特別指示它只能容納特定類型的對象。舉個例子來說,雖然指示它只能容納貓,但事實上任何人都可以把一條狗扔進來。
(2) 由于類型信息不復存在,所以集合能肯定的唯一事情就是自己容納的是指向一個對象的引用。正式使用它之前,必須對其進行轉換,使其具有正確的類型。
值得欣慰的是,Java不允許人們濫用置入集合的對象。假如將一條狗扔進一個貓的集合,那么仍會將集合內的所有東西都看作貓,所以在使用那條狗時會得到一個“異常”錯誤。在同樣的意義上,假若試圖將一條狗的引用“轉換”到一只貓,那么運行期間仍會得到一個“異常”錯誤。
下面是個例子:
```
//: CatsAndDogs.java
// Simple collection example (Vector)
import java.util.*;
class Cat {
private int catNumber;
Cat(int i) {
catNumber = i;
}
void print() {
System.out.println("Cat #" + catNumber);
}
}
class Dog {
private int dogNumber;
Dog(int i) {
dogNumber = i;
}
void print() {
System.out.println("Dog #" + dogNumber);
}
}
public class CatsAndDogs {
public static void main(String[] args) {
Vector cats = new Vector();
for(int i = 0; i < 7; i++)
cats.addElement(new Cat(i));
// Not a problem to add a dog to cats:
cats.addElement(new Dog(7));
for(int i = 0; i < cats.size(); i++)
((Cat)cats.elementAt(i)).print();
// Dog is detected only at run-time
}
} ///:~
```
可以看出,`Vector`的使用是非常簡單的:先創建一個,再用`addElement()`置入對象,以后用`elementAt()`取得那些對象(注意`Vector`有一個`size()`方法,可使我們知道已添加了多少個元素,以便防止誤超邊界,造成異常錯誤)。
`Cat`和`Dog`類都非常淺顯——除了都是“對象”之外,它們并無特別之處(倘若不明確指出從什么類繼承,就默認為從`Object`繼承。所以我們不僅能用`Vector`方法將`Cat`對象置入這個集合,也能添加`Dog`對象,同時不會在編譯期和運行期得到任何出錯提示。用`Vector`方法`elementAt()`獲取原本認為是`Cat`的對象時,實際獲得的是指向一個`Object`的引用,必須將那個對象轉換為`Cat`。隨后,需要將整個表達式用括號封閉起來,在為`Cat`調用`print()`方法之前進行強制轉換;否則就會出現一個語法錯誤。在運行期間,如果試圖將`Dog`對象轉換為`Cat`,就會得到一個異常。
這些處理的意義都非常深遠。盡管顯得有些麻煩,但卻獲得了安全上的保證。我們從此再難偶然造成一些隱藏得深的錯誤。若程序的一個部分(或幾個部分)將對象插入一個集合,但我們只是通過一次異常在程序的某個部分發現一個錯誤的對象置入了集合,就必須找出插入錯誤的位置。當然,可通過檢查代碼達到這個目的,但這或許是最笨的調試工具。另一方面,我們可從一些標準化的集合類開始自己的編程。盡管它們在功能上存在一些不足,且顯得有些笨拙,但卻能保證沒有隱藏的錯誤。
(1) 錯誤有時并不顯露出來
在某些情況下,程序似乎正確地工作,不轉換回我們原來的類型。第一種情況是相當特殊的:`String`類從編譯器獲得了額外的幫助,使其能夠正常工作。只要編譯器期待的是一個`String`對象,但它沒有得到一個,就會自動調用在`Object`里定義、并且能夠由任何Java類覆蓋的`toString()`方法。這個方法能生成滿足要求的`String`對象,然后在我們需要的時候使用。
因此,為了讓自己類的對象能顯示出來,要做的全部事情就是覆蓋`toString()`方法,如下例所示:
```
//: WorksAnyway.java
// In special cases, things just seem
// to work correctly.
import java.util.*;
class Mouse {
private int mouseNumber;
Mouse(int i) {
mouseNumber = i;
}
// Magic method:
public String toString() {
return "This is Mouse #" + mouseNumber;
}
void print(String msg) {
if(msg != null) System.out.println(msg);
System.out.println(
"Mouse number " + mouseNumber);
}
}
class MouseTrap {
static void caughtYa(Object m) {
Mouse mouse = (Mouse)m; // Cast from Object
mouse.print("Caught one!");
}
}
public class WorksAnyway {
public static void main(String[] args) {
Vector mice = new Vector();
for(int i = 0; i < 3; i++)
mice.addElement(new Mouse(i));
for(int i = 0; i < mice.size(); i++) {
// No cast necessary, automatic call
// to Object.toString():
System.out.println(
"Free mouse: " + mice.elementAt(i));
MouseTrap.caughtYa(mice.elementAt(i));
}
}
} ///:~
```
可在`Mouse`里看到對`toString()`的重定義代碼。在`main()`的第二個`for`循環中,可發現下述語句:
```
System.out.println("Free mouse: " +
mice.elementAt(i));
```
在`+`后,編譯器預期看到的是一個`String`對象。`elementAt()`生成了一個`Object`,所以為獲得希望的`String`,編譯器會默認調用`toString()`。但不幸的是,只有針對`String`才能得到象這樣的結果;其他任何類型都不會進行這樣的轉換。
隱藏轉換的第二種方法已在`Mousetrap`里得到了應用。`caughtYa()`方法接收的不是一個`Mouse`,而是一個`Object`。隨后再將其轉換為一個`Mouse`。當然,這樣做是非常冒失的,因為通過接收一個`Object`,任何東西都可以傳遞給方法。然而,假若轉換不正確——如果我們傳遞了錯誤的類型——就會在運行期間得到一個異常錯誤。這當然沒有在編譯期進行檢查好,但仍然能防止問題的發生。注意在使用這個方法時毋需進行轉換:
```
MouseTrap.caughtYa(mice.elementAt(i));
```
(2) 生成能自動判別類型的`Vector`
大家或許不想放棄剛才那個問題。一個更“健壯”的方案是用`Vector`創建一個新類,使其只接收我們指定的類型,也只生成我們希望的類型。如下所示:
```
//: GopherVector.java
// A type-conscious Vector
import java.util.*;
class Gopher {
private int gopherNumber;
Gopher(int i) {
gopherNumber = i;
}
void print(String msg) {
if(msg != null) System.out.println(msg);
System.out.println(
"Gopher number " + gopherNumber);
}
}
class GopherTrap {
static void caughtYa(Gopher g) {
g.print("Caught one!");
}
}
class GopherVector {
private Vector v = new Vector();
public void addElement(Gopher m) {
v.addElement(m);
}
public Gopher elementAt(int index) {
return (Gopher)v.elementAt(index);
}
public int size() { return v.size(); }
public static void main(String[] args) {
GopherVector gophers = new GopherVector();
for(int i = 0; i < 3; i++)
gophers.addElement(new Gopher(i));
for(int i = 0; i < gophers.size(); i++)
GopherTrap.caughtYa(gophers.elementAt(i));
}
} ///:~
```
這前一個例子類似,只是新的`GopherVector`類有一個類型為`Vector`的`private`成員(從`Vector`繼承有些麻煩,理由稍后便知),而且方法也和`Vector`類似。然而,它不會接收和產生普通`Object`,只對`Gopher`對象感興趣。
由于`GopherVector`只接收一個`Gopher`(地鼠),所以假如我們使用:
```
gophers.addElement(new Pigeon());
```
就會在編譯期間獲得一條出錯消息。采用這種方式,盡管從編碼的角度看顯得更令人沉悶,但可以立即判斷出是否使用了正確的類型。
注意在使用`elementAt()`時不必進行轉換——它肯定是一個`Gopher`。
(3) 參數化類型
這類問題并不是孤立的——我們許多時候都要在其他類型的基礎上創建新類型。此時,在編譯期間擁有特定的類型信息是非常有幫助的。這便是“參數化類型”的概念。在C++中,它由語言通過“模板”獲得了直接支持。至少,Java保留了關鍵字`generic`,期望有一天能夠支持參數化類型。但我們現在無法確定這一天何時會來臨。
- 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 推薦讀物