# 8.1 數組
對數組的大多數必要的介紹已在第4章的最后一節進行。通過那里的學習,大家已知道自己該如何定義及初始化一個數組。對象的容納是本章的重點,而數組只是容納對象的一種方式。但由于還有其他大量方法可容納數組,所以是哪些地方使數組顯得如此特別呢?
有兩方面的問題將數組與其他集合類型區分開來:效率和類型。對于Java來說,為保存和訪問一系列對象(實際是對象的引用)數組,最有效的方法莫過于數組。數組實際代表一個簡單的線性序列,它使得元素的訪問速度非常快,但我們卻要為這種速度付出代價:創建一個數組對象時,它的大小是固定的,而且不可在那個數組對象的“存在時間”內發生改變。可創建特定大小的一個數組,然后假如用光了存儲空間,就再創建一個新數組,將所有引用從舊數組移到新數組。這屬于“向量”(`Vector`)類的行為,本章稍后還會詳細討論它。然而,由于為這種大小的靈活性要付出較大的代價,所以我們認為向量的效率并沒有數組高。
C++的向量類知道自己容納的是什么類型的對象,但同Java的數組相比,它卻有一個明顯的缺點:C++向量類的`operator[]`不能進行范圍檢查,所以很容易超出邊界(然而,它可以查詢`vector`有多大,而且`at()`方法確實能進行范圍檢查)。在Java中,無論使用的是數組還是集合,都會進行范圍檢查——若超過邊界,就會獲得一個`RuntimeException`(運行期異常)錯誤。正如大家在第9章會學到的那樣,這類異常指出的是一個程序員錯誤,所以不需要在代碼中檢查它。在另一方面,由于C++的`vector`不進行范圍檢查,所以訪問速度較快——在Java中,由于對數組和集合都要進行范圍檢查,所以對性能有一定的影響。
本章還要學習另外幾種常見的集合類:`Vector`(向量)、`Stack`(棧)以及`Hashtable`(散列表)。這些類都涉及對對象的處理——好象它們沒有特定的類型。換言之,它們將其當作`Object`類型處理(`Object`類型是Java中所有類的“根”類)。從某個角度看,這種處理方法是非常合理的:我們僅需構建一個集合,然后任何Java對象都可以進入那個集合(除基本數據類型外——可用Java的基本類型封裝類將其作為常數置入集合,或者將其封裝到自己的類內,作為可以變化的值使用)。這再一次反映了數組優于常規集合:創建一個數組時,可令其容納一種特定的類型。這意味著可進行編譯期類型檢查,預防自己設置了錯誤的類型,或者錯誤指定了準備提取的類型。當然,在編譯期或者運行期,Java會防止我們將不當的消息發給一個對象。所以我們不必考慮自己的哪種做法更加危險,只要編譯器能及時地指出錯誤,同時在運行期間加快速度,目的也就達到了。此外,用戶很少會對一次異常事件感到非常驚訝的。
考慮到執行效率和類型檢查,應盡可能地采用數組。然而,當我們試圖解決一個更常規的問題時,數組的局限也可能顯得非常明顯。在研究過數組以后,本章剩余的部分將把重點放到Java提供的集合類身上。
## 8.1.1 數組和第一類對象
無論使用的數組屬于什么類型,數組標識符實際都是指向真實對象的一個引用。那些對象本身是在內存“堆”里創建的。堆對象既可“隱式”創建(即默認產生),亦可“顯式”創建(即明確指定,用一個`new`表達式)。堆對象的一部分(實際是我們能訪問的唯一字段或方法)是只讀的`length`(長度)成員,它告訴我們那個數組對象里最多能容納多少元素。對于數組對象,`[]`語法是我們能采用的唯一另類訪問方法。
下面這個例子展示了對數組進行初始化的不同方式,以及如何將數組引用分配給不同的數組對象。它也揭示出對象數組和基本數據類型數組在使用方法上幾乎是完全一致的。唯一的差別在于對象數組容納的是引用,而基本數據類型數組容納的是具體的數值(若在執行此程序時遇到困難,請參考第3章的“賦值”小節):
```
//: ArraySize.java
// Initialization & re-assignment of arrays
package c08;
class Weeble {} // A small mythical creature
public class ArraySize {
public static void main(String[] args) {
// Arrays of objects:
Weeble[] a; // Null handle
Weeble[] b = new Weeble[5]; // Null handles
Weeble[] c = new Weeble[4];
for(int i = 0; i < c.length; i++)
c[i] = new Weeble();
Weeble[] d = {
new Weeble(), new Weeble(), new Weeble()
};
// Compile error: variable a not initialized:
//!System.out.println("a.length=" + a.length);
System.out.println("b.length = " + b.length);
// The handles inside the array are
// automatically initialized to null:
for(int i = 0; i < b.length; i++)
System.out.println("b[" + i + "]=" + b[i]);
System.out.println("c.length = " + c.length);
System.out.println("d.length = " + d.length);
a = d;
System.out.println("a.length = " + a.length);
// Java 1.1 initialization syntax:
a = new Weeble[] {
new Weeble(), new Weeble()
};
System.out.println("a.length = " + a.length);
// Arrays of primitives:
int[] e; // Null handle
int[] f = new int[5];
int[] g = new int[4];
for(int i = 0; i < g.length; i++)
g[i] = i*i;
int[] h = { 11, 47, 93 };
// Compile error: variable e not initialized:
//!System.out.println("e.length=" + e.length);
System.out.println("f.length = " + f.length);
// The primitives inside the array are
// automatically initialized to zero:
for(int i = 0; i < f.length; i++)
System.out.println("f[" + i + "]=" + f[i]);
System.out.println("g.length = " + g.length);
System.out.println("h.length = " + h.length);
e = h;
System.out.println("e.length = " + e.length);
// Java 1.1 initialization syntax:
e = new int[] { 1, 2 };
System.out.println("e.length = " + e.length);
}
} ///:~
Here’s the output from the program:
b.length = 5
b[0]=null
b[1]=null
b[2]=null
b[3]=null
b[4]=null
c.length = 4
d.length = 3
a.length = 3
a.length = 2
f.length = 5
f[0]=0
f[1]=0
f[2]=0
f[3]=0
f[4]=0
g.length = 4
h.length = 3
e.length = 3
e.length = 2
```
其中,數組`a`只是初始化成一個`null`引用。此時,編譯器會禁止我們對這個引用作任何實際操作,除非已正確地初始化了它。數組`b`被初始化成指向由`Weeble`引用構成的一個數組,但那個數組里實際并未放置任何`Weeble`對象。然而,我們仍然可以查詢那個數組的大小,因為`b`指向的是一個合法對象。這也為我們帶來了一個難題:不可知道那個數組里實際包含了多少個元素,因為`length`只告訴我們可將多少元素置入那個數組。換言之,我們只知道數組對象的大小或容量,不知其實際容納了多少個元素。盡管如此,由于數組對象在創建之初會自動初始化成`null`,所以可檢查它是否為`null`,判斷一個特定的數組“空位”是否容納一個對象。類似地,由基本數據類型構成的數組會自動初始化成零(針對數值類型)、`null`(字符類型)或者`false`(布爾類型)。
數組`c`顯示出我們首先創建一個數組對象,再將`Weeble`對象賦給那個數組的所有“空位”。數組`d`揭示出“集合初始化”語法,從而創建數組對象(用`new`命令明確進行,類似于數組`c`),然后用`Weeble`對象進行初始化,全部工作在一條語句里完成。
下面這個表達式:
```
a = d;
```
向我們展示了如何取得同一個數組對象連接的引用,然后將其賦給另一個數組對象,就象我們針對對象引用的其他任何類型做的那樣。現在,`a`和`d`都指向內存堆內同樣的數組對象。
Java 1.1加入了一種新的數組初始化語法,可將其想象成“動態集合初始化”。由`d`采用的Java 1.0集合初始化方法則必須在定義`d`的同時進行。但若采用Java 1.1的語法,卻可以在任何地方創建和初始化一個數組對象。例如,假設`hide()`方法用于取得一個`Weeble`對象數組,那么調用它時傳統的方法是:
```
hide(d);
```
但在Java 1.1中,亦可動態創建想作為參數傳遞的數組,如下所示:
```
hide(new Weeble[] {new Weeble(), new Weeble() });
```
這一新式語法使我們在某些場合下寫代碼更方便了。
上述例子的第二部分揭示出這樣一個問題:對于由基本數據類型構成的數組,它們的運作方式與對象數組極為相似,只是前者直接包容了基本類型的數據值。
(1) 基本數據類型集合
集合類只能容納對象引用。但對一個數組,卻既可令其直接容納基本類型的數據,亦可容納指向對象的引用。利用象`Integer`、`Double`之類的“包裝器”類,可將基本數據類型的值置入一個集合里。但正如本章后面會在`WordCount.java`例子中講到的那樣,用于基本數據類型的包裝器類只是在某些場合下才能發揮作用。無論將基本類型的數據置入數組,還是將其封裝進入位于集合的一個類內,都涉及到執行效率的問題。顯然,若能創建和訪問一個基本數據類型數組,那么比起訪問一個封裝數據的集合,前者的效率會高出許多。
當然,假如準備一種基本數據類型,同時又想要集合的靈活性(在需要的時候可自動擴展,騰出更多的空間),就不宜使用數組,必須使用由封裝的數據構成的一個集合。大家或許認為針對每種基本數據類型,都應有一種特殊類型的`Vector`。但Java并未提供這一特性。某些形式的建模機制或許會在某一天幫助Java更好地解決這個問題(注釋①)。
①:這兒是C++比Java做得好的一個地方,因為C++通過`template`關鍵字提供了對“參數化類型”的支持。
## 8.1.2 數組的返回
假定我們現在想寫一個方法,同時不希望它僅僅返回一樣東西,而是想返回一系列東西。此時,象C和C++這樣的語言會使問題復雜化,因為我們不能返回一個數組,只能返回指向數組的一個指針。這樣就非常麻煩,因為很難控制數組的“存在時間”,它很容易造成內存“漏洞”的出現。
Java采用的是類似的方法,但我們能“返回一個數組”。當然,此時返回的實際仍是指向數組的指針。但在Java里,我們永遠不必擔心那個數組的是否可用——只要需要,它就會自動存在。而且垃圾收集器會在我們完成后自動將其清除。
作為一個例子,請思考如何返回一個字符串數組:
```
//: IceCream.java
// Returning arrays from methods
public class IceCream {
static String[] flav = {
"Chocolate", "Strawberry",
"Vanilla Fudge Swirl", "Mint Chip",
"Mocha Almond Fudge", "Rum Raisin",
"Praline Cream", "Mud Pie"
};
static String[] flavorSet(int n) {
// Force it to be positive & within bounds:
n = Math.abs(n) % (flav.length + 1);
String[] results = new String[n];
int[] picks = new int[n];
for(int i = 0; i < picks.length; i++)
picks[i] = -1;
for(int i = 0; i < picks.length; i++) {
retry:
while(true) {
int t =
(int)(Math.random() * flav.length);
for(int j = 0; j < i; j++)
if(picks[j] == t) continue retry;
picks[i] = t;
results[i] = flav[t];
break;
}
}
return results;
}
public static void main(String[] args) {
for(int i = 0; i < 20; i++) {
System.out.println(
"flavorSet(" + i + ") = ");
String[] fl = flavorSet(flav.length);
for(int j = 0; j < fl.length; j++)
System.out.println("\t" + fl[j]);
}
}
} ///:~
```
`flavorSet()`方法創建了一個名為`results`的`String`數組。該數組的大小為`n`——具體數值取決于我們傳遞給方法的參數。隨后,它從數組`flav`里隨機挑選一些“香料”(`Flavor`),并將它們置入`results`里,并最終返回`results`。返回數組與返回其他任何對象沒什么區別——最終返回的都是一個引用。至于數組到底是在`flavorSet()`里創建的,還是在其他什么地方創建的,這個問題并不重要,因為反正返回的僅是一個引用。一旦我們的操作完成,垃圾收集器會自動關照數組的清除工作。而且只要我們需要數組,它就會乖乖地聽候調遣。
另一方面,注意當`flavorSet()`隨機挑選香料的時候,它需要保證以前出現過的一次隨機選擇不會再次出現。為達到這個目的,它使用了一個無限`while`循環,不斷地作出隨機選擇,直到發現未在`picks`數組里出現過的一個元素為止(當然,也可以進行字符串比較,檢查隨機選擇是否在`results`數組里出現過,但字符串比較的效率比較低)。若成功,就添加這個元素,并中斷循環(`break`),再查找下一個(`i`值會遞增)。但假若`t`是一個已在`picks`里出現過的數組,就用標簽式的`continue`往回跳兩級,強制選擇一個新`t`。用一個調試程序可以很清楚地看到這個過程。
`main()`能顯示出20個完整的香料集合,所以我們看到`flavorSet()`每次都用一個隨機順序選擇香料。為體會這一點,最簡單的方法就是將輸出重導向進入一個文件,然后直接觀看這個文件的內容。
- 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 推薦讀物