1. 程式人生 > 實用技巧 >萬用字元與集合

萬用字元與集合

萬用字元型別

萬用字元概念

萬用字元型別中, 允許型別引數變化。 例如, 萬用字元型別

Pair<? extends Employee>

表示任何泛型 Pair 型別, 它的型別引數是 Employee 的子類, 如 Pair<Manager>, 但不是 Pair<String>

假設要編寫一個列印僱員對的方法, 像這樣:

public static void printBuddies(Pair <Employee> p) { 
    Employee first = p.getFirst(); Employee second = p.getSecondO;
    Systefn.out.println(first.getName() 
+ " and " + second.getNameQ + " are buddies."); }

正如前面講到的,不能將 Pair<Manager> 傳遞給這個方法,這一點很受限制。解決的方 法很簡單:使用萬用字元型別:

public static void printBuddies(Pair<? extends Eiployee> p)

型別 Pair<Manager> 是 Pair<? extends Employee> 的子型別。

萬用字元的超型別限定

萬用字元限定與型別變數限定十分類似,但是,還有一個附加的能力,即可以指定一個超 型別限定(supertypebound), 如下所亦:

? super Manager

這個萬用字元限制為 Manager 的所有超型別。(已有的 super關鍵字十分準確地描述了這種 聯絡, 這一點令人感到非常欣慰。

證返回物件的型別。只能把它賦給一個 Object。 下面是一個典型的示例。有一個經理的陣列,並且想把獎金最高和最低的經理放在一個 Pair 物件中。Pair 的型別是什麼? 在這裡,Pair<Employee> 是合理的, Pair<Object> 也是合 理的,下面的方法將可以接受任何適當的 Pair:

public static void minmaxBonus(Manager[] a, Pair<? super
Manager> result) { if (a.length == 0) return; Manager rain = a[0]; Manager max = a[0]; for (int i *1 ; i < a.length; i++) { if (min.getBonusO > a[i].getBonus()) rain = a[i]; if (max.getBonusO < a[i].getBonus()) max = a[i]; } result.setFirst(min); result.setSecond(max); }

直觀地講,帶有超型別限定的萬用字元可以向泛型物件寫人,帶有子型別限定的萬用字元可 以從泛型物件讀取。

子型別限定的另一個常見的用法是作為一個函式式介面的引數型別。 例如、Collection介面有一個方法:

default boolean reniovelf(Predicated super E> filter)

這個方法會刪除所有滿足給定謂詞條件的元素。例如, 如果你不喜歡有奇怪雜湊碼 的員工,就可以如下將他們刪除:

ArrayList<Employee> staff = . . .;

Predicate<Object> oddHashCode = obj -> obj.hashCodeO %2 U 0;

staff.removelf(oddHashCode);

你希望傳入一個 Predicate<Object>, 而不只是 Predicate<Employee>。Super 萬用字元可 以使這個願望成真。

無限定萬用字元

可以使用無限定的萬用字元, 例如,Pair<?>。初看起來,這好像與原始的 Pair 型別一樣。 實際上,有很大的不同。型別 Pair<?> 有以下方法:

? getFirst()

void setFirst(?)

getFirst 的返回值只能賦給一個 Object。setFirst 方法不能被呼叫, 甚至不能用 Object 調 用。Pair<?> 和 Pair 本質的不同在於: 可以用任意 Object 物件呼叫原始 Pair 類的 setObject 方法。

萬用字元捕獲

編寫一個交換成對元素的方法:

public static void swap(Pair<?> p)

萬用字元不是型別變數, 因此, 不能在編寫程式碼中使用“ ?” 作為一種型別。也就是說, 下述 程式碼是非法的:

? t = p.getFirstO; // Error

p.setFirst(p_getSecond());

p.setSecond(t);

這是一個問題, 因為在交換的時候必須臨時儲存第一個元素。幸運的是, 這個問題有一 個有趣的解決方案。我們可以寫一個輔助方法 swapHelper, 如下所示:

public static <T> void swapHelper(Pair<T> p) { 
​
T t = p.getFirstO; 
​
p.setFirst(p.getSecond()); 
​
p.setSecond(t); 
​
} 

注意, swapHelper 是一個泛型方法, 而 swap不是, 它具有固定的 Pair<?> 型別的引數。

現在可以由 swap 呼叫 swapHelper: public static void swap(Pair<?> p) { swapHelper(p); }

在這種情況下,swapHelper 方法的引數 T 捕獲萬用字元。它不知道是哪種型別的萬用字元, 但是, 這是一個明確的型別,並且 <T>swapHelper 的定義只有在 T 指出型別時才有明確的含義。

程式清單 8-3 中的測試程式將前幾節討論的各種方法綜合在一起, 讀者從中可以看到它 們彼此之間的關聯。

//程式清單8-3 pair3/PairTest3.java
package pair3;
​
/**
 * @version 1.01 2012-01-26
 * @author Cay Horstmann
 */
public class PairTest3
{
   public static void main(String[] args)
   {
      Manager ceo = new Manager("Gus Greedy", 800000, 2003, 12, 15);
      Manager cfo = new Manager("Sid Sneaky", 600000, 2003, 12, 15);
      Pair<Manager> buddies = new Pair<>(ceo, cfo);      
      printBuddies(buddies);
​
      ceo.setBonus(1000000);
      cfo.setBonus(500000);
      Manager[] managers = { ceo, cfo };
​
      Pair<Employee> result = new Pair<>();
      minmaxBonus(managers, result);
      System.out.println("first: " + result.getFirst().getName() 
         + ", second: " + result.getSecond().getName());
      maxminBonus(managers, result);
      System.out.println("first: " + result.getFirst().getName() 
         + ", second: " + result.getSecond().getName());
   }
​
   public static void printBuddies(Pair<? extends Employee> p)
   {
      Employee first = p.getFirst();
      Employee second = p.getSecond();
      System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
   }
​
   public static void minmaxBonus(Manager[] a, Pair<? super Manager> result)
   {
      if (a == null || a.length == 0) return;
      Manager min = a[0];
      Manager max = a[0];
      for (int i = 1; i < a.length; i++)
      {
         if (min.getBonus() > a[i].getBonus()) min = a[i];
         if (max.getBonus() < a[i].getBonus()) max = a[i];
      }
      result.setFirst(min);
      result.setSecond(max);
   }
​
   public static void maxminBonus(Manager[] a, Pair<? super Manager> result)
   {
      minmaxBonus(a, result);
      PairAlg.swapHelper(result); // OK--swapHelper captures wildcard type
   }
}
​
class PairAlg
{
   public static boolean hasNulls(Pair<?> p)
   {
      return p.getFirst() == null || p.getSecond() == null;
   }
​
   public static void swap(Pair<?> p) { swapHelper(p); }
​
   public static <T> void swapHelper(Pair<T> p)
   {
      T t = p.getFirst();
      p.setFirst(p.getSecond());
      p.setSecond(t);
   }
}

反射和泛型

泛型 Class 類

現在, Class 類是泛型的。例如, String.class 實際上是一個 <: 以8<8出呢> 類的物件(事 實上,是唯一的物件)。

型別引數十分有用, 這是因為它允許 ClaSS<T> 方法的返回型別更加具有針對性。下面 Class<T> 中的方法就使用了型別引數:

T newInstance() 
​
T cast(Object obj) 
​
T[] getEnumConstants() Class<? super T> getSuperclass() 
​
Constructors getConstructor( C1ass... parameterTypes) 
​
Constructors getDeclaredConstructor(Class... parameterTypes) 

newlnstance 方法返回一個例項,這個例項所屬的類由預設的構造器獲得。它的返回型別 目前被宣告為 T, 其型別與 Class<T> 描述的類相同,這樣就免除了型別轉換。

如果給定的型別確實是 T 的一個子型別,cast 方法就會返回一個現在宣告為型別 T 的對 象, 否則,丟擲一個 BadCastException 異常。

如果這個類不是 enum 類或型別 T 的列舉值的陣列, getEnumConstants方法將返回 null。

最後, getConstructor 與 getdeclaredConstructor方 法 返 回 一 個 Constructor<T> 物件。 Constructor 類也已經變成泛型, 以便 newlnstance 方法有一個正確的返回型別。

使用 Class<T> 引數進行型別匹配

有時, 匹配泛型方法中的 Class<I> 引數的型別變數很有實用價值。下面是一 標準的示例:

public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException, IllegalAccessException {
​
 return new Pair<>(c.newInstance(), c.newInstance()); 
​
}

如果呼叫

makePair(Employee.class)

Employee.class 是型別 Class<Employee> 的一個物件。makePair方法的型別引數 T 同 Employee 匹配, 並且編譯器可以推斷出這個方法將返回一個 Pair<Employee>

虛擬機器中的泛型型別資訊

Java 泛型的卓越特性之一是在虛擬機器中泛型型別的擦除。令人感到奇怪的是, 擦除的類 仍然保留一些泛型祖先的微弱記憶。例如, 原始的 Pair 類知道源於泛型類 Pair<T>, 即使一 個 Pair 型別的物件無法區分是由 PaiKString> 構造的還是由 PaiKEmployee> 構造的。

類似地,看一下方法

public static Comparable min(Coniparable[] a)

這是一個泛型方法的擦除

public static <T extends Comparable<? super T>>T min(T[] a)

可以使用反射 API 來確定:

•這個泛型方法有一個叫做 T 的型別引數。

•這個型別引數有一個子型別限定, 其自身又是一個泛型型別。

•這個限定型別有一個萬用字元引數。

•這個萬用字元引數有一個超型別限定。

•這個泛型方法有一個泛型陣列引數。

換句話說,需要重新構造實現者宣告的泛型類以及方法中的所有內容。但是,不會知道 對於特定的物件或方法呼叫,如何解釋型別引數。

程式清單 8-4 中使用泛型反射 AI>I 打印出給定類的有關內容。如果用 Pair類執行, 將會 得到下列報告:

class Pair<T> extends java.lang.Object

public T getFirst()

public T getSecond()

public void setFirst(T)

public void setSecond(T)

如果使用 PairTest2 目錄下的 ArrayAlg執行, 將會得到下列報告:

public static <T extends java.lang.Comparable〉Pair<T> minmax(T[])

//程式清單 8-4 genericReflection/GenericReflectionTest.java
package genericReflection;
​
import java.lang.reflect.*;
import java.util.*;
​
/**
 * @version 1.10 2007-05-15
 * @author Cay Horstmann
 */
public class GenericReflectionTest
{
   public static void main(String[] args)
   {
      // read class name from command line args or user input
      String name;
      if (args.length > 0) name = args[0];
      else
      {
         Scanner in = new Scanner(System.in);
         System.out.println("Enter class name (e.g. java.util.Collections): ");
         name = in.next();
      }
​
      try
      {
         // print generic info for class and public methods
         Class<?> cl = Class.forName(name);
         printClass(cl);
         for (Method m : cl.getDeclaredMethods())
            printMethod(m);
      }
      catch (ClassNotFoundException e)
      {
         e.printStackTrace();
      }
   }
​
   public static void printClass(Class<?> cl)
   {
      System.out.print(cl);
      printTypes(cl.getTypeParameters(), "<", ", ", ">", true);
      Type sc = cl.getGenericSuperclass();
      if (sc != null)
      {
         System.out.print(" extends ");
         printType(sc, false);
      }
      printTypes(cl.getGenericInterfaces(), " implements ", ", ", "", false);
      System.out.println();
   }
​
   public static void printMethod(Method m)
   {
      String name = m.getName();
      System.out.print(Modifier.toString(m.getModifiers()));
      System.out.print(" ");
      printTypes(m.getTypeParameters(), "<", ", ", "> ", true);
​
      printType(m.getGenericReturnType(), false);
      System.out.print(" ");
      System.out.print(name);
      System.out.print("(");
      printTypes(m.getGenericParameterTypes(), "", ", ", "", false);
      System.out.println(")");
   }
​
   public static void printTypes(Type[] types, String pre, String sep, String suf, 
         boolean isDefinition)
   {
      if (pre.equals(" extends ") && Arrays.equals(types, new Type[] { Object.class })) return;
      if (types.length > 0) System.out.print(pre);
      for (int i = 0; i < types.length; i++)
      {
         if (i > 0) System.out.print(sep);
         printType(types[i], isDefinition);
      }
      if (types.length > 0) System.out.print(suf);
   }
​
   public static void printType(Type type, boolean isDefinition)
   {
      if (type instanceof Class)
      {
         Class<?> t = (Class<?>) type;
         System.out.print(t.getName());
      }
      else if (type instanceof TypeVariable)
      {
         TypeVariable<?> t = (TypeVariable<?>) type;
         System.out.print(t.getName());
         if (isDefinition)
            printTypes(t.getBounds(), " extends ", " & ", "", false);
      }
      else if (type instanceof WildcardType)
      {
         WildcardType t = (WildcardType) type;
         System.out.print("?");
         printTypes(t.getUpperBounds(), " extends ", " & ", "", false);
         printTypes(t.getLowerBounds(), " super ", " & ", "", false);
      }
      else if (type instanceof ParameterizedType)
      {
         ParameterizedType t = (ParameterizedType) type;
         Type owner = t.getOwnerType();
         if (owner != null)
         {
            printType(owner, false);
            System.out.print(".");
         }
         printType(t.getRawType(), false);
         printTypes(t.getActualTypeArguments(), "<", ", ", ">", false);
      }
      else if (type instanceof GenericArrayType)
      {
         GenericArrayType t = (GenericArrayType) type;
         System.out.print("");
         printType(t.getGenericComponentType(), isDefinition);
         System.out.print("[]");
      }
   }
}
​

第九章 集合

Java 集合框架

將集合的介面與實現分離

與現代的資料結構類庫的常見情況一樣, Java 集合類庫也將介面( interface) 與 實 現 (implementation) 分離。

佇列介面指出可以在佇列的尾部新增元素, 在佇列的頭部刪除元素,並且可以査找佇列 中元素的個數。當需要收集物件, 並按照“ 先進先出” 的規則檢索物件時就應該使用佇列(見 圖 9-1 )。

佇列介面的最簡形式可能類似下面這樣:

public interface Queue<E> // a simplified form of the interface in the standard library 
​
{ 
​
    void add(E element); 
​
    E remove(); 
​
    int size();
​
}

這個介面並沒有說明佇列是如何實現的。佇列通常有兩種實現方式: 一種是使用迴圈數 組;另一種是使用連結串列(見圖 9-2 )。

每一個實現都可以通過一個實現了 Queue 介面的類表示。

public class CircularArrayQueue<E> implements Queue<E> // not an actual library class 
{
    private int head;
    private int tail; 
    CircularArrayQueue(int capacity) { .. . } 
    public void add(E element) { . . . } 
    public E remove0 { . .. } 
    public int size() { ... } 
    private E[] elements;
}
public class LinkedListQueue<E> iipleients Queue<E> // not an actual library class 
{ 
    private Link head; 
    private Link tail; 
    LinkedListQueueO { .. . } 
    public void add(E element) { ...} 
    public E remove() { ... } 
    public int size() { ... } 
} 

[注] 實際上,Java 類庫沒有名為 CircularArrayQueue 和 LinkedListQueue 的類。 這裡, 只是以這些類作為示例, 解釋一下集合介面與實現在概念上的不同。如果需要一個迴圈 陣列佇列,就可以使用 ArrayDeque 類。如果需要一個連結串列佇列, 就直接使用 LinkedList 類, 這個類實現了 Queue 介面。

當在程式中使用佇列時,一旦構建了集合就不需要知道究竟使用了哪種實現。因此, 只 有在構建集合物件時,使用具體的類才有意義。可以使用介面型別存放集合的引用。

Queue<Customer> expresslane = new CircularArrayQueue<>(100):

expressLane.add(new Customer("Harry"));、

迴圈陣列是一個有界集合, 即容量有限。如果程式中要收集的物件數量沒有上限, 就最 好使用連結串列來實現。

Collection 介面

在 Java 類庫中,集合類的基本介面是 Collection 介面。這個介面有兩個基本方法:

public interface Collection<b { 
    boolean add(E element); 
    Iterator<E> iterator();
}

add方法用於向集合中新增元素。如果新增元素確實改變了集合就返回 true, 如果集合 沒有發生變化就返回 false。例如, 如果試圖向集中新增一個物件, 而這個物件在集中已經存 在,這個新增請求就沒有實效,因為集中不允許有重複的物件。

iterator方法用於返回一個實現了 Iterator 介面的物件。可以使用這個迭代器物件依次訪 問集合中的元素。

迭代器

Iterator 介面包含 4個方法:

public interface Iterator<E> { 
    E next(); 
    boolean hasNext(); 
    void remove(); 
    default void forEachRemaining(Consumer<? super E> action); 
}

通過反覆呼叫 next 方法,可以逐個訪問集合中的每個元素。但是,如果到達了集合的末 尾,next 方法將丟擲一個 NoSuchElementException。 因此,需要在呼叫next 之前呼叫 hasNext 方法。如果迭代器物件還有多個供訪問的元素, 這個方法就返回 true。如果想要査看集合中的 所有元素,就請求一個迭代器,並在hasNext返回 true 時反覆地呼叫 next方法。例如:

Collection<String> c = . . .; 
Iterator<String> iter = c.iterator(); 
while (iter.hasNextO) { 
    String element = iter.next(); 
    dosomethingwith element 
}

用“ foreach” 迴圈可以更加簡練地表示同樣的迴圈操作:

for (String element : c) { 
    dosomethingwith element 
} 

編譯器簡單地將“ foreach” 迴圈翻譯為帶有迭代器的迴圈。

泛型實用方法

由於 Collection與 Iterator 都是泛型介面,可以編寫操作任何集合型別的實用方法。例 如,下面是一個檢測任意集合是否包含指定元素的泛型方法:

public static <E> boolean contains(Collection<E> c, Object obj) { 
    for (E element : c) 
        if (element,equals(obj)) 
            return true; 
    return false; 
}

Java類庫的設計者認為:這些實用方法中的某些方法非常有用,應該將它們提供給使用者使 用。這樣,類庫的使用者就不必自己重新構建這些方法了。contains 就是這樣一個實用方法。

集合框架中的介面

Java 集合框架為不同型別的集合定義了大量介面, 如圖 9-4 所示。

集合有兩個基本介面:Collection 和 Map。

List 是一個有序集合(or辦 元 素 會 增 加 到 容 器 中 的 特 定 位 置。可 以 採 用 兩種方式訪問元素:使用迭代器訪問, 或者使用一個整數索引來訪問。後一種方法稱為隨機訪問(random access), 因為這樣可以按任意順序訪問元素。與之不同, 使用迭代器訪問時,必須順序地訪問元素。

List 介面定義了多個用於隨機訪問的方法:Java

void add(int index, E element) 
     void remove(int index) 
     E get(int index) 
     E set(int index, E element)

Listlterator 介面是 Iterator 的一個子介面。它定義了一個方法用於在迭代器位置前面增加 一個元素:

void add(E element)

坦率地講,集合框架的這個方面設計得很不好。實際中有兩種有序集合,其效能開銷有 很大差異。由陣列支援的有序集合可以快速地隨機訪問,因此適合使用 List 方法並提供一個 整數索引來訪問。與之不同, 連結串列儘管也是有序的, 但是隨機訪問很慢,所以最好使用迭代 器來遍歷。如果原先提供兩個介面就會容易一些了。

具體的集合

鏈 表

陣列和陣列列表 都有一個重大的缺陷。這就是從陣列的中間位置刪除一個元素要付出很大的代價,其原因是 陣列中處於被刪除元素之後的所有元素都要向陣列的前端移動(見圖 9-6 )。在陣列中間的位 置上插入一個元素也是如此。

另外一個大家非常熟悉的資料結構一連結串列(linked list) 解決了這個問題。儘管陣列在 連續的儲存位置上存放物件引用, 但連結串列卻將每個物件存放在獨立的結點中。每個結點還存 放著序列中下一個結點的引用。在 Java 程式設計語言中,所有連結串列實際上都是雙向連結的 (doubly linked)— —即每個結點還存放著指向前驅結點的引用(見圖 9-7 )。

從連結串列中間刪除一個元素是一個很輕鬆的操作, 即需要更新被刪除元素附近的連結(見 圖 9-8 )。

在下面的程式碼示例中, 先新增 3 個元素, 然後再將第 2 個元素刪除:

List<String> staff = new LinkedList<>(); // LinkedList implements List 
staff.add("Amy"); 
staff.add(MBobH);
staff.add("Carl"); 
Iterator iter = staff.iterator() ; 
String first = iter.next();// visit first element 
String second =iter.next();//visit second element 
iter.remove(); // remove last visited element

但是, 連結串列與泛型集合之間有一個重要的區別。鏈 表 是 一 個 有 序 集 合(ordered collection), 每個物件的位置十分重要。LinkedList.add 方法將物件新增到連結串列的尾部。但是, 常常需要將元素新增到連結串列的中間。由於迭代器是描述集合中位置的, 所以這種依賴於位置 的 add 方法將由迭代器負責。只有對自然有序的集合使用迭代器新增元素才有實際意義。例 如, 下一節將要討論的集(set) 型別,其中的元素完全無序。因此, 在 Iterator 介面中就沒有 add 方法。

程式清單 9-1 中的程式使用的就是連結串列。它簡單地建立了兩個連結串列, 將它們合併在一起, 然後從第二個連結串列中每間隔一個元素刪除一個元素, 最後測試 removeAIl 方法。建議跟蹤一 下程式流程, 並要特別注意迭代器。

//程式清單 9-1 linkedList/LinkedListTest.java 
package linkedList;
​
import java.util.*;
​
/**
 * This program demonstrates operations on linked lists.
 * @version 1.11 2012-01-26
 * @author Cay Horstmann
 */
public class LinkedListTest
{
   public static void main(String[] args)
   {
      List<String> a = new LinkedList<>();
      a.add("Amy");
      a.add("Carl");
      a.add("Erica");
​
      List<String> b = new LinkedList<>();
      b.add("Bob");
      b.add("Doug");
      b.add("Frances");
      b.add("Gloria");
​
      // merge the words from b into a
​
      ListIterator<String> aIter = a.listIterator();
      Iterator<String> bIter = b.iterator();
​
      while (bIter.hasNext())
      {
         if (aIter.hasNext()) aIter.next();
         aIter.add(bIter.next());
      }
​
      System.out.println(a);
​
      // remove every second word from b
​
      bIter = b.iterator();
      while (bIter.hasNext())
      {
         bIter.next(); // skip one element
         if (bIter.hasNext())
         {
            bIter.next(); // skip next element
            bIter.remove(); // remove that element
         }
      }
​
      System.out.println(b);
​
      // bulk operation: remove all words in b from a
​
      a.removeAll(b);
​
      System.out.println(a);
   }
}

陣列列表

List 介面用於描述一個有序集合,並且集合中每個元素的位置十分重要。有兩種訪問元素的協議:一種是用迭代 器, 另一種是用 get 和 set 方法隨機地訪問每個元素。後者不適用於連結串列, 但對陣列卻很有 用。集合類庫提供了一種大家熟悉的 ArrayList 類, 這個類也實現了 List 介面。ArrayList 封 裝了一個動態再分配的物件陣列。

[注] 對於一個經驗豐富的 Java 程式設計師來說, 在需要動態陣列時, 可能會使用 Vector 類。 為什麼要用 ArrayList 取代 Vector 呢? 原因很簡單:Vector 類的所有方法都是同步的。 可 以由兩個執行緒安全地訪問一個 Vector 物件。但是, 如果由一個執行緒訪問 Vector, 程式碼要 在同步操作上耗費大量的時間。這種情況還是很常見的。 而 ArrayList 方法不是同步的, 因此,建議在不需要同步時使用 ArrayList, 而不要使用 Vector。

雜湊集

連結串列和陣列可以按照人們的意願排列元素的次序。但是,如果想要査看某個指定的元 素, 卻又忘記了它的位置, 就需要訪問所有元素, 直到找到為止。如果集合中包含的元 素很多, 將會消耗很多時間。如果不在意元素的順序,可以有幾種能夠快速査找元素的數 據結構。其缺點是無法控制元素出現的次序。它們將按照有利於其操作目的的原則組織 資料。

有一種眾所周知的資料結構, 可以快速地査找所需要的物件, 這就是散列表(hash table)。散列表為每個物件計算一個整數, 稱為雜湊碼(hashcode)。雜湊碼是由物件的例項 域產生的一個整數。更準確地說, 具有不同資料域的物件將產生不同的雜湊碼。

在 Java中,散列表用連結串列陣列實現。每個列表 被稱為桶(bucket) (參看圖 9-10 )。要想査找表中對 象的位置, 就要先計算它的雜湊碼, 然後與桶的總 數取餘, 所得到的結果就是儲存這個元素的桶的索 引。。例如, 如果某個物件的雜湊碼為 76268, 並且 有 128 個桶, 物件應該儲存在第 108號桶中(76268 除以 128餘 108 )。

雜湊集迭代器將依次訪問所有的桶。 由於雜湊將元素分散在表的各個位置上,所以訪問 它們的順序幾乎是隨機的。只有不關心集合中元素的順序時才應該使用 HashSet。

本節末尾的示例程式(程式清單 9-2 ) 將從 System.ii 讀取單詞,然後將它們新增到集 中,最 後, 再 打 印 出 集 中 的 所 有 單 詞。例 如,可 以 將 (可 以 從 http://www. gutenberg.org 找到)的文字輸人到這個程式中,並從命令列 shell 執行:

java SetTest < alice30.txt

這個程式將讀取輸人的所有單詞, 並且將它們新增到雜湊集中。然後遍歷雜湊集中的不 同單詞,最後打印出單詞的數量 (Alice in Wonderland共有 5909 個不同的單詞,包括開頭的 版權宣告) 。單詞以隨機的順序出現。

[警告] 在更改集中的元素時要格外小心。如果元素的雜湊碼發生了改變, 元素在資料結 構中的位置也會發生變化。
//程式清單 9-2 set/SetTest.java
package set;
​
import java.util.*;
​
/**
 * This program uses a set to print all unique words in System.in.
 * @version 1.11 2012-01-26
 * @author Cay Horstmann
 */
public class SetTest
{
   public static void main(String[] args)
   {
      Set<String> words = new HashSet<>(); // HashSet implements Set
      long totalTime = 0;
​
      Scanner in = new Scanner(System.in);
      while (in.hasNext())
      {
         String word = in.next();
         long callTime = System.currentTimeMillis();
         words.add(word);
         callTime = System.currentTimeMillis() - callTime;
         totalTime += callTime;
      }
​
      Iterator<String> iter = words.iterator();
      for (int i = 1; i <= 20 && iter.hasNext(); i++)
         System.out.println(iter.next());
      System.out.println(". . .");
      System.out.println(words.size() + " distinct words. " + totalTime + " milliseconds.");
   }
}

樹集

TreeSet類與雜湊集十分類似, 不過, 它比雜湊集有所改進。樹集是一個有序集合 ( sorted collection)o 可以以任意順序將元素插入到集合中。在對集合進行遍歷時,每個值將 自動地按照排序後的順序呈現。例如,假設插入 3 個字串,然後訪問新增的所有元素。

SortedSet<String> sorter = new TreeSetoO; // TreeSet implements SortedSet 
sorter.add("Bob");
sorter.add("Any"); 
sorter.add("Carl"); 
for (String s : sorter) System.println(s);

在程式清單 9-3 的程式中建立了兩個 Item 物件的樹集。第一個按照部件編號排序, 這是 Item 物件的預設順序。第二個通過使用一個定製的比較器來按照描述資訊排序。

//程式清單 9-3 treeSet/TreeSetTest.java
package treeSet;
​
/**
   @version 1.12 2012-01-26
   @author Cay Horstmann
*/import java.util.*;
​
/**
   This program sorts a set of item by comparing
   their descriptions.
*/
public class TreeSetTest
{  
   public static void main(String[] args)
   {  
      SortedSet<Item> parts = new TreeSet<>();
      parts.add(new Item("Toaster", 1234));
      parts.add(new Item("Widget", 4562));
      parts.add(new Item("Modem", 9912));
      System.out.println(parts);
​
      SortedSet<Item> sortByDescription = new TreeSet<>(new
         Comparator<Item>()
         {  
            public int compare(Item a, Item b)
            {  
               String descrA = a.getDescription();
               String descrB = b.getDescription();
               return descrA.compareTo(descrB);
            }
         });
​
      sortByDescription.addAll(parts);
      System.out.println(sortByDescription);
   }
}

//程式清單 9-4 treeSet/Item.java
package treeSet;
​
import java.util.*;
​
/**
 * An item with a description and a part number.
 */
public class Item implements Comparable<Item>
{
   private String description;
   private int partNumber;
​
   /**
    * Constructs an item.
    * @param aDescription the item's description
    * @param aPartNumber the item's part number
    */
   public Item(String aDescription, int aPartNumber)
   {
      description = aDescription;
      partNumber = aPartNumber;
   }
​
   /**
    * Gets the description of this item.
    * @return the description
    */
   public String getDescription()
   {
      return description;
   }
​
   public String toString()
   {
      return "[description=" + description + ", partNumber=" + partNumber + "]";
   }
​
   public boolean equals(Object otherObject)
   {
      if (this == otherObject) return true;
      if (otherObject == null) return false;
      if (getClass() != otherObject.getClass()) return false;
      var other = (Item) otherObject;
      return Objects.equals(description, other.description) && partNumber == other.partNumber;
   }
​
   public int hashCode()
   {
      return Objects.hash(description, partNumber);
   }
​
   public int compareTo(Item other)
   {
      int diff = Integer.compare(partNumber, other.partNumber);
      return diff != 0 ? diff : description.compareTo(other.description);
   }
}
​

佇列與雙端佇列

佇列可以讓人們有效地在尾部新增一個元素, 在頭部刪除一個元 素。有兩個端頭的佇列, 即雙端佇列,可以讓人們有效地在頭部和尾部同時新增或刪除元 素。不支援在佇列中間新增元素。在 Java SE 6中引人了 Deque 介面,並由 ArrayDeque 和 LinkedList 類實現。這兩個類都提供了雙端佇列,而且在必要時可以增加佇列的長度。

優先順序佇列

優先順序佇列(priorityqueue) 中的元素可以按照任意的順序插人,卻總是按照排序的順序 進行檢索。也就是說,無論何時呼叫 remove方法,總會獲得當前優先順序佇列中最小的元素。 然而,優先順序佇列並沒有對所有的元素進行排序。如果用迭代的方式處理這些元素,並不需 要對它們進行排序。優先順序佇列使用了一個優雅且高效的資料結構,稱為堆(heap)。堆是一 個可以自我調整的二叉樹,對樹執行新增(add) 和刪除(remore) 操作, 可以讓最小的元素 移動到根,而不必花費時間對元素進行排序。

與 TreeSet—樣,一個優先順序佇列既可以儲存實現了 Comparable 介面的類物件, 也可以 儲存在構造器中提供的 Comparator 物件。 使用優先順序佇列的典型示例是任務排程。

每一個任務有一個優先順序,任務以隨機順序添 加到佇列中。每當啟動一個新的任務時,都將優先順序最高的任務從佇列中刪除。

程式清單 9-5顯示了一個正在執行的優先順序佇列。與 TreeSet中的迭代不同,這裡的迭代 並不是按照元素的排列順序訪問的。而刪除卻總是刪掉剩餘元素中優先順序數最小的那個元素。

//程式清單 9-5 priorityQueue/PriorityQueueTest.java
package priorityQueue;
​
import java.util.*;
​
/**
 * This program demonstrates the use of a priority queue.
 * @version 1.01 2012-01-26
 * @author Cay Horstmann
 */
public class PriorityQueueTest
{
   public static void main(String[] args)
   {
      PriorityQueue<GregorianCalendar> pq = new PriorityQueue<>();
      pq.add(new GregorianCalendar(1906, Calendar.DECEMBER, 9)); // G. Hopper
      pq.add(new GregorianCalendar(1815, Calendar.DECEMBER, 10)); // A. Lovelace
      pq.add(new GregorianCalendar(1903, Calendar.DECEMBER, 3)); // J. von Neumann
      pq.add(new GregorianCalendar(1910, Calendar.JUNE, 22)); // K. Zuse
​
      System.out.println("Iterating over elements...");
      for (GregorianCalendar date : pq)
         System.out.println(date.get(Calendar.YEAR));
      System.out.println("Removing elements...");
      while (!pq.isEmpty())
         System.out.println(pq.remove().get(Calendar.YEAR));
   }
}