Skip to content
This repository has been archived by the owner on Aug 25, 2019. It is now read-only.

Latest commit

 

History

History
2568 lines (2071 loc) · 138 KB

第8章.md

File metadata and controls

2568 lines (2071 loc) · 138 KB

第8章 對象的容納

“如果一個程序只含有數量固定的對象,而且已知它們的存在時間,那麼這個程序可以說是相當簡單的。”

通常,我們的程序需要根據程序運行時才知道的一些標準創建新對象。若非程序正式運行,否則我們根本不知道自己到底需要多少數量的對象,甚至不知道它們的準確類型。為了滿足常規編程的需要,我們要求能在任何時候、任何地點創建任意數量的對象。所以不可依賴一個已命名的引用來容納自己的每一個對象,就象下面這樣:

MyObject myHandle;

因為根本不知道自己實際需要多少這樣的東西。

為解決這個非常關鍵的問題,Java提供了容納對象(或者對象的引用)的多種方式。其中內建的類型是數組,我們之前已討論過它,本章準備加深大家對它的認識。此外,Java的工具(實用程序)庫提供了一些“集合類”(亦稱作“容器類”,但該術語已由AWT使用,所以這裡仍採用“集合”這一稱呼)。利用這些集合類,我們可以容納乃至操縱自己的對象。本章的剩餘部分會就此進行詳細討論。

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;

向我們展示瞭如何取得同一個數組對象連接的引用,然後將其賦給另一個數組對象,就象我們針對對象引用的其他任何類型做的那樣。現在,ad都指向內存堆內同樣的數組對象。

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) 基本數據類型集合

集合類只能容納對象引用。但對一個數組,卻既可令其直接容納基本類型的數據,亦可容納指向對象的引用。利用象IntegerDouble之類的“包裝器”類,可將基本數據類型的值置入一個集合裡。但正如本章後面會在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()方法創建了一個名為resultsString數組。該數組的大小為n——具體數值取決於我們傳遞給方法的參數。隨後,它從數組flav裡隨機挑選一些“香料”(Flavor),並將它們置入results裡,並最終返回results。返回數組與返回其他任何對象沒什麼區別——最終返回的都是一個引用。至於數組到底是在flavorSet()裡創建的,還是在其他什麼地方創建的,這個問題並不重要,因為反正返回的僅是一個引用。一旦我們的操作完成,垃圾收集器會自動關照數組的清除工作。而且只要我們需要數組,它就會乖乖地聽候調遣。

另一方面,注意當flavorSet()隨機挑選香料的時候,它需要保證以前出現過的一次隨機選擇不會再次出現。為達到這個目的,它使用了一個無限while循環,不斷地作出隨機選擇,直到發現未在picks數組裡出現過的一個元素為止(當然,也可以進行字符串比較,檢查隨機選擇是否在results數組裡出現過,但字符串比較的效率比較低)。若成功,就添加這個元素,並中斷循環(break),再查找下一個(i值會遞增)。但假若t是一個已在picks裡出現過的數組,就用標籤式的continue往回跳兩級,強制選擇一個新t。用一個調試程序可以很清楚地看到這個過程。

main()能顯示出20個完整的香料集合,所以我們看到flavorSet()每次都用一個隨機順序選擇香料。為體會這一點,最簡單的方法就是將輸出重導向進入一個文件,然後直接觀看這個文件的內容。

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()方法,可使我們知道已添加了多少個元素,以便防止誤超邊界,造成異常錯誤)。

CatDog類都非常淺顯——除了都是“對象”之外,它們並無特別之處(倘若不明確指出從什麼類繼承,就默認為從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類有一個類型為Vectorprivate成員(從Vector繼承有些麻煩,理由稍後便知),而且方法也和Vector類似。然而,它不會接收和產生普通Object,只對Gopher對象感興趣。

由於GopherVector只接收一個Gopher(地鼠),所以假如我們使用:

gophers.addElement(new Pigeon());

就會在編譯期間獲得一條出錯消息。採用這種方式,儘管從編碼的角度看顯得更令人沉悶,但可以立即判斷出是否使用了正確的類型。

注意在使用elementAt()時不必進行轉換——它肯定是一個Gopher

(3) 參數化類型

這類問題並不是孤立的——我們許多時候都要在其他類型的基礎上創建新類型。此時,在編譯期間擁有特定的類型信息是非常有幫助的。這便是“參數化類型”的概念。在C++中,它由語言通過“模板”獲得了直接支持。至少,Java保留了關鍵字generic,期望有一天能夠支持參數化類型。但我們現在無法確定這一天何時會來臨。

8.3 枚舉器(迭代器)

在任何集合類中,必須通過某種方法在其中置入對象,再用另一種方法從中取得對象。畢竟,容納各種各樣的對象正是集合的首要任務。在Vector中,addElement()便是我們插入對象採用的方法,而elementAt()是提取對象的唯一方法。Vector非常靈活,我們可在任何時候選擇任何東西,並可使用不同的索引選擇多個元素。

若從更高的角度看這個問題,就會發現它的一個缺陷:需要事先知道集合的準確類型,否則無法使用。乍看來,這一點似乎沒什麼關係。但假若最開始決定使用Vector,後來在程序中又決定(考慮執行效率的原因)改變成一個List(屬於Java1.2集合庫的一部分),這時又該如何做呢?

可利用“迭代器”(Iterator)的概念達到這個目的。它可以是一個對象,作用是遍歷一系列對象,並選擇那個序列中的每個對象,同時不讓客戶程序員知道或關注那個序列的基礎結構。此外,我們通常認為迭代器是一種“輕量級”對象;也就是說,創建它只需付出極少的代價。但也正是由於這個原因,我們常發現迭代器存在一些似乎很奇怪的限制。例如,有些迭代器只能朝一個方向移動。 Java的Enumeration(枚舉,註釋②)便是具有這些限制的一個迭代器的例子。除下面這些外,不可再用它做其他任何事情:

(1) 用一個名為elements()的方法要求集合為我們提供一個Enumeration。我們首次調用它的nextElement()時,這個Enumeration會返回序列中的第一個元素。

(2) 用nextElement()獲得下一個對象。

(3) 用hasMoreElements()檢查序列中是否還有更多的對象。

②:“迭代器”這個詞在C++和OOP的其他地方是經常出現的,所以很難確定為什麼Java的開發者採用了這樣一個奇怪的名字。Java 1.2的集合庫修正了這個問題以及其他許多問題。

只可用Enumeration做這些事情,不能再有更多。它屬於迭代器一種簡單的實現方式,但功能依然十分強大。為體會它的運作過程,讓我們複習一下本章早些時候提到的CatsAndDogs.java程序。在原始版本中,elementAt()方法用於選擇每一個元素,但在下述修訂版中,可看到使用了一個“枚舉”:

//: CatsAndDogs2.java
// Simple collection with Enumeration
import java.util.*;

class Cat2 {
  private int catNumber;
  Cat2(int i) {
    catNumber = i;
  }
  void print() {
    System.out.println("Cat number " +catNumber);
  }
}

class Dog2 {
  private int dogNumber;
  Dog2(int i) {
    dogNumber = i;
  }
  void print() {
    System.out.println("Dog number " +dogNumber);
  }
}

public class CatsAndDogs2 {
  public static void main(String[] args) {
    Vector cats = new Vector();
    for(int i = 0; i < 7; i++)
      cats.addElement(new Cat2(i));
    // Not a problem to add a dog to cats:
    cats.addElement(new Dog2(7));
    Enumeration e = cats.elements();
    while(e.hasMoreElements())
      ((Cat2)e.nextElement()).print();
    // Dog is detected only at run-time
  }
} ///:~

我們看到唯一的改變就是最後幾行。不再是:

for(int i = 0; i < cats.size(); i++)
((Cat)cats.elementAt(i)).print();

而是用一個Enumeration遍歷整個序列:

while(e.hasMoreElements())
((Cat2)e.nextElement()).print();

使用Enumeration,我們不必關心集合中的元素數量。所有工作均由hasMoreElements()nextElement()自動照管了。

下面再看看另一個例子,讓我們創建一個常規用途的打印方法:

//: HamsterMaze.java
// Using an Enumeration
import java.util.*;

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());
  }
} ///:~

仔細研究一下打印方法:

static void printAll(Enumeration e) {
  while(e.hasMoreElements())
    System.out.println(
      e.nextElement().toString());
}

注意其中沒有與序列類型有關的信息。我們擁有的全部東西便是Enumeration。為了解有關序列的情況,一個Enumeration便足夠了:可取得下一個對象,亦可知道是否已抵達了末尾。取得一系列對象,然後在其中遍歷,從而執行一個特定的操作——這是一個頗有價值的編程概念,本書許多地方都會沿用這一思路。

這個看似特殊的例子甚至可以更為通用,因為它使用了常規的toString()方法(之所以稱為常規,是由於它屬於Object類的一部分)。下面是調用打印的另一個方法(儘管在效率上可能會差一些):

System.out.println("" + e.nextElement());

它採用了封裝到Java內部的“自動轉換成字符串”技術。一旦編譯器碰到一個字符串,後面跟隨一個+,就會希望後面又跟隨一個字符串,並自動調用toString()。在Java 1.1中,第一個字符串是不必要的;所有對象都會轉換成字符串。亦可對此執行一次轉換,獲得與調用toString()同樣的效果:

System.out.println((String)e.nextElement())

但我們想做的事情通常並不僅僅是調用Object方法,所以會再度面臨類型轉換的問題。對於自己感興趣的類型,必須假定自己已獲得了一個Enumeration,然後將結果對象轉換成為那種類型(若操作錯誤,會得到運行期異常)。

8.4 集合的類型

標準Java 1.0和1.1庫配套提供了非常少的一系列集合類。但對於自己的大多數編程要求,它們基本上都能勝任。正如大家到本章末尾會看到的,Java 1.2提供的是一套重新設計過的大型集合庫。

8.4.1 Vector

Vector的用法很簡單,這已在前面的例子中得到了證明。儘管我們大多數時候只需用addElement()插入對象,用elementAt()一次提取一個對象,並用elements()獲得對序列的一個“枚舉”。但仍有其他一系列方法是非常有用的。同我們對於Java庫慣常的做法一樣,在這裡並不使用或講述所有這些方法。但請務必閱讀相應的電子文檔,對它們的工作有一個大概的認識。

(1) 崩潰Java

Java標準集合裡包含了toString()方法,所以它們能生成自己的String表達方式,包括它們容納的對象。例如在Vector中,toString()會在Vector的各個元素中步進和遍歷,併為每個元素調用toString()。假定我們現在想打印出自己類的地址。看起來似乎簡單地引用this即可(特別是C++程序員有這樣做的傾向):

//: CrashJava.java
// One way to crash Java
import java.util.*;

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);
  }
} ///:~

若只是簡單地創建一個CrashJava對象,並將其打印出來,就會得到無窮無盡的一系列異常錯誤。然而,假如將CrashJava對象置入一個Vector,並象這裡演示的那樣打印Vector,就不會出現什麼錯誤提示,甚至連一個異常都不會出現。此時Java只是簡單地崩潰(但至少它沒有崩潰我的操作系統)。這已在Java 1.1中測試通過。

此時發生的是字符串的自動類型轉換。當我們使用下述語句時:

"CrashJava address: " + this

編譯器就在一個字符串後面發現了一個+以及好象並非字符串的其他東西,所以它會試圖將this轉換成一個字符串。轉換時調用的是toString(),後者會產生一個遞歸調用。若在一個Vector內出現這種事情,看起來棧就會溢出,同時異常控制機制根本沒有機會作出響應。

若確實想在這種情況下打印出對象的地址,解決方案就是調用ObjecttoString方法。此時就不必加入this,只需使用super.toString()。當然,採取這種做法也有一個前提:我們必須從Object直接繼承,或者沒有一個父類覆蓋了toString方法。

8.4.2 BitSet

BitSet實際是由“二進制位”構成的一個Vector。如果希望高效率地保存大量“開-關”信息,就應使用BitSet。它只有從尺寸的角度看才有意義;如果希望的高效率的訪問,那麼它的速度會比使用一些固有類型的數組慢一些。

此外,BitSet的最小長度是一個長整數(Long)的長度:64位。這意味著假如我們準備保存比這更小的數據,如8位數據,那麼BitSet就顯得浪費了。所以最好創建自己的類,用它容納自己的標誌位。

在一個普通的Vector中,隨我們加入越來越多的元素,集合也會自我膨脹。在某種程度上,BitSet也不例外。也就是說,它有時會自行擴展,有時則不然。而且Java的1.0版本似乎在這方面做得最糟,它的BitSet表現十分差強人意(Java1.1已改正了這個問題)。下面這個例子展示了BitSet是如何運作的,同時演示了1.0版本的錯誤:

//: Bits.java
// Demonstration of BitSet
import java.util.*;

public class Bits {
  public static void main(String[] args) {
    Random rand = new Random();
    // Take the LSB of nextInt():
    byte bt = (byte)rand.nextInt();
    BitSet bb = new BitSet();
    for(int i = 7; i >=0; i--)
      if(((1 << i) &  bt) != 0)
        bb.set(i);
      else
        bb.clear(i);
    System.out.println("byte value: " + bt);
    printBitSet(bb);

    short st = (short)rand.nextInt();
    BitSet bs = new BitSet();
    for(int i = 15; i >=0; i--)
      if(((1 << i) &  st) != 0)
        bs.set(i);
      else
        bs.clear(i);
    System.out.println("short value: " + st);
    printBitSet(bs);

    int it = rand.nextInt();
    BitSet bi = new BitSet();
    for(int i = 31; i >=0; i--)
      if(((1 << i) &  it) != 0)
        bi.set(i);
      else
        bi.clear(i);
    System.out.println("int value: " + it);
    printBitSet(bi);

    // Test bitsets >= 64 bits:
    BitSet b127 = new BitSet();
    b127.set(127);
    System.out.println("set bit 127: " + b127);
    BitSet b255 = new BitSet(65);
    b255.set(255);
    System.out.println("set bit 255: " + b255);
    BitSet b1023 = new BitSet(512);
// Without the following, an exception is thrown
// in the Java 1.0 implementation of BitSet:
//    b1023.set(1023);
    b1023.set(1024);
    System.out.println("set bit 1023: " + b1023);
  }
  static void printBitSet(BitSet b) {
    System.out.println("bits: " + b);
    String bbits = new String();
    for(int j = 0; j < b.size() ; j++)
      bbits += (b.get(j) ? "1" : "0");
    System.out.println("bit pattern: " + bbits);
  }
} ///:~

隨機數字生成器用於創建一個隨機的byteshortint。每一個都會轉換成BitSet內相應的位模型。此時一切都很正常,因為BitSet是64位的,所以它們都不會造成最終尺寸的增大。但在Java 1.0中,一旦BitSet大於64位,就會出現一些令人迷惑不解的行為。假如我們設置一個只比BitSet當前分配存儲空間大出1的一個位,它能夠正常地擴展。但一旦試圖在更高的位置設置位,同時不先接觸邊界,就會得到一個惱人的異常。這正是由於BitSet在Java 1.0裡不能正確擴展造成的。本例創建了一個512位的BitSet。構造器分配的存儲空間是位數的兩倍。所以假如設置位1024或更高的位,同時沒有先設置位1023,就會在Java 1.0裡得到一個異常。但幸運的是,這個問題已在Java 1.1得到了改正。所以如果是為Java 1.0寫代碼,請儘量避免使用BitSet

8.4.3 Stack

Stack有時也可以稱為“後入先出”(LIFO)集合。換言之,我們在棧裡最後“壓入”的東西將是以後第一個“彈出”的。和其他所有Java集合一樣,我們壓入和彈出的都是“對象”,所以必須對自己彈出的東西進行“轉換”。

一種很少見的做法是拒絕使用Vector作為一個Stack的基本構成元素,而是從Vector裡“繼承”一個Stack。這樣一來,它就擁有了一個Vector的所有特徵及行為,另外加上一些額外的Stack行為。很難判斷出設計者到底是明確想這樣做,還是屬於一種固有的設計。

下面是一個簡單的棧示例,它能讀入數組的每一行,同時將其作為字符串壓入棧。

//: Stacks.java
// Demonstration of Stack Class
import java.util.*;

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());
  }
} ///:~

months數組的每一行都通過push()繼承進入棧,稍後用pop()從棧的頂部將其取出。要聲明的一點是,Vector操作亦可針對Stack對象進行。這可能是由繼承的特質決定的——Stack“屬於”一種Vector。因此,能對Vector進行的操作亦可針對Stack進行,例如elementAt()方法。

8.4.4 Hashtable

Vector允許我們用一個數字從一系列對象中作出選擇,所以它實際是將數字同對象關聯起來了。但假如我們想根據其他標準選擇一系列對象呢?棧就是這樣的一個例子:它的選擇標準是“最後壓入棧的東西”。這種“從一系列對象中選擇”的概念亦可叫作一個“映射”、“字典”或者“關聯數組”。從概念上講,它看起來象一個Vector,但卻不是通過數字來查找對象,而是用另一個對象來查找它們!這通常都屬於一個程序中的重要進程。

在Java中,這個概念具體反映到抽象類Dictionary身上。該類的接口是非常直觀的size()告訴我們其中包含了多少元素;isEmpty()判斷是否包含了元素(是則為true);put(Object key, Object value)添加一個值(我們希望的東西),並將其同一個鍵關聯起來(想用於搜索它的東西);get(Object key)獲得與某個鍵對應的值;而remove(Object Key)用於從列表中刪除“鍵-值”對。還可以使用枚舉技術:keys()產生對鍵的一個枚舉(Enumeration);而elements()產生對所有值的一個枚舉。這便是一個Dictionary(字典)的全部。

Dictionary的實現過程並不麻煩。下面列出一種簡單的方法,它使用了兩個Vector,一個用於容納鍵,另一個用來容納值:

//: AssocArray.java
// Simple version of a Dictionary
import java.util.*;

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])));
  }
} ///:~

在對AssocArray的定義中,我們注意到的第一個問題是它“擴展”了字典。這意味著AssocArray屬於Dictionary的一種類型,所以可對其發出與Dictionary一樣的請求。如果想生成自己的Dictionary,而且就在這裡進行,那麼要做的全部事情只是填充位於Dictionary內的所有方法(而且必須覆蓋所有方法,因為它們——除構造器外——都是抽象的)。

Vector keyvalue通過一個標準索引編號鏈接起來。也就是說,如果用roof的一個鍵以及blue的一個值調用put()——假定我們準備將一個房子的各部分與它們的油漆顏色關聯起來,而且AssocArray裡已有100個元素,那麼roof就會有101個鍵元素,而blue有101個值元素。而且要注意一下get(),假如我們作為鍵傳遞roof,它就會產生與keys.index.Of()的索引編號,然後用那個索引編號生成相關的值向量內的值。

main()中進行的測試是非常簡單的;它只是將小寫字符轉換成大寫字符,這顯然可用更有效的方式進行。但它向我們揭示出了AssocArray的強大功能。

標準Java庫只包含Dictionary的一個變種,名為Hashtable(散列表,註釋③)。Java的散列表具有與AssocArray相同的接口(因為兩者都是從Dictionary繼承來的)。但有一個方面卻反映出了差別:執行效率。若仔細想想必須為一個get()做的事情,就會發現在一個Vector裡搜索鍵的速度要慢得多。但此時用散列表卻可以加快不少速度。不必用冗長的線性搜索技術來查找一個鍵,而是用一個特殊的值,名為“散列碼”。散列碼可以獲取對象中的信息,然後將其轉換成那個對象“相對唯一”的整數(int)。所有對象都有一個散列碼,而hashCode()是根類Object的一個方法。Hashtable獲取對象的hashCode(),然後用它快速查找鍵。這樣可使性能得到大幅度提升(④)。散列表的具體工作原理已超出了本書的範圍(⑤)——大家只需要知道散列表是一種快速的“字典”(Dictionary)即可,而字典是一種非常有用的工具。

③:如計劃使用RMI(在第15章詳述),應注意將遠程對象置入散列表時會遇到一個問題(參閱《Core Java》,作者Conrell和Horstmann,Prentice-Hall 1997年出版)

④:如這種速度的提升仍然不能滿足你對性能的要求,甚至可以編寫自己的散列表例程,從而進一步加快表格的檢索過程。這樣做可避免在與Object之間進行轉換的時間延誤,也可以避開由Java類庫散列表例程內建的同步過程。

⑤:我的知道的最佳參考讀物是《Practical Algorithms for Programmers》,作者為Andrew Binstock和John Rex,Addison-Wesley 1995年出版。

作為應用散列表的一個例子,可考慮用一個程序來檢驗Java的Math.random()方法的隨機性到底如何。在理想情況下,它應該產生一系列完美的隨機分佈數字。但為了驗證這一點,我們需要生成數量眾多的隨機數字,然後計算落在不同範圍內的數字多少。散列表可以極大簡化這一工作,因為它能將對象同對象關聯起來(此時是將Math.random()生成的值同那些值出現的次數關聯起來)。如下所示:

//: Statistics.java
// Simple demonstration of Hashtable
import java.util.*;

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);
  }
} ///:~

main()中,每次產生一個隨機數字,它都會封裝到一個Integer對象裡,使引用能夠隨同散列表一起使用(不可對一個集合使用基本數據類型,只能使用對象引用)。containKey()方法檢查這個鍵是否已經在集合裡(也就是說,那個數字以前發現過嗎?)若已在集合裡,則get()方法獲得那個鍵關聯的值,此時是一個Counter(計數器)對象。計數器內的值i隨後會增加1,表明這個特定的隨機數字又出現了一次。

假如鍵以前尚未發現過,那麼方法put()仍然會在散列表內置入一個新的“鍵-值”對。在創建之初,Counter會自己的變量i自動初始化為1,它標誌著該隨機數字的第一次出現。

為顯示散列表,只需把它簡單地打印出來即可。Hashtable toString()方法能遍歷所有鍵-值對,併為每一對都調用toString()Integer toString()是事先定義好的,可看到計數器使用的toString。一次運行的結果(添加了一些換行)如下:

{19=526, 18=533, 17=460, 16=513, 15=521, 14=495,
 13=512, 12=483, 11=488, 10=487, 9=514, 8=523,
 7=497, 6=487, 5=480, 4=489, 3=509, 2=503, 1=475,
 0=505}

大家或許會對Counter類是否必要感到疑惑,它看起來似乎根本沒有封裝類Integer的功能。為什麼不用intInteger呢?事實上,由於所有集合能容納的僅有對象引用,所以根本不可以使用整數。學過集合後,封裝類的概念對大家來說就可能更容易理解了,因為不可以將任何基本數據類型置入集合裡。然而,我們對Java包裝器能做的唯一事情就是將其初始化成一個特定的值,然後讀取那個值。也就是說,一旦包裝器對象已經創建,就沒有辦法改變一個值。這使得Integer包裝器對解決我們的問題毫無意義,所以不得不創建一個新類,用它來滿足自己的要求。

(1) 創建“關鍵”類

在前面的例子裡,我們用一個標準庫的類(Integer)作為Hashtable的一個鍵使用。作為一個鍵,它能很好地工作,因為它已經具備正確運行的所有條件。但在使用散列表的時候,一旦我們創建自己的類作為鍵使用,就會遇到一個很常見的問題。例如,假設一套天氣預報系統將Groundhog(土拔鼠)對象匹配成Prediction(預報)。這看起來非常直觀:我們創建兩個類,然後將Groundhog作為鍵使用,而將Prediction作為值使用。如下所示:

//: SpringDetector.java
// Looks plausible, but doesn't work right.
import java.util.*;

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));
  }
} ///:~

每個Groundhog都具有一個標識號碼,所以赤了在散列表中查找一個Prediction,只需指示它“告訴我與Groundhog號碼3相關的Prediction”。Prediction類包含了一個布爾值,用Math.random()進行初始化,以及一個toString()為我們解釋結果。在main()中,用Groundhog以及與它們相關的Prediction填充一個散列表。散列表被打印出來,以便我們看到它們確實已被填充。隨後,用標識號碼為3的一個Groundhog查找與Groundhog #3對應的預報。

看起來似乎非常簡單,但實際是不可行的。問題在於Groundhog是從通用的Object根類繼承的(若當初未指定基類,則所有類最終都是從Object繼承的)。事實上是用ObjecthashCode()方法生成每個對象的散列碼,而且默認情況下只使用它的對象的地址。所以,Groundhog(3)的第一個實例並不會產生與Groundhog(3)第二個實例相等的散列碼,而我們用第二個實例進行檢索。

大家或許認為此時要做的全部事情就是正確地覆蓋hashCode()。但這樣做依然行不能,除非再做另一件事情:覆蓋也屬於Object一部分的equals()。當散列表試圖判斷我們的鍵是否等於表內的某個鍵時,就會用到這個方法。同樣地,默認的Object.equals()只是簡單地比較對象地址,所以一個Groundhog(3)並不等於另一個Groundhog(3)

因此,為了在散列表中將自己的類作為鍵使用,必須同時覆蓋hashCode()equals(),就象下面展示的那樣:

//: SpringDetector2.java
// If you create a class that's used as a key in
// a Hashtable, you must override hashCode()
// and equals().
import java.util.*;

class Groundhog2 {
  int ghNumber;
  Groundhog2(int n) { ghNumber = n; }
  public int hashCode() { return ghNumber; }
  public boolean equals(Object o) {
    return (o instanceof Groundhog2)
      && (ghNumber == ((Groundhog2)o).ghNumber);
  }
}

public class SpringDetector2 {
  public static void main(String[] args) {
    Hashtable ht = new Hashtable();
    for(int i = 0; i < 10; i++)
      ht.put(new Groundhog2(i),new Prediction());
    System.out.println("ht = " + ht + "\n");
    System.out.println(
      "Looking up prediction for groundhog #3:");
    Groundhog2 gh = new Groundhog2(3);
    if(ht.containsKey(gh))
      System.out.println((Prediction)ht.get(gh));
  }
} ///:~

注意這段代碼使用了來自前一個例子的Prediction,所以SpringDetector.java必須首先編譯,否則就會在試圖編譯SpringDetector2.java時得到一個編譯期錯誤。

Groundhog2.hashCode()將土拔鼠號碼作為一個標識符返回(在這個例子中,程序員需要保證沒有兩個土拔鼠用同樣的ID號碼並存)。為了返回一個獨一無二的標識符,並不需要hashCode()equals()方法必須能夠嚴格判斷兩個對象是否相等。

equals()方法要進行兩種檢查:檢查對象是否為null;若不為null,則繼續檢查是否為Groundhog2的一個實例(要用到instanceof關鍵字,第11章會詳加論述)。即使為了繼續執行equals(),它也應該是一個Groundhog2。正如大家看到的那樣,這種比較建立在實際ghNumber的基礎上。這一次一旦我們運行程序,就會看到它終於產生了正確的輸出(許多Java庫的類都覆蓋了hashcode()equals()方法,以便與自己提供的內容適應)。

(2) 屬性:Hashtable的一種類型

在本書的第一個例子中,我們使用了一個名為Properties(屬性)的Hashtable類型。在那個例子中,下述程序行:

Properties p = System.getProperties();
p.list(System.out);

調用了一個名為getProperties()static方法,用於獲得一個特殊的Properties對象,對系統的某些特徵進行描述。list()屬於Properties的一個方法,可將內容發給我們選擇的任何流式輸出。也有一個save()方法,可用它將屬性列表寫入一個文件,以便日後用load()方法讀取。

儘管Properties類是從Hashtable繼承的,但它也包含了一個散列表,用於容納“默認”屬性的列表。所以假如沒有在主列表裡找到一個屬性,就會自動搜索默認屬性。

Properties類亦可在我們的程序中使用(第17章的ClassScanner.java便是一例)。在Java庫的用戶文檔中,往往可以找到更多、更詳細的說明。

8.4.5 再論枚舉器

我們現在可以開始演示Enumeration(枚舉)的真正威力:將穿越一個序列的操作與那個序列的基礎結構分隔開。在下面的例子裡,PrintData類用一個Enumeration在一個序列中移動,併為每個對象都調用toString()方法。此時創建了兩個不同類型的集合:一個Vector和一個Hashtable。並且在它們裡面分別填充MouseHamster對象(本章早些時候已定義了這些類;注意必須先編譯HamsterMaze.javaWorksAnyway.java,否則下面的程序不能編譯)。由於Enumeration隱藏了基層集合的結構,所以PrintData不知道或者不關心Enumeration來自於什麼類型的集合:

//: Enumerators2.java
// Revisiting Enumerations
import java.util.*;

class PrintData {
  static void print(Enumeration e) {
    while(e.hasMoreElements())
      System.out.println(
        e.nextElement().toString());
  }
}

class Enumerators2 {
  public static void main(String[] args) {
    Vector v = new Vector();
    for(int i = 0; i < 5; i++)
      v.addElement(new Mouse(i));

    Hashtable h = new Hashtable();
    for(int i = 0; i < 5; i++)
      h.put(new Integer(i), new Hamster(i));

    System.out.println("Vector");
    PrintData.print(v.elements());
    System.out.println("Hashtable");
    PrintData.print(h.elements());
  }
} ///:~

注意PrintData.print()利用了這些集合中的對象屬於Object類這一事實,所以它調用了toString()。但在解決自己的實際問題時,經常都要保證自己的Enumeration穿越某種特定類型的集合。例如,可能要求集合中的所有元素都是一個Shape(幾何形狀),並含有draw()方法。若出現這種情況,必須從Enumeration.nextElement()返回的Object進行向下轉換,以便產生一個Shape

8.5 排序

Java 1.0和1.1庫都缺少的一樣東西是算術運算,甚至沒有最簡單的排序運算方法。因此,我們最好創建一個Vector,利用經典的Quicksort(快速排序)方法對其自身進行排序。

編寫通用的排序代碼時,面臨的一個問題是必須根據對象的實際類型來執行比較運算,從而實現正確的排序。當然,一個辦法是為每種不同的類型都寫一個不同的排序方法。然而,應認識到假若這樣做,以後增加新類型時便不易實現代碼的重複利用。

程序設計一個主要的目標就是“將發生變化的東西同保持不變的東西分隔開”。在這裡,保持不變的代碼是通用的排序算法,而每次使用時都要變化的是對象的實際比較方法。因此,我們不可將比較代碼“硬編碼”到多個不同的排序例程內,而是採用“回調”技術。利用回調,經常發生變化的那部分代碼會封裝到它自己的類內,而總是保持相同的代碼則“回調”發生變化的代碼。這樣一來,不同的對象就可以表達不同的比較方式,同時向它們傳遞相同的排序代碼。

下面這個“接口”(Interface)展示瞭如何比較兩個對象,它將那些“要發生變化的東西”封裝在內:

//: Compare.java
// Interface for sorting callback:
package c08;

interface Compare {
  boolean lessThan(Object lhs, Object rhs);
  boolean lessThanOrEqual(Object lhs, Object rhs);
} ///:~

對這兩種方法來說,lhs代表本次比較中的“左手”對象,而rhs代表“右手”對象。

可創建Vector的一個子類,通過Compare實現“快速排序”。對於這種算法,包括它的速度以及原理等等,在此不具體說明。欲知詳情,可參考Binstock和Rex編著的《Practical Algorithms for Programmers》,由Addison-Wesley於1995年出版。

//: SortVector.java
// A generic sorting vector
package c08;
import java.util.*;

public class SortVector extends Vector {
  private Compare compare; // To hold the callback
  public SortVector(Compare comp) {
    compare = comp;
  }
  public void sort() {
    quickSort(0, size() - 1);
  }
  private void quickSort(int left, int right) {
    if(right > left) {
      Object o1 = elementAt(right);
      int i = left - 1;
      int j = right;
      while(true) {
        while(compare.lessThan(
              elementAt(++i), o1))
          ;
        while(j > 0)
          if(compare.lessThanOrEqual(
             elementAt(--j), o1))
            break; // out of while
        if(i >= j) break;
        swap(i, j);
      }
      swap(i , right);
      quickSort(left, i-1);
      quickSort(i+1, right);
    }
  }
  private void swap(int loc1, int loc2) {
    Object tmp = elementAt(loc1);
    setElementAt(elementAt(loc2), loc1);
    setElementAt(tmp, loc2);
  }
} ///:~

現在,大家可以明白“回調”一詞的來歷,這是由於quickSort()方法“往回調用”了Compare中的方法。從中亦可理解這種技術如何生成通用的、可重複利用(複用)的代碼。

為使用SortVector,必須創建一個類,令其為我們準備排序的對象實現Compare。此時內部類並不顯得特別重要,但對於代碼的組織卻是有益的。下面是針對String對象的一個例子:

//: StringSortTest.java
// Testing the generic sorting Vector
package c08;
import java.util.*;

public class StringSortTest {
  static class StringCompare implements Compare {
    public boolean lessThan(Object l, Object r) {
      return ((String)l).toLowerCase().compareTo(
        ((String)r).toLowerCase()) < 0;
    }
    public boolean
    lessThanOrEqual(Object l, Object r) {
      return ((String)l).toLowerCase().compareTo(
        ((String)r).toLowerCase()) <= 0;
    }
  }
  public static void main(String[] args) {
    SortVector sv =
      new SortVector(new StringCompare());
    sv.addElement("d");
    sv.addElement("A");
    sv.addElement("C");
    sv.addElement("c");
    sv.addElement("b");
    sv.addElement("B");
    sv.addElement("D");
    sv.addElement("a");
    sv.sort();
    Enumeration e = sv.elements();
    while(e.hasMoreElements())
      System.out.println(e.nextElement());
  }
} ///:~

內部類是“靜態”(Static)的,因為它毋需連接一個外部類即可工作。

大家可以看到,一旦設置好框架,就可以非常方便地重複使用象這樣的一個設計——只需簡單地寫一個類,將“需要發生變化”的東西封裝進去,然後將一個對象傳給SortVector即可。

比較時將字符串強制為小寫形式,所以大寫A會排列於小寫a的旁邊,而不會移動一個完全不同的地方。然而,該例也顯示了這種方法的一個不足,因為上述測試代碼按照出現順序排列同一個字母的大寫和小寫形式:A a b B c C d D。但這通常不是一個大問題,因為經常處理的都是更長的字符串,所以上述效果不會顯露出來(Java 1.2的集合提供了排序功能,已解決了這個問題)。

繼承(extends)在這兒用於創建一種新類型的Vector——也就是說,SortVector屬於一種Vector,並帶有一些附加的功能。繼承在這裡可發揮很大的作用,但了帶來了問題。它使一些方法具有了final屬性(已在第7章講述),所以不能覆蓋它們。如果想創建一個排好序的Vector,令其只接收和生成String對象,就會遇到麻煩。因為addElement()elementAt()都具有final屬性,而且它們都是我們必須覆蓋的方法,否則便無法實現只能接收和產生String對象。

但在另一方面,請考慮採用“組合”方法:將一個對象置入一個新類的內部。此時,不是改寫上述代碼來達到這個目的,而是在新類裡簡單地使用一個SortVector。在這種情況下,用於實現Compare接口的內部類就可以“匿名”地創建。如下所示:

//: StrSortVector.java
// Automatically sorted Vector that
// accepts and produces only Strings
package c08;
import java.util.*;

public class StrSortVector {
  private SortVector v = new SortVector(
    // Anonymous inner class:
    new Compare() {
      public boolean
      lessThan(Object l, Object r) {
        return
          ((String)l).toLowerCase().compareTo(
          ((String)r).toLowerCase()) < 0;
      }
      public boolean
      lessThanOrEqual(Object l, Object r) {
        return
          ((String)l).toLowerCase().compareTo(
          ((String)r).toLowerCase()) <= 0;
      }
    }
  );
  private boolean sorted = false;
  public void addElement(String s) {
    v.addElement(s);
    sorted = false;
  }
  public String elementAt(int index) {
    if(!sorted) {
      v.sort();
      sorted = true;
    }
    return (String)v.elementAt(index);
  }
  public Enumeration elements() {
    if(!sorted) {
      v.sort();
      sorted = true;
    }
    return v.elements();
  }
  // Test it:
  public static void main(String[] args) {
    StrSortVector sv = new StrSortVector();
    sv.addElement("d");
    sv.addElement("A");
    sv.addElement("C");
    sv.addElement("c");
    sv.addElement("b");
    sv.addElement("B");
    sv.addElement("D");
    sv.addElement("a");
    Enumeration e = sv.elements();
    while(e.hasMoreElements())
      System.out.println(e.nextElement());
  }
} ///:~

這樣便可快速複用來自SortVector的代碼,從而獲得希望的功能。然而,並不是來自SortVectorVector的所有public方法都能在StrSortVector中出現。若按這種形式複用代碼,可在新類裡為包含類內的每一個方法都生成一個定義。當然,也可以在剛開始時只添加少數幾個,以後根據需要再添加更多的。新類的設計最終會穩定下來。

這種方法的好處在於它仍然只接納String對象,也只產生String對象。而且相應的檢查是在編譯期間進行的,而非在運行期。當然,只有addElement()elementAt()才具備這一特性;elements()仍然會產生一個Enumeration(枚舉),它在編譯期的類型是未定的。當然,對Enumeration以及在StrSortVector中的類型檢查會照舊進行;如果真的有什麼錯誤,運行期間會簡單地產生一個異常。事實上,我們在編譯或運行期間能保證一切都正確無誤嗎?(也就是說,“代碼測試時也許不能保證”,以及“該程序的用戶有可能做一些未經我們測試的事情”)。儘管存在其他選擇和爭論,使用繼承都要容易得多,只是在轉換時讓人深感不便。同樣地,一旦為Java加入參數化類型,就有望解決這個問題。

大家在這個類中可以看到有一個名為sorted的標誌。每次調用addElement()時,都可對Vector進行排序,而且將其連續保持在一個排好序的狀態。但在開始讀取之前,人們總是向一個Vector添加大量元素。所以與其在每個addElement()後排序,不如一直等到有人想讀取Vector,再對其進行排序。後者的效率要高得多。這種除非絕對必要,否則就不採取行動的方法叫作“懶惰求值”(還有一種類似的技術叫作“懶惰初始化”——除非真的需要一個字段值,否則不進行初始化)。

8.6 通用集合庫

通過本章的學習,大家已知道標準Java庫提供了一些特別有用的集合,但距完整意義的集合尚遠。除此之外,象排序這樣的算法根本沒有提供支持。C++出色的一個地方就是它的庫,特別是“標準模板庫”(STL)提供了一套相當完整的集合,以及許多象排序和檢索這樣的算法,可以非常方便地對那些集合進行操作。有感這一現狀,並以這個模型為基礎,ObjectSpace公司設計了Java版本的“通用集合庫”(從前叫作“Java通用庫”,即JGL;但JGL這個縮寫形式侵犯了Sun公司的版權——儘管本書仍然沿用這個簡稱)。這個庫儘可能遵照STL的設計(照顧到兩種語言間的差異)。JGL實現了許多功能,可滿足對一個集合庫的大多數常規需求,它與C++的模板機制非常相似。JGL包括相互鏈接起來的列表、設置、隊列、映射、棧、序列以及迭代器,它們的功能比Enumeration(枚舉)強多了。同時提供了一套完整的算法,如檢索和排序等。在某些方面,ObjectSpace的設計也顯得比Sun的庫設計模式“智能”一些。舉個例子來說,JGL集合中的方法不會進入final狀態,所以很容易繼承和改寫那些方法。

JGL已包括到一些廠商發行的Java套件中,而且ObjectSpace公司自己也允許所有用戶免費使用JGL,包括商業性的使用。詳細情況和軟件下載可訪問 http://www.ObjectSpace.com 。與JGL配套提供的聯機文檔做得非常好,可作為自己的一個絕佳起點使用。

8.7 新集合

對我來說,集合類屬於最強大的一種工具,特別適合在原創編程中使用。大家可能已感覺到我對Java 1.1提供的集合多少有點兒失望。因此,看到Java 1.2對集合重新引起了正確的注意後,確實令人非常愉快。這個版本的集合也得到了完全的重新設計(由Sun公司的Joshua Bloch)。我認為新設計的集合是Java 1.2中兩項最主要的特性之一(另一項是Swing庫,將在第13章敘述),因為它們極大方便了我們的編程,也使Java變成一種更成熟的編程系統。

有些設計使得元素間的結合變得更緊密,也更容易讓人理解。例如,許多名字都變得更短、更明確了,而且更易使用;類型同樣如此。有些名字進行了修改,更接近於通俗:我感覺特別好的一個是用“迭代器”(Inerator)代替了“枚舉”(Enumeration)。

此次重新設計也加強了集合庫的功能。現在新增的行為包括鏈接列表、隊列以及撤消組隊(即“雙終點隊列”)。

集合庫的設計是相當困難的(會遇到大量庫設計問題)。在C++中,STL用多個不同的類來覆蓋基礎。這種做法比起STL以前是個很大的進步,那時根本沒做這方面的考慮。但仍然沒有很好地轉換到Java裡面。結果就是一大堆特別容易混淆的類。在另一個極端,我曾發現一個集合庫由單個類構成:collection,它同時作為VectorHashtable使用。新集合庫的設計者則希望達到一種新的平衡:實現人們希望從一個成熟集合庫上獲得的完整功能,同時又要比STL和其他類似的集合庫更易學習和使用。這樣得到的結果在某些場合顯得有些古怪。但和早期Java庫的一些決策不同,這些古怪之處並非偶然出現的,而是以複雜性作為代價,在進行仔細權衡之後得到的結果。這樣做也許會延長人們掌握一些庫概念的時間,但很快就會發現自己很樂於使用那些新工具,而且變得越來越離不了它。

新的集合庫考慮到了“容納自己對象”的問題,並將其分割成兩個明確的概念:

(1) 集合(Collection):一組單獨的元素,通常應用了某種規則。在這裡,一個List(列表)必須按特定的順序容納元素,而一個Set(集)不可包含任何重複的元素。相反,“包”(Bag)的概念未在新的集合庫中實現,因為“列表”已提供了類似的功能。

(2) 映射(Map):一系列“鍵-值”對(這已在散列表身上得到了充分的體現)。從表面看,這似乎應該成為一個“鍵-值”對的“集合”,但假若試圖按那種方式實現它,就會發現實現過程相當笨拙。這進一步證明了應該分離成單獨的概念。另一方面,可以方便地查看Map的某個部分。只需創建一個集合,然後用它表示那一部分即可。這樣一來,Map就可以返回自己鍵的一個Set、一個包含自己值的List或者包含自己“鍵-值”對的一個List。和數組相似,Map可方便擴充到多個“維”,毋需涉及任何新概念。只需簡單地在一個Map裡包含其他Map(後者又可以包含更多的Map,以此類推)。

CollectionMap可通過多種形式實現,具體由編程要求決定。下面列出的是一個幫助大家理解的新集合示意圖:

這張圖剛開始的時候可能讓人有點兒摸不著頭腦,但在通讀了本章以後,相信大家會真正理解它實際只有三個集合組件:MapListSet。而且每個組件實際只有兩、三種實現方式(註釋⑥),而且通常都只有一種特別好的方式。只要看出了這一點,集合就不會再令人生畏。

⑥:寫作本章時,Java 1.2尚處於β測試階段,所以這張示意圖沒有包括以後會加入的TreeSet

虛線框代表“接口”,點線框代表“抽象”類,而實線框代表普通(實際)類。點線箭頭表示一個特定的類準備實現一個接口(在抽象類的情況下,則是“部分”實現一個接口)。雙線箭頭表示一個類可生成箭頭指向的那個類的對象。例如,任何集合都可以生成一個迭代器(Iterator),而一個列表可以生成一個ListIterator(以及原始的迭代器,因為列表是從集合繼承的)。

致力於容納對象的接口是CollectionListSetMap。在傳統情況下,我們需要寫大量代碼才能同這些接口打交道。而且為了指定自己想使用的準確類型,必須在創建之初進行設置。所以可能創建下面這樣的一個List

List x = new LinkedList();

當然,也可以決定將x作為一個LinkedList使用(而不是一個普通的List),並用x負載準確的類型信息。使用接口的好處就是一旦決定改變自己的實現細節,要做的全部事情就是在創建的時候改變它,就象下面這樣:

List x = new ArrayList();

其餘代碼可以保持原封不動。

在類的分級結構中,可看到大量以Abstract(抽象)開頭的類,這剛開始可能會使人感覺迷惑。它們實際上是一些工具,用於“部分”實現一個特定的接口。舉個例子來說,假如想生成自己的Set,就不是從Set接口開始,然後自行實現所有方法。相反,我們可以從AbstractSet繼承,只需極少的工作即可得到自己的新類。儘管如此,新集合庫仍然包含了足夠的功能,可滿足我們的幾乎所有需求。所以考慮到我們的目的,可忽略所有以Abstract開頭的類。

因此,在觀看這張示意圖時,真正需要關心的只有位於最頂部的“接口”以及普通(實際)類——均用實線方框包圍。通常需要生成實際類的一個對象,將其向上轉換為對應的接口。以後即可在代碼的任何地方使用那個接口。下面是一個簡單的例子,它用String對象填充一個集合,然後打印出集合內的每一個元素:

//: SimpleCollection.java
// A simple example using the new Collections
package c08.newcollections;
import java.util.*;

public class SimpleCollection {
  public static void main(String[] args) {
    Collection c = new ArrayList();
    for(int i = 0; i < 10; i++)
      c.add(Integer.toString(i));
    Iterator it = c.iterator();
    while(it.hasNext())
      System.out.println(it.next());
  }
} ///:~

新集合庫的所有代碼示例都置於子目錄newcollections下,這樣便可提醒自己這些工作只對於Java 1.2有效。這樣一來,我們必須用下述代碼來調用程序:

java c08.newcollections.SimpleCollection

採用的語法與其他程序是差不多的。

大家可以看到新集合屬於java.util庫的一部分,所以在使用時不需要再添加任何額外的import語句。

main()的第一行創建了一個ArrayList對象,然後將其向上轉換成為一個集合。由於這個例子只使用了Collection方法,所以從Collection繼承的一個類的任何對象都可以正常工作。但ArrayList是一個典型的Collection,它代替了Vector的位置。 顯然,add()方法的作用是將一個新元素置入集合裡。然而,用戶文檔謹慎地指出add()“保證這個集合包含了指定的元素”。這一點是為Set作鋪墊的,後者只有在元素不存在的前提下才會真的加入那個元素。對於ArrayList以及其他任何形式的Listadd()肯定意味著“直接加入”。

利用iterator()方法,所有集合都能生成一個“迭代器”(Iterator)。迭代器其實就象一個“枚舉”(Enumeration),是後者的一個替代物,只是:

(1) 它採用了一個歷史上默認、而且早在OOP中得到廣泛採納的名字(迭代器)。

(2) 採用了比Enumeration更短的名字:hasNext()代替了hasMoreElement(),而next()代替了nextElement()

(3) 添加了一個名為remove()的新方法,可刪除由Iterator生成的上一個元素。所以每次調用next()的時候,只需調用remove()一次。

SimpleCollection.java中,大家可看到創建了一個迭代器,並用它在集合裡遍歷,打印出每個元素。

8.7.1 使用Collections

下面這張表格總結了用一個集合能做的所有事情(亦可對SetList做同樣的事情,儘管List還提供了一些額外的功能)。Map不是從Collection繼承的,所以要單獨對待。

Boolean add(Object)

*Ensures that the Collection contains the argument. Returns false if it doesn’t add the argument.

Boolean addAll(Collection)

*Adds all the elements in the argument. Returns true if any elements were added.

void clear( )

*Removes all the elements in the Collection.

Boolean contains(Object)

True if the Collection contains the argument.

Boolean containsAll(Collection)

True if the Collection contains all the elements in the argument.

Boolean isEmpty( )

True if the Collection has no elements.

Iterator iterator( )

Returns an Iterator that you can use to move through the elements in the Collection.

Boolean remove(Object)

*If the argument is in the Collection, one instance of that element is removed. Returns true if a removal occurred.

Boolean removeAll(Collection)

*Removes all the elements that are contained in the argument. Returns true if any removals occurred.

Boolean retainAll(Collection)

*Retains only elements that are contained in the argument (an “intersection” from set theory). Returns true if any changes occurred.

int size( )

Returns the number of elements in the Collection.

Object[] toArray( )

Returns an array containing all the elements in the Collection.

Object[] toArray(Object[] a)

Returns an array containing all the elements in the Collection, whose type is that of the array a rather than plain Object (you must cast the array to the right type).


*This is an “optional” method, which means it might not be implemented by a particular Collection. If not, that method throws an UnsupportedOperationException. Exceptions will be covered in Chapter 9.
  • boolean add(Object) *保證集合內包含了參數。如果它沒有添加參數,就返回false(假)
  • boolean addAll(Collection) *添加參數內的所有元素。如果沒有添加元素,則返回true(真)
  • void clear() *刪除集合內的所有元素
  • boolean contains(Object) 若集合包含參數,就返回“真”
  • boolean containsAll(Collection) 若集合包含了參數內的所有元素,就返回“真”
  • boolean isEmpty() 若集合內沒有元素,就返回“真”
  • Iterator iterator() 返回一個迭代器,以用它遍歷集合的各元素
  • boolean remove(Object) *如參數在集合裡,就刪除那個元素的一個實例。如果已進行了刪除,就返回“真”
  • boolean removeAll(Collection) *刪除參數裡的所有元素。如果已進行了任何刪除,就返回“真”
  • boolean retainAll(Collection) *只保留包含在一個參數裡的元素(一個理論的“交集”)。如果已進行了任何改變,就返回“真”
  • int size() 返回集合內的元素數量
  • Object[] toArray() 返回包含了集合內所有元素的一個數組

*這是一個“可選的”方法,有的集合可能並未實現它。若確實如此,該方法就會遇到一個UnsupportedOperatiionException,即一個“操作不支持”異常,詳見第9章。

下面這個例子向大家演示了所有方法。同樣地,它們只對從集合繼承的東西有效,一個ArrayList作為一種“不常用的分母”使用:

//: Collection1.java
// Things you can do with all Collections
package c08.newcollections;
import java.util.*;

public class Collection1 {
  // Fill with 'size' elements, start
  // counting at 'start':
  public static Collection
  fill(Collection c, int start, int size) {
    for(int i = start; i < start + size; i++)
      c.add(Integer.toString(i));
    return c;
  }
  // Default to a "start" of 0:
  public static Collection
  fill(Collection c, int size) {
    return fill(c, 0, size);
  }
  // Default to 10 elements:
  public static Collection fill(Collection c) {
    return fill(c, 0, 10);
  }
  // Create & upcast to Collection:
  public static Collection newCollection() {
    return fill(new ArrayList());
    // ArrayList is used for simplicity, but it's
    // only seen as a generic Collection
    // everywhere else in the program.
  }
  // Fill a Collection with a range of values:
  public static Collection
  newCollection(int start, int size) {
    return fill(new ArrayList(), start, size);
  }
  // Moving through a List with an iterator:
  public static void print(Collection c) {
    for(Iterator x = c.iterator(); x.hasNext();)
      System.out.print(x.next() + " ");
    System.out.println();
  }    
  public static void main(String[] args) {
    Collection c = newCollection();
    c.add("ten");
    c.add("eleven");
    print(c);
    // Make an array from the List:
    Object[] array = c.toArray();
    // Make a String array from the List:
    String[] str =
      (String[])c.toArray(new String[1]);
    // Find max and min elements; this means
    // different things depending on the way
    // the Comparable interface is implemented:
    System.out.println("Collections.max(c) = " +
      Collections.max(c));
    System.out.println("Collections.min(c) = " +
      Collections.min(c));
    // Add a Collection to another Collection
    c.addAll(newCollection());
    print(c);
    c.remove("3"); // Removes the first one
    print(c);
    c.remove("3"); // Removes the second one
    print(c);
    // Remove all components that are in the
    // argument collection:
    c.removeAll(newCollection());
    print(c);
    c.addAll(newCollection());
    print(c);
    // Is an element in this Collection?
    System.out.println(
      "c.contains(\"4\") = " + c.contains("4"));
    // Is a Collection in this Collection?
    System.out.println(
      "c.containsAll(newCollection()) = " +
      c.containsAll(newCollection()));
    Collection c2 = newCollection(5, 3);
    // Keep all the elements that are in both
    // c and c2 (an intersection of sets):
    c.retainAll(c2);
    print(c);
    // Throw away all the elements in c that
    // also appear in c2:
    c.removeAll(c2);
    System.out.println("c.isEmpty() = " +
      c.isEmpty());
    c = newCollection();
    print(c);
    c.clear(); // Remove all elements
    System.out.println("after c.clear():");
    print(c);
  }
} ///:~

通過第一個方法,我們可用測試數據填充任何集合。在當前這種情況下,只是將int轉換成String。第二個方法將在本章其餘的部分經常採用。

newCollection()的兩個版本都創建了ArrayList,用於包含不同的數據集,並將它們作為集合對象返回。所以很明顯,除了Collection接口之外,不會再用到其他什麼。

print()方法也會在本節經常用到。由於它用一個迭代器(Iterator)在一個集合內遍歷,而任何集合都可以產生這樣的一個迭代器,所以它適用於ListSet,也適用於由一個Map生成的Collection

main()用簡單的手段顯示出了集合內的所有方法。

在後續的小節裡,我們將比較ListSetMap的不同實現方案,同時指出在各種情況下哪一種方案應成為首選(帶有星號的那個)。大家會發現這裡並未包括一些傳統的類,如VectorStack以及Hashtable等。因為不管在什麼情況下,新集合內都有自己首選的類。

8.7.2 使用Lists

List (interface)

Order is the most important feature of a List; it promises to maintain elements in a particular sequence. List adds a number of methods to Collection that allow insertion and removal of elements in the middle of a List. (This is recommended only for a LinkedList.) A List will produce a ListIterator, and using this you can traverse the List in both directions, as well as insert and remove elements in the middle of the list (again, recommended only for a LinkedList).

ArrayList*

A List backed by an array. Use instead of Vector as a general-purpose object holder. Allows rapid random access to elements, but is slow when inserting and removing elements from the middle of a list. ListIterator should be used only for back-and-forth traversal of an ArrayList, but not for inserting and removing elements, which is expensive compared to LinkedList.

LinkedList

Provides optimal sequential access, with inexpensive insertions and deletions from the middle of the list. Relatively slow for random access. (Use ArrayList instead.) Also has addFirst( ), addLast( ), getFirst( ), getLast( ), removeFirst( ), and removeLast( ) (which are not defined in any interfaces or base classes) to allow it to be used as a stack, a queue, and a dequeue.

  • List(接口) 順序是List最重要的特性;它可保證元素按照規定的順序排列。ListCollection添加了大量方法,以便我們在List中部插入和刪除元素(只推薦對LinkedList這樣做)。List也會生成一個ListIterator(列表迭代器),利用它可在一個列表裡朝兩個方向遍歷,同時插入和刪除位於列表中部的元素(同樣地,只建議對LinkedList這樣做)

  • ArrayList* 由一個數組後推得到的List。作為一個常規用途的對象容器使用,用於替換原先的Vector。允許我們快速訪問元素,但在從列表中部插入和刪除元素時,速度卻嫌稍慢。一般只應該用ListIterator對一個ArrayList進行向前和向後遍歷,不要用它刪除和插入元素;與LinkedList相比,它的效率要低許多

  • LinkedList 提供優化的順序訪問性能,同時可以高效率地在列表中部進行插入和刪除操作。但在進行隨機訪問時,速度卻相當慢,此時應換用ArrayList。也提供了addFirst()addLast()getFirst()getLast()removeFirst()以及removeLast()(未在任何接口或基類中定義),以便將其作為一個規格、隊列以及一個雙向隊列使用

下面這個例子中的方法每個都覆蓋了一組不同的行為:每個列表都能做的事情(basicTest()),通過一個迭代器遍歷(iterMotion())、用一個迭代器改變某些東西(iterManipulation())、體驗列表處理的效果(testVisual())以及只有LinkedList才能做的事情等:

//: List1.java
// Things you can do with Lists
package c08.newcollections;
import java.util.*;

public class List1 {
  // Wrap Collection1.fill() for convenience:
  public static List fill(List a) {
    return (List)Collection1.fill(a);
  }
  // You can use an Iterator, just as with a
  // Collection, but you can also use random
  // access with get():
  public static void print(List a) {
    for(int i = 0; i < a.size(); i++)
      System.out.print(a.get(i) + " ");
    System.out.println();
  }
  static boolean b;
  static Object o;
  static int i;
  static Iterator it;
  static ListIterator lit;
  public static void basicTest(List a) {
    a.add(1, "x"); // Add at location 1
    a.add("x"); // Add at end
    // Add a collection:
    a.addAll(fill(new ArrayList()));
    // Add a collection starting at location 3:
    a.addAll(3, fill(new ArrayList()));
    b = a.contains("1"); // Is it in there?
    // Is the entire collection in there?
    b = a.containsAll(fill(new ArrayList()));
    // Lists allow random access, which is cheap
    // for ArrayList, expensive for LinkedList:
    o = a.get(1); // Get object at location 1
    i = a.indexOf("1"); // Tell index of object
    // indexOf, starting search at location 2:
    i = a.indexOf("1", 2);
    b = a.isEmpty(); // Any elements inside?
    it = a.iterator(); // Ordinary Iterator
    lit = a.listIterator(); // ListIterator
    lit = a.listIterator(3); // Start at loc 3
    i = a.lastIndexOf("1"); // Last match
    i = a.lastIndexOf("1", 2); // ...after loc 2
    a.remove(1); // Remove location 1
    a.remove("3"); // Remove this object
    a.set(1, "y"); // Set location 1 to "y"
    // Keep everything that's in the argument
    // (the intersection of the two sets):
    a.retainAll(fill(new ArrayList()));
    // Remove elements in this range:
    a.removeRange(0, 2);
    // Remove everything that's in the argument:
    a.removeAll(fill(new ArrayList()));
    i = a.size(); // How big is it?
    a.clear(); // Remove all elements
  }
  public static void iterMotion(List a) {
    ListIterator it = a.listIterator();
    b = it.hasNext();
    b = it.hasPrevious();
    o = it.next();
    i = it.nextIndex();
    o = it.previous();
    i = it.previousIndex();
  }
  public static void iterManipulation(List a) {
    ListIterator it = a.listIterator();
    it.add("47");
    // Must move to an element after add():
    it.next();
    // Remove the element that was just produced:
    it.remove();
    // Must move to an element after remove():
    it.next();
    // Change the element that was just produced:
    it.set("47");
  }
  public static void testVisual(List a) {
    print(a);
    List b = new ArrayList();
    fill(b);
    System.out.print("b = ");
    print(b);
    a.addAll(b);
    a.addAll(fill(new ArrayList()));
    print(a);
    // Shrink the list by removing all the
    // elements beyond the first 1/2 of the list
    System.out.println(a.size());
    System.out.println(a.size()/2);
    a.removeRange(a.size()/2, a.size()/2 + 2);
    print(a);
    // Insert, remove, and replace elements
    // using a ListIterator:
    ListIterator x = a.listIterator(a.size()/2);
    x.add("one");
    print(a);
    System.out.println(x.next());
    x.remove();
    System.out.println(x.next());
    x.set("47");
    print(a);
    // Traverse the list backwards:
    x = a.listIterator(a.size());
    while(x.hasPrevious())
      System.out.print(x.previous() + " ");
    System.out.println();
    System.out.println("testVisual finished");
  }
  // There are some things that only
  // LinkedLists can do:
  public static void testLinkedList() {
    LinkedList ll = new LinkedList();
    Collection1.fill(ll, 5);
    print(ll);
    // Treat it like a stack, pushing:
    ll.addFirst("one");
    ll.addFirst("two");
    print(ll);
    // Like "peeking" at the top of a stack:
    System.out.println(ll.getFirst());
    // Like popping a stack:
    System.out.println(ll.removeFirst());
    System.out.println(ll.removeFirst());
    // Treat it like a queue, pulling elements
    // off the tail end:
    System.out.println(ll.removeLast());
    // With the above operations, it's a dequeue!
    print(ll);
  }
  public static void main(String args[]) {
    // Make and fill a new list each time:
    basicTest(fill(new LinkedList()));
    basicTest(fill(new ArrayList()));
    iterMotion(fill(new LinkedList()));
    iterMotion(fill(new ArrayList()));
    iterManipulation(fill(new LinkedList()));
    iterManipulation(fill(new ArrayList()));
    testVisual(fill(new LinkedList()));
    testLinkedList();
  }
} ///:~

basicTest()iterMotiion()中,只是簡單地發出調用,以便揭示出正確的語法。而且儘管捕獲了返回值,但是並未使用它。在某些情況下,之所以不捕獲返回值,是由於它們沒有什麼特別的用處。在正式使用它們前,應仔細研究一下自己的聯機文檔,掌握這些方法完整、正確的用法。

8.7.3 使用Sets

Set擁有與Collection完全相同的接口,所以和兩種不同的List不同,它沒有什麼額外的功能。相反,Set完全就是一個Collection,只是具有不同的行為(這是實例和多態性最理想的應用:用於表達不同的行為)。在這裡,一個Set只允許每個對象存在一個實例(正如大家以後會看到的那樣,一個對象的“值”的構成是相當複雜的)。

Set (interface)

Each element that you add to the Set must be unique; otherwise the Set doesn’t add the duplicate element. Objects added to a Set must define equals( ) to establish object uniqueness. Set has exactly the same interface as Collection. The Set interface does not guarantee it will maintain its elements in any particular order.

HashSet*

For Sets where fast lookup time is important. Objects must also define hashCode( ).

TreeSet

An ordered Set backed by a red-black tree. This way, you can extract an ordered sequence from a Set.
  • Set(接口) 添加到Set的每個元素都必須是獨一無二的;否則Set就不會添加重複的元素。添加到Set裡的對象必須定義equals(),從而建立對象的唯一性。Set擁有與Collection完全相同的接口。一個Set不能保證自己可按任何特定的順序維持自己的元素
  • HashSet* 用於除非常小的以外的所有Set。對象也必須定義hashCode()
  • ArraySet 由一個數組後推得到的Set。面向非常小的Set設計,特別是那些需要頻繁創建和刪除的。對於小Set,與HashSet相比,ArraySet創建和迭代所需付出的代價都要小得多。但隨著Set的增大,它的性能也會大打折扣。不需要HashCode()
  • TreeSet 由一個“紅黑樹”後推得到的順序Set(註釋⑦)。這樣一來,我們就可以從一個Set裡提到一個順序集合

⑦:直至本書寫作的時候,TreeSet仍然只是宣佈,尚未正式實現。所以這裡沒有提供使用TreeSet的例子。

下面這個例子並沒有列出用一個Set能夠做的全部事情,因為接口與Collection是相同的,前例已經練習過了。相反,我們要例示的重點在於使一個Set獨一無二的行為:

//: Set1.java
// Things you can do with Sets
package c08.newcollections;
import java.util.*;

public class Set1 {
  public static void testVisual(Set a) {
    Collection1.fill(a);
    Collection1.fill(a);
    Collection1.fill(a);
    Collection1.print(a); // No duplicates!
    // Add another set to this one:
    a.addAll(a);
    a.add("one");
    a.add("one");
    a.add("one");
    Collection1.print(a);
    // Look something up:
    System.out.println("a.contains(\"one\"): " +
      a.contains("one"));
  }
  public static void main(String[] args) {
    testVisual(new HashSet());
    testVisual(new TreeSet());
  }
} ///:~

重複的值被添加到Set,但在打印的時候,我們會發現Set只接受每個值的一個實例。

運行這個程序時,會注意到由HashSet維持的順序與ArraySet是不同的。這是由於它們採用了不同的方法來保存元素,以便它們以後的定位。ArraySet保持著它們的順序狀態,而HashSet使用一個散列函數,這是特別為快速檢索設計的)。創建自己的類型時,一定要注意Set需要通過一種方式來維持一種存儲順序,就象本章早些時候展示的“groundhog”(土拔鼠)例子那樣。下面是一個例子:

//: Set2.java
// Putting your own type in a Set
package c08.newcollections;
import java.util.*;

class MyType implements Comparable {
  private int i;
  public MyType(int n) { i = n; }
  public boolean equals(Object o) {
    return
      (o instanceof MyType)
      && (i == ((MyType)o).i);
  }
  public int hashCode() { return i; }
  public String toString() { return i + " "; }
  public int compareTo(Object o) {
    int i2 = ((MyType) o).i;
    return (i2 < i ? -1 : (i2 == i ? 0 : 1));
  }
}

public class Set2 {
  public static Set fill(Set a, int size) {
    for(int i = 0; i < size; i++)
      a.add(new MyType(i));
    return a;
  }
  public static Set fill(Set a) {
    return fill(a, 10);
  }
  public static void test(Set a) {
    fill(a);
    fill(a); // Try to add duplicates
    fill(a);
    a.addAll(fill(new TreeSet()));
    System.out.println(a);
  }
  public static void main(String[] args) {
    test(new HashSet());
    test(new TreeSet());
  }
} ///:~

equals()hashCode()的定義遵照“groundhog”例子已經給出的形式。在兩種情況下都必須定義一個equals()。但只有要把類置入一個HashSet的前提下,才有必要使用hashCode()——這種情況是完全有可能的,因為通常應先選擇作為一個Set實現。

8.7.4 使用Maps

Map (interface)

Maintains key-value associations (pairs), so you can look up a value using a key.

HashMap*

Implementation based on a hash table. (Use this instead of Hashtable.) Provides constant-time performance for inserting and locating pairs. Performance can be adjusted via constructors that allow you to set the capacity and load factor of the hash table.

TreeMap

Implementation based on a red-black tree. When you view the keys or the pairs, they will be in sorted order (determined by Comparable or Comparator, discussed later). The point of a TreeMap is that you get the results in sorted order. TreeMap is the only Map with the subMap( ) method, which allows you to return a portion of the tree.

  • Map(接口) 維持“鍵-值”對應關係(對),以便通過一個鍵查找相應的值
  • HashMap* 基於一個散列表實現(用它代替Hashtable)。針對“鍵-值”對的插入和檢索,這種形式具有最穩定的性能。可通過構造器對這一性能進行調整,以便設置散列表的“能力”和“裝載因子”
  • ArrayMap 由一個ArrayList後推得到的Map。對迭代的順序提供了精確的控制。面向非常小的Map設計,特別是那些需要經常創建和刪除的。對於非常小的Map,創建和迭代所付出的代價要比HashMap低得多。但在Map變大以後,性能也會相應地大幅度降低
  • TreeMap 在一個“紅-黑”樹的基礎上實現。查看鍵或者“鍵-值”對時,它們會按固定的順序排列(取決於Comparable
  • Comparator,稍後即會講到)。TreeMap最大的好處就是我們得到的是已排好序的結果。TreeMap是含有subMap()方法的唯一一種Map,利用它可以返回樹的一部分

下例包含了兩套測試數據以及一個fill()方法,利用該方法可以用任何兩維數組(由Object構成)填充任何Map。這些工具也會在其他Map例子中用到。

//: Map1.java
// Things you can do with Maps
package c08.newcollections;
import java.util.*;

public class Map1 {
  public final static String[][] testData1 = {
    { "Happy", "Cheerful disposition" },
    { "Sleepy", "Prefers dark, quiet places" },
    { "Grumpy", "Needs to work on attitude" },
    { "Doc", "Fantasizes about advanced degree"},
    { "Dopey", "'A' for effort" },
    { "Sneezy", "Struggles with allergies" },
    { "Bashful", "Needs self-esteem workshop"},
  };
  public final static String[][] testData2 = {
    { "Belligerent", "Disruptive influence" },
    { "Lazy", "Motivational problems" },
    { "Comatose", "Excellent behavior" }
  };
  public static Map fill(Map m, Object[][] o) {
    for(int i = 0; i < o.length; i++)
      m.put(o[i][0], o[i][1]);
    return m;
  }
  // Producing a Set of the keys:
  public static void printKeys(Map m) {
    System.out.print("Size = " + m.size() +", ");
    System.out.print("Keys: ");
    Collection1.print(m.keySet());
  }
  // Producing a Collection of the values:
  public static void printValues(Map m) {
    System.out.print("Values: ");
    Collection1.print(m.values());
  }
  // Iterating through Map.Entry objects (pairs):
  public static void print(Map m) {
    Collection entries = m.entries();
    Iterator it = entries.iterator();
    while(it.hasNext()) {
      Map.Entry e = (Map.Entry)it.next();
      System.out.println("Key = " + e.getKey() +
        ", Value = " + e.getValue());
    }
  }
  public static void test(Map m) {
    fill(m, testData1);
    // Map has 'Set' behavior for keys:
    fill(m, testData1);
    printKeys(m);
    printValues(m);
    print(m);
    String key = testData1[4][0];
    String value = testData1[4][1];
    System.out.println("m.containsKey(\"" + key +
      "\"): " + m.containsKey(key));
    System.out.println("m.get(\"" + key + "\"): "
      + m.get(key));
    System.out.println("m.containsValue(\""
      + value + "\"): " +
      m.containsValue(value));
    Map m2 = fill(new TreeMap(), testData2);
    m.putAll(m2);
    printKeys(m);
    m.remove(testData2[0][0]);
    printKeys(m);
    m.clear();
    System.out.println("m.isEmpty(): "
      + m.isEmpty());
    fill(m, testData1);
    // Operations on the Set change the Map:
    m.keySet().removeAll(m.keySet());
    System.out.println("m.isEmpty(): "
      + m.isEmpty());
  }
  public static void main(String args[]) {
    System.out.println("Testing HashMap");
    test(new HashMap());
    System.out.println("Testing TreeMap");
    test(new TreeMap());
  }
} ///:~

printKeys()printValues()以及print()方法並不只是有用的工具,它們也清楚地揭示了一個MapCollection“景象”的產生過程。keySet()方法會產生一個Set,它由Map中的鍵後推得來。在這兒,它只被當作一個Collection對待。values()也得到了類似的對待,它的作用是產生一個List,其中包含了Map中的所有值(注意鍵必須是獨一無二的,而值可以有重複)。由於這些Collection是由Map後推得到的,所以一個Collection中的任何改變都會在相應的Map中反映出來。

print()方法的作用是收集由entries產生的Iterator(迭代器),並用它同時打印出每個“鍵-值”對的鍵和值。程序剩餘的部分提供了每種Map操作的簡單示例,並對每種類型的Map進行了測試。

當創建自己的類,將其作為Map中的一個鍵使用時,必須注意到和以前的Set相同的問題。

8.7.5 決定實現方案

從早些時候的那幅示意圖可以看出,實際上只有三個集合組件:MapListSet。而且每個接口只有兩種或三種實現方案。若需使用由一個特定的接口提供的功能,如何才能決定到底採取哪一種方案呢?

為理解這個問題,必須認識到每種不同的實現方案都有自己的特點、優點和缺點。比如在那張示意圖中,可以看到HashtableVectorStack的“特點”是它們都屬於“傳統”類,所以不會干擾原有的代碼。但在另一方面,應儘量避免為新的(Java 1.2)代碼使用它們。

其他集合間的差異通常都可歸納為它們具體是由什麼“後推”的。換言之,取決於物理意義上用於實現目標接口的數據結構是什麼。例如,ArrayListLinkedList以及Vector(大致等價於ArrayList)都實現了List接口,所以無論選用哪一個,我們的程序都會得到類似的結果。然而,ArrayList(以及Vector)是由一個數組後推得到的;而LinkedList是根據常規的雙重鏈接列表方式實現的,因為每個單獨的對象都包含了數據以及指向列表內前後元素的引用。正是由於這個原因,假如想在一個列表中部進行大量插入和刪除操作,那麼LinkedList無疑是最恰當的選擇(LinkedList還有一些額外的功能,建立於AbstractSequentialList中)。若非如此,就情願選擇ArrayList,它的速度可能要快一些。

作為另一個例子,Set既可作為一個ArraySet實現,亦可作為HashSet實現。ArraySet是由一個ArrayList後推得到的,設計成只支持少量元素,特別適合要求創建和刪除大量Set對象的場合使用。然而,一旦需要在自己的Set中容納大量元素,ArraySet的性能就會大打折扣。寫一個需要Set的程序時,應默認選擇HashSet。而且只有在某些特殊情況下(對性能的提升有迫切的需求),才應切換到ArraySet

(1) 決定使用何種List

為體會各種List實現方案間的差異,最簡便的方法就是進行一次性能測驗。下述代碼的作用是建立一個內部基類,將其作為一個測試床使用。然後為每次測驗都創建一個匿名內部類。每個這樣的內部類都由一個test()方法調用。利用這種方法,可以方便添加和刪除測試項目。

//: ListPerformance.java
// Demonstrates performance differences in Lists
package c08.newcollections;
import java.util.*;

public class ListPerformance {
  private static final int REPS = 100;
  private abstract static class Tester {
    String name;
    int size; // Test quantity
    Tester(String name, int size) {
      this.name = name;
      this.size = size;
    }
    abstract void test(List a);
  }
  private static Tester[] tests = {
    new Tester("get", 300) {
      void test(List a) {
        for(int i = 0; i < REPS; i++) {
          for(int j = 0; j < a.size(); j++)
            a.get(j);
        }
      }
    },
    new Tester("iteration", 300) {
      void test(List a) {
        for(int i = 0; i < REPS; i++) {
          Iterator it = a.iterator();
          while(it.hasNext())
            it.next();
        }
      }
    },
    new Tester("insert", 1000) {
      void test(List a) {
        int half = a.size()/2;
        String s = "test";
        ListIterator it = a.listIterator(half);
        for(int i = 0; i < size * 10; i++)
          it.add(s);
      }
    },
    new Tester("remove", 5000) {
      void test(List a) {
        ListIterator it = a.listIterator(3);
        while(it.hasNext()) {
          it.next();
          it.remove();
        }
      }
    },
  };
  public static void test(List a) {
    // A trick to print out the class name:
    System.out.println("Testing " +
      a.getClass().getName());
    for(int i = 0; i < tests.length; i++) {
      Collection1.fill(a, tests[i].size);
      System.out.print(tests[i].name);
      long t1 = System.currentTimeMillis();
      tests[i].test(a);
      long t2 = System.currentTimeMillis();
      System.out.println(": " + (t2 - t1));
    }
  }
  public static void main(String[] args) {
    test(new ArrayList());
    test(new LinkedList());
  }
} ///:~

內部類Tester是一個抽象類,用於為特定的測試提供一個基類。它包含了一個要在測試開始時打印的字符串、一個用於計算測試次數或元素數量的size參數、用於初始化字段的一個構造器以及一個抽象方法test()test()做的是最實際的測試工作。各種類型的測試都集中到一個地方:tests數組。我們用繼承於Tester的不同匿名內部類來初始化該數組。為添加或刪除一個測試項目,只需在數組裡簡單地添加或移去一個內部類定義即可,其他所有工作都是自動進行的。

首先用元素填充傳遞給test()List,然後對tests數組中的測試計時。由於測試用機器的不同,結果當然也會有所區別。這個程序的宗旨是揭示出不同集合類型的相對性能比較。下面是某一次運行得到的結果:

類型 獲取 迭代 插入 刪除
ArrayList 110 270 1920 4780
LinkedList 1870 7580 170 110

可以看出,在ArrayList中進行隨機訪問(即get())以及循環迭代是最划得來的;但對於LinkedList卻是一個不小的開銷。但另一方面,在列表中部進行插入和刪除操作對於LinkedList來說卻比ArrayList划算得多。我們最好的做法也許是先選擇一個ArrayList作為自己的默認起點。以後若發現由於大量的插入和刪除造成了性能的降低,再考慮換成LinkedList不遲。

(2) 決定使用何種Set

可在ArraySet以及HashSet間作出選擇,具體取決於Set的大小(如果需要從一個Set中獲得一個順序列表,請用TreeSet;註釋⑧)。下面這個測試程序將有助於大家作出這方面的抉擇:

//: SetPerformance.java
package c08.newcollections;
import java.util.*;

public class SetPerformance {
  private static final int REPS = 200;
  private abstract static class Tester {
    String name;
    Tester(String name) { this.name = name; }
    abstract void test(Set s, int size);
  }
  private static Tester[] tests = {
    new Tester("add") {
      void test(Set s, int size) {
        for(int i = 0; i < REPS; i++) {
          s.clear();
          Collection1.fill(s, size);
        }
      }
    },
    new Tester("contains") {
      void test(Set s, int size) {
        for(int i = 0; i < REPS; i++)
          for(int j = 0; j < size; j++)
            s.contains(Integer.toString(j));
      }
    },
    new Tester("iteration") {
      void test(Set s, int size) {
        for(int i = 0; i < REPS * 10; i++) {
          Iterator it = s.iterator();
          while(it.hasNext())
            it.next();
        }
      }
    },
  };
  public static void test(Set s, int size) {
    // A trick to print out the class name:
    System.out.println("Testing " +
      s.getClass().getName() + " size " + size);
    Collection1.fill(s, size);
    for(int i = 0; i < tests.length; i++) {
      System.out.print(tests[i].name);
      long t1 = System.currentTimeMillis();
      tests[i].test(s, size);
      long t2 = System.currentTimeMillis();
      System.out.println(": " +
        ((double)(t2 - t1)/(double)size));
    }
  }
  public static void main(String[] args) {
    // Small:
    test(new TreeSet(), 10);
    test(new HashSet(), 10);
    // Medium:
    test(new TreeSet(), 100);
    test(new HashSet(), 100);
    // Large:
    test(new HashSet(), 1000);
    test(new TreeSet(), 1000);
  }
} ///:~

⑧:TreeSet在本書寫作時尚未成為一個正式的特性,但在這個例子中可以很輕鬆地為其添加一個測試。

最後對ArraySet的測試只有500個元素,而不是1000個,因為它太慢了。

類型 測試大小 添加 包含 迭代
TreeSet 10 22.0 11.0 16.0
100 22.5 13.2 12.1
1000 31.1 18.7 11.8
HashSet 10 5.0 6.0 27.0
100 6.6 6.6 10.9
1000 7.4 6.6 9.5

進行add()以及contains()操作時,HashSet顯然要比ArraySet出色得多,而且性能明顯與元素的多寡關係不大。一般編寫程序的時候,幾乎永遠用不著使用ArraySet

(3) 決定使用何種Map

選擇不同的Map實現方案時,注意Map的大小對於性能的影響是最大的,下面這個測試程序清楚地闡示了這一點:

//: MapPerformance.java
// Demonstrates performance differences in Maps
package c08.newcollections;
import java.util.*;

public class MapPerformance {
  private static final int REPS = 200;
  public static Map fill(Map m, int size) {
    for(int i = 0; i < size; i++) {
      String x = Integer.toString(i);
      m.put(x, x);
    }
    return m;
  }
  private abstract static class Tester {
    String name;
    Tester(String name) { this.name = name; }
    abstract void test(Map m, int size);
  }
  private static Tester[] tests = {
    new Tester("put") {
      void test(Map m, int size) {
        for(int i = 0; i < REPS; i++) {
          m.clear();
          fill(m, size);
        }
      }
    },
    new Tester("get") {
      void test(Map m, int size) {
        for(int i = 0; i < REPS; i++)
          for(int j = 0; j < size; j++)
            m.get(Integer.toString(j));
      }
    },
    new Tester("iteration") {
      void test(Map m, int size) {
        for(int i = 0; i < REPS * 10; i++) {
          Iterator it = m.entries().iterator();
          while(it.hasNext())
            it.next();
        }
      }
    },
  };
  public static void test(Map m, int size) {
    // A trick to print out the class name:
    System.out.println("Testing " +
      m.getClass().getName() + " size " + size);
    fill(m, size);
    for(int i = 0; i < tests.length; i++) {
      System.out.print(tests[i].name);
      long t1 = System.currentTimeMillis();
      tests[i].test(m, size);
      long t2 = System.currentTimeMillis();
      System.out.println(": " +
        ((double)(t2 - t1)/(double)size));
    }
  }
  public static void main(String[] args) {
    // Small:
    test(new Hashtable(), 10);
    test(new HashMap(), 10);
    test(new TreeMap(), 10);
    // Medium:
    test(new Hashtable(), 100);
    test(new HashMap(), 100);
    test(new TreeMap(), 100);
    // Large:
    test(new HashMap(), 1000);
    test(new Hashtable(), 1000);
    test(new TreeMap(), 1000);
  }
} ///:~

由於Map的大小是最嚴重的問題,所以程序的計時測試按Map的大小(或容量)來分割時間,以便得到令人信服的測試結果。下面列出一系列結果(在你的機器上可能不同):

類型 測試大小 置入 取出 迭代
Hashtable 10 11.0 5.0 44.0
100 7.7 7.7 16.5
1000 8.0 8.0 14.4
TreeMap 10 16.0 11.0 22.0
100 25.8 15.4 13.2
1000 33.8 20.9 13.6
HashMap 10 11.0 6.0 33.0
100 8.2 7.7 13.7
1000 8.0 7.8 11.9

即使大小為10,ArrayMap的性能也要比HashMap差——除迭代循環時以外。而在使用Map時,迭代的作用通常並不重要(get()通常是我們時間花得最多的地方)。TreeMap提供了出色的put()以及迭代時間,但get()的性能並不佳。但是,我們為什麼仍然需要使用TreeMap呢?這樣一來,我們可以不把它作為Map使用,而作為創建順序列表的一種途徑。樹的本質在於它總是順序排列的,不必特別進行排序(它的排序方式馬上就要講到)。一旦填充了一個TreeMap,就可以調用keySet()來獲得鍵的一個Set“景象”。然後用toArray()產生包含了那些鍵的一個數組。隨後,可用static方法Array.binarySearch()快速查找排好序的數組中的內容。當然,也許只有在HashMap的行為不可接受的時候,才需要採用這種做法。因為HashMap的設計宗旨就是進行快速的檢索操作。最後,當我們使用Map時,首要的選擇應該是HashMap。只有在極少數情況下才需要考慮其他方法。

此外,在上面那張表裡,有另一個性能問題沒有反映出來。下述程序用於測試不同類型Map的創建速度:

//: MapCreation.java
// Demonstrates time differences in Map creation
package c08.newcollections;
import java.util.*;

public class MapCreation {
  public static void main(String[] args) {
    final long REPS = 100000;
    long t1 = System.currentTimeMillis();
    System.out.print("Hashtable");
    for(long i = 0; i < REPS; i++)
      new Hashtable();
    long t2 = System.currentTimeMillis();
    System.out.println(": " + (t2 - t1));
    t1 = System.currentTimeMillis();
    System.out.print("TreeMap");
    for(long i = 0; i < REPS; i++)
      new TreeMap();
    t2 = System.currentTimeMillis();
    System.out.println(": " + (t2 - t1));
    t1 = System.currentTimeMillis();
    System.out.print("HashMap");
    for(long i = 0; i < REPS; i++)
      new HashMap();
    t2 = System.currentTimeMillis();
    System.out.println(": " + (t2 - t1));
  }
} ///:~

在寫這個程序期間,TreeMap的創建速度比其他兩種類型明顯快得多(但你應親自嘗試一下,因為據說新版本可能會改善ArrayMap的性能)。考慮到這方面的原因,同時由於前述TreeMap出色的put()性能,所以如果需要創建大量Map,而且只有在以後才需要涉及大量檢索操作,那麼最佳的策略就是:創建和填充TreeMap;以後檢索量增大的時候,再將重要的TreeMap轉換成HashMap——使用HashMap(Map)構造器。同樣地,只有在事實證明確實存在性能瓶頸後,才應關心這些方面的問題——先用起來,再根據需要加快速度。

8.7.6 未支持的操作

利用static(靜態)數組Arrays.toList(),也許能將一個數組轉換成List,如下所示:

//: Unsupported.java
// Sometimes methods defined in the Collection
// interfaces don't work!
package c08.newcollections;
import java.util.*;

public class Unsupported {
  private static String[] s = {
    "one", "two", "three", "four", "five",
    "six", "seven", "eight", "nine", "ten",
  };
  static List a = Arrays.toList(s);
  static List a2 = Arrays.toList(
    new String[] { s[3], s[4], s[5] });
  public static void main(String[] args) {
    Collection1.print(a); // Iteration
    System.out.println(
      "a.contains(" + s[0] + ") = " +
      a.contains(s[0]));
    System.out.println(
      "a.containsAll(a2) = " +
      a.containsAll(a2));
    System.out.println("a.isEmpty() = " +
      a.isEmpty());
    System.out.println(
      "a.indexOf(" + s[5] + ") = " +
      a.indexOf(s[5]));
    // Traverse backwards:
    ListIterator lit = a.listIterator(a.size());
    while(lit.hasPrevious())
      System.out.print(lit.previous());
    System.out.println();
    // Set the elements to different values:
    for(int i = 0; i < a.size(); i++)
      a.set(i, "47");
    Collection1.print(a);
    // Compiles, but won't run:
    lit.add("X"); // Unsupported operation
    a.clear(); // Unsupported
    a.add("eleven"); // Unsupported
    a.addAll(a2); // Unsupported
    a.retainAll(a2); // Unsupported
    a.remove(s[0]); // Unsupported
    a.removeAll(a2); // Unsupported
  }
} ///:~

從中可以看出,實際只實現了CollectionList接口的一部分。剩餘的方法導致了不受歡迎的一種情況,名為UnsupportedOperationException。在下一章裡,我們會講述異常的詳細情況,但在這裡有必要進行一下簡單說明。這裡的關鍵在於“集合接口”,以及新集合庫內的另一些接口,它們都包含了“可選的”方法。在實現那些接口的集合類中,或者提供、或者沒有提供對那些方法的支持。若調用一個未獲支持的方法,就會導致一個UnsupportedOperationException(操作未支持異常),這表明出現了一個編程錯誤。

大家或許會覺得奇怪,不是說“接口”和基類最大的“賣點”就是它們許諾這些方法能產生一些有意義的行為嗎?上述異常破壞了那個許諾——它調用的一部分方法不僅不能產生有意義的行為,而且還會中止程序的運行。在這些情況下,類型的所謂安全保證似乎顯得一錢不值!但是,情況並沒有想象的那麼壞。通過CollectionListSet或者Map,編譯器仍然限制我們只能調用那個接口中的方法,所以它和Smalltalk還是存在一些區別的(在Smalltalk中,可為任何對象調用任何方法,而且只有在運行程序時才知道這些調用是否可行)。除此以外,以Collection作為參數的大多數方法只能從那個集合中讀取數據——Collection的所有read方法都不是可選的。

這樣一來,系統就可避免在設計期間出現接口的衝突。而在集合庫的其他設計模式中,最終經常都會得到數量過多的接口,用它們描述基本方案的每一種變化形式,所以學習和掌握顯得非常困難。有些時候,甚至難於捕捉接口中的所有特殊情況,因為人們可能設計出任何新接口。但Java的“不支持的操作”方法卻達到了新集合庫的一個重要設計目標:易於學習和使用。但是,為了使這一方法真正有效,卻需滿足下述條件:

(1) UnsupportedOperationException必須屬於一種“非常”事件。也就是說,對於大多數類來說,所有操作都應是可行的。只有在一些特殊情況下,一、兩個操作才可能未獲支持。新集合庫滿足了這一條件,因為絕大多數時候用到的類——ArrayListLinkedListHashListHashMap,以及其他集合方案——都提供了對所有操作的支持。但是,如果想新建一個集合,同時不想為集合接口中的所有方法都提供有意義的定義,同時令其仍與現有庫配合,這種設計方法也確實提供了一個“後門”可以利用。

(2) 若一個操作未獲支持,那麼UnsupportedOperationException(未支持的操作異常)極有可能在實現期間出現,則不是在產品已交付給客戶以後才會出現。它畢竟指出的是一個編程錯誤——不正確地使用了一個類。這一點不能十分確定,通過也可以看出這種方案的“試驗”特徵——只有經過多次試驗,才能找出最理想的工作方式。

在上面的例子中,Arrays.toList()產生了一個List(列表),該列表是由一個固定長度的數組後推出來的。因此唯一能夠支持的就是那些不改變數組長度的操作。在另一方面,若請求一個新接口表達不同種類的行為(可能叫作FixedSizeList——固定長度列表),就有遭遇更大的複雜程度的危險。這樣一來,以後試圖使用庫的時候,很快就會發現自己不知從何處下手。

對那些採用CollectionListSet或者Map作為參數的方法,它們的文檔應當指出哪些可選的方法是必須實現的。舉個例子來說,排序要求實現set()Iterator.set()方法,但不包括add()remove()

8.7.7 排序和搜索

Java 1.2添加了自己的一套實用工具,可用來對數組或列表進行排列和搜索。這些工具都屬於兩個新類的“靜態”方法。這兩個類分別是用於排序和搜索數組的Arrays,以及用於排序和搜索列表的Collections

(1) 數組

Arrays類為所有基本數據類型的數組提供了一個重載的sort()binarySearch(),它們亦可用於StringObject。下面這個例子顯示出如何排序和搜索一個字節數組(其他所有基本數據類型都是類似的)以及一個String數組:

//: Array1.java
// Testing the sorting & searching in Arrays
package c08.newcollections;
import java.util.*;

public class Array1 {
  static Random r = new Random();
  static String ssource =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
    "abcdefghijklmnopqrstuvwxyz";
  static char[] src = ssource.toCharArray();
  // Create a random String
  public static String randString(int length) {
    char[] buf = new char[length];
    int rnd;
    for(int i = 0; i < length; i++) {
      rnd = Math.abs(r.nextInt()) % src.length;
      buf[i] = src[rnd];
    }
    return new String(buf);
  }
  // Create a random array of Strings:
  public static
  String[] randStrings(int length, int size) {
    String[] s = new String[size];
    for(int i = 0; i < size; i++)
      s[i] = randString(length);
    return s;
  }
  public static void print(byte[] b) {
    for(int i = 0; i < b.length; i++)
      System.out.print(b[i] + " ");
    System.out.println();
  }
  public static void print(String[] s) {
    for(int i = 0; i < s.length; i++)
      System.out.print(s[i] + " ");
    System.out.println();
  }
  public static void main(String[] args) {
    byte[] b = new byte[15];
    r.nextBytes(b); // Fill with random bytes
    print(b);
    Arrays.sort(b);
    print(b);
    int loc = Arrays.binarySearch(b, b[10]);
    System.out.println("Location of " + b[10] +
      " = " + loc);
    // Test String sort & search:
    String[] s = randStrings(4, 10);
    print(s);
    Arrays.sort(s);
    print(s);
    loc = Arrays.binarySearch(s, s[4]);
    System.out.println("Location of " + s[4] +
      " = " + loc);
  }
} ///:~

類的第一部分包含了用於產生隨機字符串對象的實用工具,可供選擇的隨機字母保存在一個字符數組中。randString()返回一個任意長度的字符串;而readStrings()創建隨機字符串的一個數組,同時給定每個字符串的長度以及希望的數組大小。兩個print()方法簡化了對示範數組的顯示。在main()中,Random.nextBytes()用隨機選擇的字節填充數組參數(沒有對應的Random方法用於創建其他基本數據類型的數組)。獲得一個數組後,便可發現為了執行sort()或者binarySearch(),只需發出一次方法調用即可。與binarySearch()有關的還有一個重要的警告:若在執行一次binarySearch()之前不調用sort(),便會發生不可預測的行為,其中甚至包括無限循環。

String的排序以及搜索是相似的,但在運行程序的時候,我們會注意到一個有趣的現象:排序遵守的是字典順序,亦即大寫字母在字符集中位於小寫字母的前面。因此,所有大寫字母都位於列表的最前面,後面再跟上小寫字母——Z居然位於a的前面。似乎連電話簿也是這樣排序的。

(2) 可比較與比較器

但假若我們不滿足這一排序方式,又該如何處理呢?例如本書後面的索引,如果必須對以Aa開頭的詞條分別到兩處地方查看,那麼肯定會使讀者頗不耐煩。

若想對一個Object數組進行排序,那麼必須解決一個問題。根據什麼來判定兩個Object的順序呢?不幸的是,最初的Java設計者並不認為這是一個重要的問題,否則就已經在根類Object裡定義它了。這樣造成的一個後果便是:必須從外部進行Object的排序,而且新的集合庫提供了實現這一操作的標準方式(最理想的是在Object裡定義它)。

針對Object數組(以及String,它當然屬於Object的一種),可使用一個sort(),並令其接納另一個參數:實現了Comparator接口(即“比較器”接口,新集合庫的一部分)的一個對象,並用它的單個compare()方法進行比較。這個方法將兩個準備比較的對象作為自己的參數使用——若第一個參數小於第二個,返回一個負整數;若相等,返回零;若第一個參數大於第二個,則返回正整數。基於這一規則,上述例子的String部分便可重新寫過,令其進行真正按字母順序的排序:

//: AlphaComp.java
// Using Comparator to perform an alphabetic sort
package c08.newcollections;
import java.util.*;

public class AlphaComp implements Comparator {
  public int compare(Object o1, Object o2) {
    // Assume it's used only for Strings...
    String s1 = ((String)o1).toLowerCase();
    String s2 = ((String)o2).toLowerCase();
    return s1.compareTo(s2);
  }
  public static void main(String[] args) {
    String[] s = Array1.randStrings(4, 10);
    Array1.print(s);
    AlphaComp ac = new AlphaComp();
    Arrays.sort(s, ac);
    Array1.print(s);
    // Must use the Comparator to search, also:
    int loc = Arrays.binarySearch(s, s[3], ac);
    System.out.println("Location of " + s[3] +
     " = " + loc);
  }
} ///:~

通過轉換為Stringcompare()方法會進行“暗示”性的測試,保證自己操作的只能是String對象——運行期系統會捕獲任何差錯。將兩個字符串都強迫換成小寫形式後,String.compareTo()方法會產生預期的結果。

若用自己的Comparator來進行一次sort(),那麼在使用binarySearch()時必須使用那個相同的Comparator

Arrays類提供了另一個sort()方法,它會採用單個參數:一個Object數組,但沒有Comparator。這個sort()方法也必須用同樣的方式來比較兩個Object。通過實現Comparable接口,它採用了賦予一個類的“自然比較方法”。這個接口含有單獨一個方法——compareTo(),能分別根據它小於、等於或者大於參數而返回負數、零或者正數,從而實現對象的比較。下面這個例子簡單地闡示了這一點:

//: CompClass.java
// A class that implements Comparable
package c08.newcollections;
import java.util.*;

public class CompClass implements Comparable {
  private int i;
  public CompClass(int ii) { i = ii; }
  public int compareTo(Object o) {
    // Implicitly tests for correct type:
    int argi = ((CompClass)o).i;
    if(i == argi) return 0;
    if(i < argi) return -1;
    return 1;
  }
  public static void print(Object[] a) {
    for(int i = 0; i < a.length; i++)
      System.out.print(a[i] + " ");
    System.out.println();
  }
  public String toString() { return i + ""; }
  public static void main(String[] args) {
    CompClass[] a = new CompClass[20];
    for(int i = 0; i < a.length; i++)
      a[i] = new CompClass(
        (int)(Math.random() *100));
    print(a);
    Arrays.sort(a);
    print(a);
    int loc = Arrays.binarySearch(a, a[3]);
    System.out.println("Location of " + a[3] +
     " = " + loc);
  }
} ///:~

當然,我們的compareTo()方法亦可根據實際情況增大複雜程度。

(3) 列表

可用與數組相同的形式排序和搜索一個列表(List)。用於排序和搜索列表的靜態方法包含在類Collections中,但它們擁有與Arrays中差不多的簽名:sort(List)用於對一個實現了Comparable的對象列表進行排序;binarySearch(List,Object)用於查找列表中的某個對象;sort(List,Comparator)利用一個“比較器”對一個列表進行排序;

binarySearch(List,Object,Comparator)則用於查找那個列表中的一個對象(註釋⑨)。下面這個例子利用了預先定義好的CompClassAlphaComp來示範Collections中的各種排序工具:

//: ListSort.java
// Sorting and searching Lists with 'Collections'
package c08.newcollections;
import java.util.*;

public class ListSort {
  public static void main(String[] args) {
    final int SZ = 20;
    // Using "natural comparison method":
    List a = new ArrayList();
    for(int i = 0; i < SZ; i++)
      a.add(new CompClass(
        (int)(Math.random() *100)));
    Collection1.print(a);
    Collections.sort(a);
    Collection1.print(a);
    Object find = a.get(SZ/2);
    int loc = Collections.binarySearch(a, find);
    System.out.println("Location of " + find +
     " = " + loc);
    // Using a Comparator:
    List b = new ArrayList();
    for(int i = 0; i < SZ; i++)
      b.add(Array1.randString(4));
    Collection1.print(b);
    AlphaComp ac = new AlphaComp();
    Collections.sort(b, ac);
    Collection1.print(b);
    find = b.get(SZ/2);
    // Must use the Comparator to search, also:
    loc = Collections.binarySearch(b, find, ac);
    System.out.println("Location of " + find +
     " = " + loc);
  }
} ///:~

⑨:在本書寫作時,已宣佈了一個新的Collections.stableSort(),可用它進行合併式排序,但還沒有它的測試版問世。

這些方法的用法與在Arrays中的用法是完全一致的,只是用一個列表代替了數組。

TreeMap也必須根據Comparable或者Comparator對自己的對象進行排序。

8.7.8 實用工具

Collections類中含有其他大量有用的實用工具:

enumeration(Collection)

Produces an old-style Enumeration for the argument.

max(Collection)

min(Collection)

Produces the maximum or minimum element in the argument using the natural comparison method of the objects in the Collection.

max(Collection, Comparator)

min(Collection, Comparator)

Produces the maximum or minimum element in the Collection using the Comparator.

nCopies(int n, Object o)

Returns an immutable List of size n whose handles all point to o.

subList(List, int min, int max)

Returns a new List backed by the specified argument List that is a window into that argument with indexes starting at min and stopping just before max.
  • enumeration(Collection) 為參數產生原始風格的Enumeration(枚舉)

  • max(Collection)min(Collection) 在參數中用集合內對象的自然比較方法產生最大或最小元素

  • max(Collection,Comparator)min(Collection,Comparator) 在集合內用比較器產生最大或最小元素

  • nCopies(int n, Object o) 返回長度為n的一個不可變列表,它的所有引用均指向o

  • subList(List,int min,int max) 返回由指定參數列表後推得到的一個新列表。可將這個列表想象成一個“窗口”,它自索引為min的地方開始,正好結束於max的前面

注意min()max()都是隨同Collection對象工作的,而非隨同List,所以不必擔心Collection是否需要排序(就象早先指出的那樣,在執行一次binarySearch()——即二進制搜索——之前,必須對一個List或者一個數組執行sort())。

(1) 使CollectionMap不可修改

通常,創建CollectionMap的一個“只讀”版本顯得更有利一些。Collections類允許我們達到這個目標,方法是將原始容器傳遞進入一個方法,並令其傳回一個只讀版本。這個方法共有四種變化形式,分別用於Collection(如果不想把集合當作一種更特殊的類型對待)、ListSet以及Map。下面這個例子演示了為它們分別構建只讀版本的正確方法:

//: ReadOnly.java
// Using the Collections.unmodifiable methods
package c08.newcollections;
import java.util.*;

public class ReadOnly {
  public static void main(String[] args) {
    Collection c = new ArrayList();
    Collection1.fill(c); // Insert useful data
    c = Collections.unmodifiableCollection(c);
    Collection1.print(c); // Reading is OK
    //! c.add("one"); // Can't change it

    List a = new ArrayList();
    Collection1.fill(a);
    a = Collections.unmodifiableList(a);
    ListIterator lit = a.listIterator();
    System.out.println(lit.next()); // Reading OK
    //! lit.add("one"); // Can't change it

    Set s = new HashSet();
    Collection1.fill(s);
    s = Collections.unmodifiableSet(s);
    Collection1.print(s); // Reading OK
    //! s.add("one"); // Can't change it

    Map m = new HashMap();
    Map1.fill(m, Map1.testData1);
    m = Collections.unmodifiableMap(m);
    Map1.print(m); // Reading OK
    //! m.put("Ralph", "Howdy!");
  }
} ///:~

對於每種情況,在將其正式變為只讀以前,都必須用有有效的數據填充容器。一旦載入成功,最佳的做法就是用“不可修改”調用產生的引用替換現有的引用。這樣做可有效避免將其變成不可修改後不慎改變其中的內容。在另一方面,該工具也允許我們在一個類中將能夠修改的容器保持為private狀態,並可從一個方法調用中返回指向那個容器的一個只讀引用。這樣一來,雖然我們可在類裡修改它,但其他任何人都只能讀。

為特定類型調用“不可修改”的方法不會造成編譯期間的檢查,但一旦發生任何變化,對修改特定容器的方法的調用便會產生一個UnsupportedOperationException異常。

(2) CollectionMap的同步

synchronized關鍵字是“多線程”機制一個非常重要的部分。我們到第14章才會對這一機制作深入的探討。在這兒,大家只需注意到Collections類提供了對整個容器進行自動同步的一種途徑。它的語法與“不可修改”的方法是類似的:

//: Synchronization.java
// Using the Collections.synchronized methods
package c08.newcollections;
import java.util.*;

public class Synchronization {
  public static void main(String[] args) {
    Collection c =
      Collections.synchronizedCollection(
        new ArrayList());
    List list = Collections.synchronizedList(
      new ArrayList());
    Set s = Collections.synchronizedSet(
      new HashSet());
    Map m = Collections.synchronizedMap(
      new HashMap());
  }
} ///:~

在這種情況下,我們通過適當的“同步”方法直接傳遞新容器;這樣做可避免不慎暴露出未同步的版本。

新集合也提供了能防止多個進程同時修改一個容器內容的機制。若在一個容器裡迭代,同時另一些進程介入,並在那個容器中插入、刪除或修改一個對象,便會面臨發生衝突的危險。我們可能已傳遞了那個對象,可能它位位於我們前面,可能容器的大小在我們調用size()後已發生了收縮——我們面臨各種各樣可能的危險。針對這個問題,新的集合庫集成了一套解決的機制,能查出除我們的進程自己需要負責的之外的、對容器的其他任何修改。若探測到有其他方面也準備修改容器,便會立即產生一個ConcurrentModificationException(併發修改異常)。我們將這一機制稱為“立即失敗”——它並不用更復雜的算法在“以後”偵測問題,而是“立即”產生異常。

8.8 總結

下面複習一下由標準Java(1.0和1.1)庫提供的集合(BitSet未包括在這裡,因為它更象一種負有特殊使命的類):

(1) 數組包含了對象的數字化索引。它容納的是一種已知類型的對象,所以在查找一個對象時,不必對結果進行轉換處理。數組可以是多維的,而且能夠容納基本數據類型。但是,一旦把它創建好以後,大小便不能變化了。

(2) Vector(向量)也包含了對象的數字索引——可將數組和Vector想象成隨機訪問集合。當我們加入更多的元素時,Vector能夠自動改變自身的大小。但Vector只能容納對象的引用,所以它不可包含基本數據類型;而且將一個對象引用從集合中取出來的時候,必須對結果進行轉換處理。

(3) Hashtable(散列表)屬於Dictionary(字典)的一種類型,是一種將對象(而不是數字)同其他對象關聯到一起的方式。散列表也支持對對象的隨機訪問,事實上,它的整個設計模式都在突出訪問的“高速度”。

(4) Stack(棧)是一種“後入先出”(LIFO)的隊列。

若你曾經熟悉數據結構,可能會疑惑為何沒看到一套更大的集合。從功能的角度出發,你真的需要一套更大的集合嗎?對於Hashtable,可將任何東西置入其中,並以非常快的速度檢索;對於Enumeration(枚舉),可遍歷一個序列,並對其中的每個元素都採取一個特定的操作。那是一種功能足夠強勁的工具。

Hashtable沒有“順序”的概念。Vector和數組為我們提供了一種線性順序,但若要把一個元素插入它們任何一個的中部,一般都要付出“慘重”的代價。除此以外,隊列、拆散隊列、優先級隊列以及樹都涉及到元素的“排序”——並非僅僅將它們置入,以便以後能按線性順序查找或移動它們。這些數據結構也非常有用,這也正是標準C++中包含了它們的原因。考慮到這個原因,只應將標準Java庫的集合看作自己的一個起點。而且倘若必須使用Java 1.0或1.1,則可在需要超越它們的時候使用JGL。

如果能使用Java 1.2,那麼只使用新集合即可,它一般能滿足我們的所有需要。注意本書在Java 1.1身上花了大量篇幅,所以書中用到的大量集合都是隻能在Java1.1中用到的那些:VectorHashtable。就目前來看,這是一個不得以而為之的做法。但是,這樣處理亦可提供與老Java代碼更出色的向後兼容能力。若要用Java1.2寫新代碼,新的集合往往能更好地為你服務。

8.9 練習

(1) 新建一個名為Gerbil的類,在構造器中初始化一個int gerbilNumber(類似本章的Mouse例子)。為其寫一個名為hop()的方法,用它打印出符合hop()條件的Gerbil的編號。建一個Vector,併為Vector添加一系列Gerbil對象。現在,用elementAt()方法在Vector中遍歷,併為每個Gerbil都調用hop()

(2) 修改練習1,用Enumeration在調用hop()的同時遍歷Vector

(3) 在AssocArray.java中,修改這個例子,令其使用一個Hashtable,而不是AssocArray

(4) 獲取練習1用到的Gerbil類,改為把它置入一個Hashtable,然後將Gerbil的名稱作為一個String(鍵)與置入表格的每個Gerbil(值)都關聯起來。獲得用於keys()的一個Enumeration,並用它在Hashtable裡遍歷,查找每個鍵的Gerbil,打印出鍵,然後將gerbil告訴給hop()

(5) 修改第7章的練習1,用一個Vector容納Rodent(齧齒動物),並用EnumerationRodent序列中遍歷。記住Vector只能容納對象,所以在訪問單獨的Rodent時必須採用一個轉換(如RTTI)。

(6) 轉到第7章的中間位置,找到那個GreenhouseControls.java(溫室控制)例子,該例應該由三個文件構成。在Controller.java中,類EventSet僅是一個集合。修改它的代碼,用一個Stack代替EventSet。當然,這時可能並不僅僅用Stack取代EventSet這樣簡單;也需要用一個Enumeration遍歷事件集。可考慮在某些時候將集合當作Stack對待,另一些時候則當作Vector對待——這樣或許能使事情變得更加簡單。

(7) (有一定挑戰性)在與所有Java發行包配套提供的Java源碼庫中找出用於Vector的源碼。複製這些代碼,製作名為 intVector的一個特殊版本,只在其中包含int數據。思考是否能為所有基本數據類型都製作Vector的一個特殊版本。接下來,考慮假如製作一個鏈接列表類,令其能隨同所有基本數據類型使用,那麼會發生什麼情況。若在Java中提供了參數化類型,利用它們便可自動完成這一工作(還有其他許多好處)。