萬用字元與集合
萬用字元概念
萬用字元型別中, 允許型別引數變化。 例如, 萬用字元型別
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<? superManager> 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 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)); } }