1. 程式人生 > >Java程式設計思想 第十七章:深入研究容器

Java程式設計思想 第十七章:深入研究容器

1. 完整的容器分類法

下面是集合類庫的完整圖:
在這裡插入圖片描述

Java SE5新添加了:

  1. Queue介面(LinkedList已經為實現該介面做了修改)及其實現PriorityQueue和各種風格的BlockingQueue。
  2. ConcurrentMap介面及其實現ConcurrentHashMap,用於多執行緒機制
  3. CopyOnWriteArrayList和CopyOnWriteArraySet,用於多執行緒機制
  4. EnumSet和EnumMap,為使用enum而設計的set和map的特殊實現
  5. 在Collections類中的多個便利方法

虛線框表示abstract類,它們只是部分實現了特定介面的工具。例如,如果要建立自己的Set,那並不用從Set介面開始並實現其中全部方法,只需從AbstractSet繼承,然後執行一些建立新類必須的工作。

2. 填充容器

Collections類也有一些實用的static方法,其中包括fill,同Arrays一樣只複製同一物件引用來填充整個容器的,並且只對List物件有用,但是所產生的列表可以傳遞給構造器或addAll方法

  • Collections.addAll() 將一些元素新增到一個Collection中
  • Collections.nCopies() 複製一些元素到Collections中,返回一個List集合
  • Collections.fill() 複製元素填充到集合當中

3. 使用Abstract類

每個java.util容器都有其自己的Abstract類,它們提供了該容器的部分實現,因此必須做的只是去實現那些產生想要的容器所必需的方法。

享元模式:可在普通解決方案需要過多物件,或者產生普通物件太佔用空間時使用享元。享元模式使得物件的一部分可以被具體化,因此,與物件中的所有事物都包含在物件內部不同,可以在更加高效的外部表中查詢物件的一部分或整體。

  • AbstractList 實現了List介面
  • AbstractMap 實現了Map介面
  • AbstractSet 實現了Set介面

4.Collection的功能方法(Collection是一個介面)

下面表格列出了可以通過Collection執行的所有操作。它們也可以是通過Set或者List執行的所有操作。

在這裡插入圖片描述

Iterator迭代器介面方法:

  • hasNext(); 判斷是否存在下一個物件元素
  • next(); 獲取下一個元素
  • remove(); 移除元素

4. 可選操作

執行各種不同的新增和移除的方法在Collection介面中都是可選操作。這意味著實現類並不需要為這些方法提供功能定義。

可選操作宣告呼叫某些方法將不會執行有意義的行為,相反,它們會丟擲異常。如果一個操作是可選的,編譯器仍舊會嚴格要求你只能呼叫該介面中的方法。

  1. UnsupportedOperationException必須是一種罕見的事件。即,對於大多數類來說,所有的操作都應該是可以工作的,只有在特例中才會有未獲得支援的操作。在Java容器類庫中確實如此,因為只有99%的時間裡面使用的所有的容器類,如ArrayList、LinkedList、HashSet和HashMap,以及其他具體的實現,都支援所有的操作。
  2. 如果一個操作是為獲得支援的,那麼在實現介面的時候就可能會導致UnsupportedOperationException異常。

4.1 未獲支援的操作

最常見的未獲支援的操作,源於背後由固定尺寸的資料結構支援的容器。當使用Arrays.asList將陣列轉換為List時,就會得到這樣的容器。還可以通過使用Collections類中不可修改的方法,選擇建立任何會丟擲UnsupportedOperationException的容器:

因為Arrays.asList會生成一個List,它基於一個固定大小的陣列,僅支援那些不會改變陣列大小的操作。任何會引起對底層資料結構的尺寸進行修改的方法都會產生一個UnsupportedOperationException異常,以表示對未獲支援操作的呼叫。

Arrays.asList產生固定尺寸的List,而Collections.unmodifiableList產生不可修改的列表。set方法可以在二者產生的物件之間有粒度上的變化。Arrays.asList可以修改元素,沒有違反尺寸固定的特性。

5. List的功能方法

List介面:

  • get(int); 獲取指定索引位置的列表元素
  • set(int,E); 設定指定索引位置的列表元素
  • add(int,E); 將指定的元素新增到此列表的索引位置。
  • remove(int); 移除指定索引位置的元素;
  • indexOf(Object); 從列表頭部查詢Object物件元素
  • lastIndexOf(Objcet); 從列表尾部查詢Object物件元素
  • listIterator(); 返回列表迭代器
  • listIterator(int); 返回指定索引位置的列表迭代器
  • subList(int,int); 該方法返回的是父list一個檢視,從fromIndex(包含),到toIndex(不包含)

在這裡插入圖片描述

ListIterator迭代器介面方法:

  • hasPrevious(); 如果以逆向遍歷列表,列表迭代器前面還有元素,則返回 true,否則返回false
  • previous(); 返回列表中ListIterator指向位置前面的元素
  • set(E); 從列表中將next()或previous()返回的最後一個元素返回的最後一個元素更改為指定元素e
  • add(E); 將指定的元素插入列表,插入位置為迭代器當前位置之前
  • nextIndex(); 返回列表中ListIterator所需位置後面元素的索引
  • previousIndex(); 返回列表中ListIterator所需位置前面元素的索引

basicTest包含每個list都可以執行的操作;iterMotion使用Iterator遍歷元素;對應的iterManipulation使用Iterator修改元素;testVisual用以檢視List的操作效果;

public class Lists {
  private static boolean b;
  private static String s;
  private static int i;
  private static Iterator<String> it;
  private static ListIterator<String> lit;
  public static void basicTest(List<String> a) {
    a.add(1, "x"); // 在位置1新增
    a.add("x"); // 在結尾新增
    // 新增一個集合
    a.addAll(Countries.names(25));
    // 從3開始新增一個集合
    a.addAll(3, Countries.names(25));
    b = a.contains("1"); // 是否存在
    // 整個集合都在裡面嗎
    b = a.containsAll(Countries.names(25));
    // 隨機訪問列表, ArrayList簡單, LinkedList昂貴
    s = a.get(1); // Get (typed) object at location 1
    i = a.indexOf("1"); // Tell index of object
    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
    a.remove(1); // Remove location 1
    a.remove("3"); // Remove this object
    a.set(1, "y"); // Set location 1 to "y"
    a.retainAll(Countries.names(25));
    a.removeAll(Countries.names(25));
    i = a.size(); 
    a.clear();
  }
  public static void iterMotion(List<String> a) {
    ListIterator<String> it = a.listIterator();
    b = it.hasNext();
    b = it.hasPrevious();
    s = it.next();
    i = it.nextIndex();
    s = it.previous();
    i = it.previousIndex();
  }
  public static void iterManipulation(List<String> a) {
    ListIterator<String> it = a.listIterator();
    it.add("47");
    // Must move to an element after add():
    it.next();
    // Remove the element after the newly produced one:
    it.remove();
    // Must move to an element after remove():
    it.next();
    // Change the element after the deleted one:
    it.set("47");
  }
  public static void testVisual(List<String> a) {
    print(a);
    List<String> b = Countries.names(25);
    print("b = " + b);
    a.addAll(b);
    a.addAll(b);
    print(a);
    // Insert, remove, and replace elements
    // using a ListIterator:
    ListIterator<String> x = a.listIterator(a.size()/2);
    x.add("one");
    print(a);
    print(x.next());
    x.remove();
    print(x.next());
    x.set("47");
    print(a);
    // Traverse the list backwards:
    x = a.listIterator(a.size());
    while(x.hasPrevious()) {
      printnb(x.previous() + " ");
    }
    print();
    print("testVisual finished");
  }
  // There are some things that only LinkedLists can do:
  public static void testLinkedList() {
    LinkedList<String> ll = new LinkedList<String>();
    ll.addAll(Countries.names(25));
    print(ll);
    // Treat it like a stack, pushing:
    ll.addFirst("one");
    ll.addFirst("two");
    print(ll);
    // Like "peeking" at the top of a stack:
    print(ll.getFirst());
    // Like popping a stack:
    print(ll.removeFirst());
    print(ll.removeFirst());
    // Treat it like a queue, pulling elements
    // off the tail end:
    print(ll.removeLast());
    print(ll);
  }
  public static void main(String[] args) {
    // Make and fill a new list each time:
    basicTest(
      new LinkedList<String>(Countries.names(25)));
    basicTest(
      new ArrayList<String>(Countries.names(25)));
    iterMotion(
      new LinkedList<String>(Countries.names(25)));
    iterMotion(
      new ArrayList<String>(Countries.names(25)));
    iterManipulation(
      new LinkedList<String>(Countries.names(25)));
    iterManipulation(
      new ArrayList<String>(Countries.names(25)));
    testVisual(
      new LinkedList<String>(Countries.names(25)));
    testLinkedList();
  }
}

6. Set和儲存順序

Set介面:繼承來自Collection介面的行為

儲存順序不同的set實現不僅具有不同的行為,而且它們對於可以在特定的set中放置元素的型別也有不同的要求:
在這裡插入圖片描述

HashSet如果沒有其他限制,應該預設選擇,因為它對速度進行了優化。

必須為雜湊儲存和樹形儲存都建立equals方法,但是hashCode只有在類被置於HashSet和LinkedHashSet時才必須。但都應該覆蓋equals方法,總是同時覆蓋hashCode方法。

6.1 SortedSet

SortedSet中的元素可以保證處於排序狀態,SortedSet介面中的下列方法提供附加的功能:

  • Comparator comparator()返回當前Set使用的Comparator;或者返回null,表示以自然方式排序。
  • Object first()返回容器中的第一個元素。
  • Object last()返回容器中的最末一個元素。
  • SortedSet subSet(fromElement, toElement)生成此Set的子集,範圍從fromElement(包含)到toElement(不包含)。
  • SortedSet headSet(toElement)生成此Set的子集,由小於toElement的元素組成
  • SortedSet tailSet(fromElement)生成此Set的子集,由大於或等於fromElement的元素組成

7. Queue

Queue介面:

  • boolean add(E e); 新增一個元素,新增失敗時會丟擲異常
  • boolean offer(E e); 新增一個元素,通過返回值判斷是否新增成功
  • E remove(); 移除一個元素,刪除失敗時會丟擲異常
  • E poll(); 移除一個元素,返回移除的元素
  • E element(); 在佇列的頭部查詢元素,如果佇列為空,丟擲異常
  • E peek(); 在佇列的頭部查詢元素,如果佇列為空,返回null
    Queue在Java SE5中僅有的兩個實現是LinkedList和PriorityQueue,它們的差異在於排序行為而不是效能

7.1 優先順序佇列

列表中的每個物件都包含一個字串和一個主要的以及次要的優先順序值,該列表的排序順序也是通過實現Comparable而進行控制的:

Queue<String> queue=new PriorityQueue<>();

7.2 雙向佇列

雙向佇列就像一個佇列,但是可以在任意一段新增或移除元素。在LinkedList中包含支援雙向佇列的方法,但在Java標準類庫中沒有任何顯式的用於雙向佇列的介面。因此,LinkedList無法去實現這樣的介面,無法像前面轉型到Queue那樣向上轉型為Deque。但是可以使用組合建立一個Deque類,並直接從LinkedList中暴露相關方法:

  • queue.addFirst(); 向佇列首部新增元素
  • queue.addList(); 向佇列尾部新增元素
  • queue.getLast(); 獲取佇列尾部元素
  • queue.getFirst(); 獲取佇列首部元素
  • queue.removeFirst(); 移除佇列首部元素
  • queue.removeLast(); 移除佇列尾元素
  • queue.size(); 返回佇列大小

8. 理解Map

Map介面:

  • size(); 返回Map大小
  • isEmpty(); 判斷Map集合是否為空
  • containsKey(); 判斷Mpa集合是否包括Key鍵
  • containsVaule(); 判斷Map集合是否包括Vaule
  • get(Object); 獲得Map集合鍵為Object的Vaule
  • put(K,V); 新增一個K,V鍵值對
  • remove(Object); 移除K鍵值對
  • putAll(Map); 新增所有元素Map集合
  • clear(); 清空Map中所有集合元素
  • keySet(); 返回鍵Key的Set集合
  • values(); 返回值Vaule的Set集合
  • entrySet(); 返回Map.entrySet集合
  • remove(Objcet,Object); 移除K,V集合
  • replace(K,V,V); 替換鍵值對

8.1 效能

get進行線性搜尋時,執行速度會相當慢,這正是HashMap提高速度的地方。HashMap使用特殊值,稱作雜湊碼,來取代對鍵的緩慢搜尋。雜湊碼是相對唯一的,用以代表物件的int值,它是通過將該物件的某些資訊進行轉換而生成的。hashCode()是跟類Object中的方法,因此所有Java物件都能產生雜湊碼。HashMap就是使用物件的hashCode進行快速查詢的,此方法能夠顯著提高效能。

在這裡插入圖片描述

8.2 SortedMap介面

使用SortedMap(TreeMap的唯一實現),可確保鍵處於排序狀態。使得它具有額外的功能,這些功能由SortedMap介面中的下列方法提供:

  • Comparator comparator():返回當前Map使用的Comparator;或者返回null,表示以自然方式排序
  • T firstKey返回Map中的第一個鍵
  • T lastKey返回Map中的最末一個鍵
  • SortedMap subMap(fromKey,toKey)生成此Map的子集,範圍由fromKey(包括)到toKey(不包含)的鍵確定
  • SortedMap headMap(toKey)生成此Map的子集,由鍵小於toKey的所有鍵值對組成
  • SortedMap tailMap(fromkey)生成此Map的子集,由鍵大於或等於fromKey的所有鍵值對組成

8.3 LinkedHashMap

為了提高速度,LinkedHashMap雜湊化所有的元素,但在遍歷鍵值對時,卻又以元素的插入順序返回鍵值對。此外,可以在構造器中設定LinkedHashMap,使之使用基於訪問的最近最少使用(LRU)演算法,於是沒有被訪問過的元素就會出現在佇列的前面。對於需要定期清理元素以節省空間的程式來說,此功能是的程式很容易實現:

ublic class LinkedHashMapDemo {
  public static void main(String[] args) {
    LinkedHashMap<Integer,String> linkedMap = new LinkedHashMap<Integer,String>(new CountingMapData(9));
    print(linkedMap);
    linkedMap = new LinkedHashMap<Integer,String>(16, 0.75f, true);
    linkedMap.putAll(new CountingMapData(9));
    print(linkedMap);
    for(int i = 0; i < 6; i++) {
      linkedMap.get(i);
    }
    print(linkedMap);
    print(linkedMap.get(0));
    print(linkedMap);
  }
} /*
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0}
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0}
{6=G0, 7=H0, 8=I0, 0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0}
A0
{6=G0, 7=H0, 8=I0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 0=A0}
*/

鍵值對是以插入順序進行遍歷的,但是,在LRU版本中,在只訪問過前六個元素後,最後三個元素移到了佇列前面。然後再次訪問元素0時,它被移到了佇列後端。

9. 雜湊與雜湊碼

對於一個放入Map集合的物件,它的類必須實現hashcode和equal方法

正確的equals必須滿足5個條件:

  1. 自反省,對任意x,x.equals(x)一定返回true
  2. 對稱性,對任意x和y,如果x.equals(y)為true,則y.equals(x)也為true
  3. 傳遞性,對任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,則x.equals(z)一定返回true
  4. 一致性,對任意x和y,如果物件中用於等價比較的資訊沒有改變,那麼無論呼叫x.equals(y)多少次,返回結果應該保持一致
  5. 對任何不是null的x,x.equals(null)一定返回false

9.1 理解hashCode()

前面只是說明雜湊的資料結構(HashSet、HashMap、LinkedHashMap和LinkedHashSet)要正確處理鍵必須覆蓋hashCode和equals方法。要很好的解決問題,必須瞭解資料結構的內部構造。

首先,雜湊的目的在於:想要使用一個物件來查詢另一個物件。不過可以使用TreeMap或者自己實現的Map達到目的。

9.2 為速度而雜湊

雜湊的價值在於速度:雜湊使得查詢得以快速進行。由於瓶頸位於鍵的查詢速度,因此解決方案之一就是保持鍵的排序狀態,然後使用Collections.binarySearch進行查詢。

雜湊則更進一步,使用陣列(儲存一組元素最快的資料結構)來表示鍵的資訊(不是鍵本身)。但陣列不能調整容量。所以陣列並不儲存鍵本身。而是通過鍵物件生成一個數字,將其作為陣列的下標。這個數字就是雜湊碼。不同的鍵可以產生相同的下標,也就是說,可能會有衝突,因此,陣列多大就不重要了,任何鍵總能在陣列中找到它的位置。

於是查詢一個值的過程首先是計算雜湊碼,然後使用雜湊碼查詢陣列。如果沒有衝突,那就有了一個完美的雜湊函式。通常,衝突由外部連結處理:陣列並不直接儲存值,而是儲存值的list。然後對list的值使用equals方法進行線性查詢(這部分較慢)。但是如果雜湊函式好的話,陣列的每個位置就只有較少的值。因此,不是查詢整個list,而是快速地跳到陣列的某個位置,只對很少的元素進行比較。這就是HashMap如此快的原因。

9.3 覆蓋hashCode()

設計hashCode()時重要因素就是:無論何時,對同一個物件呼叫hashCode()都應該生成同樣的值。此外,也不應該是hashCode()依賴於具有唯一性的物件資訊,尤其是使用this的值。因為這樣做無法生成一個新的鍵,使之與put中元素的鍵值對中的鍵相同。

String為例:程式中有多個String物件,都包含相同的字串序列,那麼這些String物件都對映到同一塊記憶體區域。所以new String(“hello”)生成的兩個例項,雖然相互獨立的,但是對它們使用hashCode()應該產生相同的結果。

如何設計一個好的hashCode方法

  1. 給int變數result賦予一個非零值常量,例如17
  2. 為物件內每一個有意義的域f(即每個可以做equals()操作的域)計算出一個int雜湊碼c:

在這裡插入圖片描述

  1. 合併計算得到的雜湊碼:result = 37 * resut + c
  2. 返回result
  3. 檢查hashCode()最後生成的結果,確保相同的物件有相同的雜湊碼
public class CountedString {
  private static List<String> created = new ArrayList<String>();
  private String s;
  private int id = 0;
  public CountedString(String str) {
    s = str;
    created.add(s);
    for(String s2 : created) {
      if(s2.equals(s)) {
        id++;
      }
    }
  }
  @Override
  public String toString() {
    return "String: " + s + " id: " + id + " hashCode(): " + hashCode();
  }
  @Override
  public int hashCode() {
    // 非常簡單的方法: 返回s.hashCode() * id
    int result = 17;
    result = 37 * result + s.hashCode();
    result = 37 * result + id;
    return result;
  }
  @Override
  public boolean equals(Object o) {
    return o instanceof CountedString &&
      s.equals(((CountedString)o).s) &&
      id == ((CountedString)o).id;
  }
  public static void main(String[] args) {
    Map<CountedString,Integer> map =
      new HashMap<CountedString,Integer>();
    CountedString[] cs = new CountedString[5];
    for(int i = 0; i < cs.length; i++) {
      cs[i] = new CountedString("hi");
      map.put(cs[i], i);
    }
    print(map);
    for(CountedString cstring : cs) {
      print("Looking up " + cstring);
      print(map.get(cstring));
    }
  }
} /*
{String: hi id: 4 hashCode(): 146450=3, String: hi id: 1 hashCode(): 146447=0, String: hi id: 3 hashCode(): 146449=2, String: hi id: 5 hashCode(): 146451=4, String: hi id: 2 hashCode(): 146448=1}
Looking up String: hi id: 1 hashCode(): 146447
0
Looking up String: hi id: 2 hashCode(): 146448
1
Looking up String: hi id: 3 hashCode(): 146449
2
Looking up String: hi id: 4 hashCode(): 146450
3
Looking up String: hi id: 5 hashCode(): 146451
4
*/

10.選擇介面的不同實現

容器之間的區別通常歸結為由什麼在背後支援它們。也就是說,所使用的介面是有什麼樣的資料結構實現的。ArrayList底層由陣列支援;而LinkedList是由雙向連結串列實現,其中每個物件包含資料的同時還包含執行連結串列的前一個與後一個元素引用。因此,如果經常在表中插入或刪除元素,LinkedList就比較合適;否則,應該使用速度更快的ArrayList。

Set中,HashSet是最常用的,查詢速度最快;LinkedHashSet保持元素插入的次序;TreeSet基於Treemap ,生成一個總是處於排序狀態的Set。

10.1對List的選擇

  1. get和set的測試使用了隨機數生成器來執行List的隨機訪問。在輸出中,對於背後有陣列支援的List和ArrayList,無論列表的大小如何變化,這些訪問速度都很快。而對於LinkedList來說,訪問時間對於較大的列表來說明顯增加。很顯然,如果你需要執行大量的隨機訪問,連結串列不是一個很好的選擇。
  2. interadd測試使用迭代器在列表中間插入元素。對於ArrayList來說,當列表變大的時候,其開銷會變得十分巨大,但是對於LinkedList來說,相對比較低廉,並且不會受著列表尺寸的變大而變大。
  3. insert和remove測試都使用了索引為5作為插入和刪除點,而沒有選擇List兩端的元素,LinkedList對於插入和刪除操作來說十分低廉,並且不會隨著列表尺寸的變大而變大。但是對於ArrayList來說,插入操作的代價特別高昂,並且隨著列表尺寸的變大而變大。

總結:
最佳的做法可能是將ArrayList做為預設首選,只要需要使用額外的功能,或者當程式的效能因為經常從表中間進行插入和刪除而變差的時候,才會選擇LinkedList。如果使用的是固定數量的元素,那麼既可以選擇使用背後有陣列支撐的List,也可以選擇真正的陣列。
CopyOnWriteArrayList是List的一個特殊實現,專門用於併發程式設計。

10.2 對Set的選擇

HashSet的效能基本上總是比TreeSet好,特別是在新增和查詢元素時。TreeSet存在的唯一原因是它可以維持元素的排序狀態;所以,只有當需要一個排好序的Set時,才應該使用TreeSet。因為其內部結構支援排序,並且因為迭代是更有可能執行的操作,所以,用TreeSet迭代通常比用HashSet更快。

對於插入操作,LinkedHashSet比HashSet的代價更高,這是由維護連結串列所帶來額外開銷造成的。

10.3 對Map的選擇

  1. 所有Map實現的插入操作都會隨著Map的尺寸的變大而明顯變慢。但是,查詢的