JAVA--集合知識總結
Java 提供了容納物件(或者物件的控制代碼)的多種方式。其中內建的型別是陣列,此外, Java 的工具庫提供了一些
“集合類”,利用這些集合類,我們可以容納乃至操縱自己的物件。
宣告:本篇部落格內容參考自《java程式設計思想》,程式碼均來自書中,大部分內容擷取自該書
陣列和第一類物件
無論使用的陣列屬於什麼型別,陣列識別符號實際都是指向真實物件的一個控制代碼。那些物件本身是在記憶體
“堆”裡建立的。堆物件既可“隱式”建立(即預設產生),亦可“顯式”建立(即明確指定,用一個 new
表示式)。堆物件的一部分(實際是我們能訪問的唯一欄位或方法)是隻讀的length(長度)成員,它告訴
我們那個陣列物件裡最多能容納多少元素。對於陣列物件,“ []”語法是我們能採用的唯一另類訪問方法。
物件陣列和基本資料型別陣列在使用方法上幾乎是完全一致的。唯一的差別在於物件陣列容納的是控制代碼,而基本資料型別陣列容納的是具體的數值
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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
輸出如下:
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
其中,陣列 a 只是初始化成一個 null 控制代碼。此時,編譯器會禁止我們對這個控制代碼作任何實際操作,除非已正
確地初始化了它。陣列 b 被初始化成指向由 Weeble 控制代碼構成的一個數組,但那個數組裡實際並未放置任何
Weeble 物件。然而,我們仍然可以查詢那個陣列的大小,因為 b 指向的是一個合法物件。
換言之,我們只知道陣列物件的大小或容量,不知其實際容納了多少個元素。
儘管如此,由於陣列物件在建立之初會自動初始化成 null,所以可檢查它是否為 null,判斷一個特定的陣列“空位”是否容納一個物件。類似地,由基本資料型別構成的陣列會自動初始化成零(針對數值型別)、 null(字元型別)或者false(布林型別)
陣列 c 顯示出我們首先建立一個數組物件,再將 Weeble 物件賦給那個陣列的所有“空位”。陣列 d 揭示出
“集合初始化”語法,從而建立陣列物件(用 new 命令明確進行,類似於陣列 c),然後用 Weeble 物件進行
初始化,全部工作在一條語句裡完成。
下面這個表示式:
a = d;
向我們展示瞭如何取得同一個陣列物件連線的控制代碼,然後將其賦給另一個數組物件,向我們展示瞭如何取得同一個陣列物件連線的控制代碼,然後將其賦給另一個數組物件
- 基本資料型別集合
集合類只能容納物件控制代碼。但對一個數組,卻既可令其直接容納基本型別的資料,亦可容納指向物件的句
柄。利用象 Integer、 Double 之類的“ 封裝器”類,可將基本資料型別的值置入一個集合裡。
無論將基本型別的資料置入陣列,還是將其封裝進入位於集合的一個類內,都涉及到執行效率的問題。顯
然,若能建立和訪問一個基本資料型別陣列,那麼比起訪問一個封裝資料的集合,前者的效率會高出許多。
陣列的返回
假定我們現在想寫一個方法,同時不希望它僅僅返回一樣東西,而是想返回一系列東西。此時,象C 和 C++這樣的語言會使問題複雜化,因為我們不能返回一個數組,只能返回指向陣列的一個指標。這樣就非常麻煩,因為很難控制陣列的“存在時間”,它很容易造成記憶體“漏洞”的出現。
Java 採用的是類似的方法,但我們能“返回一個數組”。當然,此時返回的實際仍是指向陣列的指標。但在Java 裡,我們永遠不必擔心那個陣列的是否可用—— 只要需要,它就會自動存在。而且垃圾收集器會在我們完成後自動將其清除
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++)213
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]);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
flavorSet()方法建立了一個名為 results 的 String 陣列。該陣列的大小為 n—— 具體數值取決於我們傳遞給方法的自變數。隨後,它從陣列 flav 裡隨機挑選一些“香料”( Flavor),並將它們置入 results 裡,並最終返回 results。返回陣列與返回其他任何物件沒什麼區別—— 最終返回的都是一個控制代碼。
另一方面,注意當 flavorSet()隨機挑選香料的時候,它需要保證以前出現過的一次隨機選擇不會再次出現。為達到這個目的,它使用了一個無限 while 迴圈,不斷地作出隨機選擇,直到發現未在 picks 數組裡出現過的一個元素為止(當然,也可以進行字串比較,檢查隨機選擇是否在 results 數組裡出現過,但字串比較的效率比較低)。若成功,就新增這個元素,並中斷迴圈( break),再查詢下一個( i 值會遞增)。但假若 t 是一個已在 picks 裡出現過的陣列,就用標籤式的 continue 往回跳兩級,強制選擇一個新 t。 用一個除錯程式可以很清楚地看到這個過程。
集合
為容納一組物件,最適宜的選擇應當是陣列。而且假如容納的是一系列基本資料型別,更是必須採用陣列。
缺點:型別未知
使用 Java 集合的“缺點”是在將物件置入一個集合時丟失了型別資訊。之所以會發生這種情況,是由於當初編寫集合時,那個集合的程式設計師根本不知道使用者到底想把什麼型別置入集合。若指示某個集合只允許特定的型別,會妨礙它成為一個“常規用途”的工具,為使用者帶來麻煩。為解決這個問題,集合實際容納的是型別為 Object 的一些物件的控制代碼。
當然,也要注意集合並不包括基本資料型別,因為它們並不是從“任何東西”繼承來的。
Java 不允許人們濫用置入集合的物件。假如將一條狗扔進一個貓的集合,那麼仍會將集合內的所有東西都看作貓,所以在使用那條狗時會得到一個“違例”錯誤。在同樣的意義上,假若試圖將一條狗的控制代碼“造型”到一隻貓,那麼執行期間仍會得到一個“違例”錯誤
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
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 錯誤有時並不顯露出來
在某些情況下,程式似乎正確地工作,不造型回我們原來的型別。第一種情況是相當特殊的: String 類從編譯器獲得了額外的幫助,使其能夠正常工作。只要編譯器期待的是一個String 物件,但它沒有得到一個,就會自動呼叫在 Object 裡定義、並且能夠由任何 Java 類覆蓋的 toString()方法。這個方法能生成滿足要求的String 物件,然後在我們需要的時候使用。因此,為了讓自己類的物件能顯示出來,要做的全部事情就是覆蓋toString()方法。
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));216
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));
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
可在 Mouse 裡看到對 toString()的重定義程式碼。在 main()的第二個 for 迴圈中,可發現下述語句:
System.out.println("Free mouse: " +
mice.elementAt(i));
- 1
- 2
- 1
- 2
在“ +”後,編譯器預期看到的是一個 String 物件。 elementAt()生成了一個 Object,所以為獲得希望的String,編譯器會預設呼叫 toString()。但不幸的是,只有針對 String 才能得到象這樣的結果;其他任何型別都不會進行這樣的轉換。
隱藏造型的第二種方法已在 Mousetrap 裡得到了應用。 caughtYa()方法接收的不是一個 Mouse,而是一個Object。隨後再將其造型為一個 Mouse。當然,這樣做是非常冒失的,因為通過接收一個 Object,任何東西都可以傳遞給方法。然而,假若造型不正確—— 如果我們傳遞了錯誤的型別—— 就會在執行期間得到一個違例錯誤。這當然沒有在編譯期進行檢查好,但仍然能防止問題的發生。注意在使用這個方法時毋需進行造型:
MouseTrap.caughtYa(mice.elementAt(i));
- 生成能自動判別型別的 Vector
一個更“健壯”的方案是用 Vector 建立一個新類,使其只接收我們指定的
型別,也只生成我們希望的型別。
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));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
新的 GopherVector 類有一個型別為 Vector 的 private 成員(從 Vector 繼承有些麻煩,理由稍後便知),而且方法也和 Vector 類似。然而,它不會接收和產生普通 Object,只對 Gopher 物件
感興趣。
由於 GopherVector 只接收一個 Gopher(地鼠),所以假如我們使用:
gophers.addElement(new Pigeon());
就會在編譯期間獲得一條出錯訊息。採用這種方式,儘管從編碼的角度看顯得更令人沉悶,但可以立即判斷出是否使用了正確的型別。注意在使用 elementAt()時不必進行造型—— 它肯定是一個 Gopher
列舉器
容納各種各樣的物件正是集合的首要任務。在 Vector 中, addElement()便是我們插入物件採用的方法,而 elementAt()是
提取物件的唯一方法。 Vector 非常靈活,我們可在任何時候選擇任何東西,並可使用不同的索引選擇多個元素。
若從更高的角度看這個問題,就會發現它的一個缺陷:需要事先知道集合的準確型別,否則無法使用。乍看來,這一點似乎沒什麼關係。但假若最開始決定使用Vector,後來在程式中又決定(考慮執行效率的原因)改變成一個 List(屬於 Java1.2 集合庫的一部分),這時又該如何做呢?
我們通常認為反覆器是一種“輕量級”物件;也就是說,建立它只需付出極少的代價。但也正是由於這個原因,我們常發現反覆器存在一些似乎很奇怪的限制。例如,有些反覆器只能朝一個方向移動。
Java 的 Enumeration(列舉,註釋②)便是具有這些限制的一個反覆器的例子。除下面這些外,不可再用它
做其他任何事情:
(1) 用一個名為 elements()的方法要求集合為我們提供一個 Enumeration。我們首次呼叫它的 nextElement()
時,這個 Enumeration 會返回序列中的第一個元素。
(2) 用 nextElement() 獲得下一個物件。
(3) 用 hasMoreElements()檢查序列中是否還有更多的物件
class Hamster {
private int hamsterNumber;
Hamster(int i) {
hamsterNumber = i;
}
public String toString() {
return "This is Hamster #" + hamsterNumber;
}
}
class Printer {
static void printAll(Enumeration e) {
while(e.hasMoreElements())
System.out.println(
e.nextElement().toString());
}
}
public class HamsterMaze {
public static void main(String[] args) {
Vector v = new Vector();
for(int i = 0; i < 3; i++)
v.addElement(new Hamster(i));
Printer.printAll(v.elements());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
仔細研究一下列印方法:
static void printAll(Enumeration e) {
while(e.hasMoreElements())
System.out.println(
e.nextElement().toString());
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
注意其中沒有與序列型別有關的資訊。我們擁有的全部東西便是Enumeration。為了解有關序列的情況,一個 Enumeration 便足夠了:可取得下一個物件,亦可知道是否已抵達了末尾。取得一系列物件,然後在其中遍歷,從而執行一個特定的操作—— 這是一個頗有價值的程式設計概念
集合的型別
V e c t o r
崩潰 Java
Java 標準集合裡包含了 toString()方法,所以它們能生成自己的 String 表達方式,包括它們容納的物件。
例如在 Vector 中, toString()會在 Vector 的各個元素中步進和遍歷,併為每個元素呼叫 toString()。假定我們現在想打印出自己類的地址。看起來似乎簡單地引用 this 即可(特別是 C++程式設計師有這樣做的傾向):
public class CrashJava {
public String toString() {
return "CrashJava address: " + this + "\n";
}
public static void main(String[] args) {
Vector v = new Vector();
for(int i = 0; i < 10; i++)
v.addElement(new CrashJava());
System.out.println(v);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
此時發生的是字串的自動型別轉換。當我們使用下述語句時:
“CrashJava address: ” + this
編譯器就在一個字串後面發現了一個“ +”以及好象並非字串的其他東西,所以它會試圖將 this 轉換成一個字串。轉換時呼叫的是 toString(),後者會產生一個遞迴呼叫。若在一個 Vector 內出現這種事情,看起來堆疊就會溢位,同時違例控制機制根本沒有機會作出響應。
若確實想在這種情況下打印出物件的地址,解決方案就是呼叫 Object 的 toString 方法。此時就不必加入this,只需使用 super.toString()。當然,採取這種做法也有一個前提:我們必須從 Object 直接繼承,或者沒有一個父類覆蓋了 toString 方法。
B i t S e t
BitSet 實際是由“ 二進位制位”構成的一個 Vector。如果希望高效率地儲存大量“開-關”資訊,就應使用BitSet。它只有從尺寸的角度看才有意義;如果希望的高效率的訪問,那麼它的速度會比使用一些固有型別的陣列慢一些。
BitSet 的最小長度是一個長整數( Long)的長度: 64 位。這意味著假如我們準備儲存比這更小的資料,如 8 位資料,那麼 BitSet 就顯得浪費了。所以最好建立自己的類,用它容納自己的標誌位。
S t a c k
Stack 有時也可以稱為“後入先出”( LIFO)集合。換言之,我們在堆疊裡最後“壓入”的東西將是以後第
一個“彈出”的。和其他所有 Java 集合一樣,我們壓入和彈出的都是“物件”,所以必須對自己彈出的東西
進行“造型”。
下面是一個簡單的堆疊示例,它能讀入陣列的每一行,同時將其作為字串壓入堆疊。
public class Stacks {
static String[] months = {
"January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December" };
public static void main(String[] args) {
Stack stk = new Stack();
for(int i = 0; i < months.length; i++)
stk.push(months[i] + " ");
System.out.println("stk = " + stk);
// Treating a stack as a Vector:
stk.addElement("The last line");
System.out.println(
"element 5 = " + stk.elementAt(5));
System.out.println("popping elements:");
while(!stk.empty())
System.out.println(stk.pop());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
months 陣列的每一行都通過 push()繼承進入堆疊,稍後用 pop()從堆疊的頂部將其取出。要宣告的一點是,Vector 操作亦可針對 Stack 物件進行。這可能是由繼承的特質決定的—— Stack“屬於”一種 Vector。因此,能對 Vector 進行的操作亦可針對 Stack 進行,例如 elementAt()方法
H a s h t a b l e
Vector 允許我們用一個數字從一系列物件中作出選擇,所以它實際是將數字同對象關聯起來了。
但假如我們想根據其他標準選擇一系列物件呢?堆疊就是這樣的一個例子:它的選擇標準是“最後壓入堆疊的東西”。
這種“從一系列物件中選擇”的概念亦可叫作一個“對映”、“字典”或者“關聯陣列”。從概念上講,它看起來象一個 Vector,但卻不是通過數字來查詢物件,而是用另一個物件來查詢它們!這通常都屬於一個程式中的重要程序。
在 Java 中,這個概念具體反映到抽象類 Dictionary 身上。該類的介面是非常直觀的 size()告訴我們其中包含了多少元素; isEmpty()判斷是否包含了元素(是則為 true); put(Object key, Object value)新增一個值(我們希望的東西),並將其同一個鍵關聯起來(想用於搜尋它的東西); get(Object key)獲得與某個鍵對應的值;而 remove(Object Key)用於從列表中刪除“鍵-值”對。還可以使用列舉技術: keys()產生對鍵的一個列舉( Enumeration);而 elements()產生對所有值的一個列舉。這便是一個 Dict ionary(字典)的全部。
public class AssocArray extends Dictionary {
private Vector keys = new Vector();
private Vector values = new Vector();
public int size() { return keys.size(); }
public boolean isEmpty() {
return keys.isEmpty();
}
public Object put(Object key, Object value) {
keys.addElement(key);
values.addElement(value);
return key;
}
public Object get(Object key) {
int index = keys.indexOf(key);
// indexOf() Returns -1 if key not found:
if(index == -1) return null;
return values.elementAt(index);
}
public Object remove(Object key) {
int index = keys.indexOf(key);
if(index == -1) return null;
keys.removeElementAt(index);
Object returnval = values.elementAt(index);
values.removeElementAt(index);
return returnval;
}
public Enumeration keys() {
return keys.elements();
}
public Enumeration elements() {
return values.elements();
}
// Test it:
public static void main(String[] args) {
AssocArray aa = new AssocArray();
for(char c = 'a'; c <= 'z'; c++)
aa.put(String.valueOf(c),
String.valueOf(c)
.toUpperCase());
char[] ca = { 'a', 'e', 'i', 'o', 'u' };
for(int i = 0; i < ca.length; i++)
System.out.println("Uppercase: " +
aa.get(String.valueOf(ca[i])));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
在對 AssocArray 的定義中,我們注意到的第一個問題是它“擴充套件”了字典。這意味著 AssocArray 屬於Dictionary 的一種型別,所以可對其發出與 Dictionary 一樣的請求。如果想生成自己的 Dictionary,而且就在這裡進行,那麼要做的全部事情只是填充位於 Dictionary 內的所有方法(而且必須覆蓋所有方法,因為
它們—— 除構建器外—— 都是抽象的)。
標準 Java 庫只包含 Dictionary 的一個變種,名為 Hashtable(散列表,註釋③)。 Java 的散列表具有與AssocArray 相同的介面(因為兩者都是從 Dictionary 繼承來的)。但有一個方面卻反映出了差別:執行效率。若仔細想想必須為一個 get()做的事情,就會發現在一個 Vector 裡搜尋鍵的速度要慢得多。但此時用散列表卻可以加快不少速度。不必用冗長的線性搜尋技術來查詢一個鍵,而是用一個特殊的值,名為“雜湊碼”。雜湊碼可以獲取物件中的資訊,然後將其轉換成那個物件“相對唯一”的整數( int)。所有物件都有一個雜湊碼,而 hashCode()是根類 Object 的一個方法。 Hashtable 獲取物件的 hashCode(),然後用它快速查詢鍵。
class Counter {
int i = 1;
public String toString() {
return Integer.toString(i);
}
}
class Statistics {
public static void main(String[] args) {
Hashtable ht = new Hashtable();
for(int i = 0; i < 10000; i++) {
// Produce a number between 0 and 20:
Integer r =
new Integer((int)(Math.random() * 20));
if(ht.containsKey(r))
((Counter)ht.get(r)).i++;
else
ht.put(r, new Counter());
}
System.out.println(ht);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
1.建立“關鍵”類
但在使用散列表的時候,一旦我們建立自己的類作為鍵使
用,就會遇到一個很常見的問題。例如,假設一套天氣預報系統將Groundhog(土拔鼠)物件匹配成Prediction(預報) 。這看起來非常直觀:我們建立兩個類,然後將Groundhog 作為鍵使用,而將Prediction 作為值使用。如下所示:
class Groundhog {
int ghNumber;
Groundhog(int n) { ghNumber = n; }
}
class Prediction {
boolean shadow = Math.random() > 0.5;
public String toString() {
if(shadow)
return "Six more weeks of Winter!";
else
return "Early Spring!";
}
}
public class SpringDetector {
public static void main(String[] args) {
Hashtable ht = new Hashtable();
for(int i = 0; i < 10; i++)
ht.put(new Groundhog(i), new Prediction());
System.out.println("ht = " + ht + "\n");
System.out.println(
"Looking up prediction for groundhog #3:");
Groundhog gh = new Groundhog(3);
if(ht.containsKey(gh))
System.out.println((Prediction)ht.get(gh));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
問題在於Groundhog 是從通用的 Object 根類繼承的(若當初未指
定基礎類,則所有類最終都是從 Object 繼承的)。事實上是用 Object 的 hashCode()方法生成每個物件的雜湊碼,而且預設情況下只使用它的物件的地址。所以, Groundhog(3)的第一個例項並不會產生與Groundhog(3)第二個例項相等的雜湊碼,而我們用第二個例項進行檢索
或許認為此時要做的全部事情就是正確地覆蓋 hashCode()。但這樣做依然行不能,除非再做另一件事情:覆蓋也屬於 Object 一部分的 equals()。當散列表試圖判斷我們的鍵是否等於表內的某個鍵時,就會用到這個方法。同樣地,預設的 Object.equals()只是簡單地比較物件地址,所以一個 Groundhog(3)並不等於
另一個 Groundhog(3)。
因此,為了在散列表中將自己的類作為鍵使用,必須同時覆蓋 hashCode()和 equals(),就象下面展示的那樣: