1. 程式人生 > 實用技巧 >容器深入研究

容器深入研究

2019獨角獸企業重金招聘Python工程師標準>>> hot3.png

1、完整的容器分類法

  • 集合類庫更加完備的圖,包括抽象類和遺留構件(不包括Queue的實現):

    150209_puTe_570654.png

  • Java SE5新新增:

    Queue介面及其實現PriorityQueue和各種風格的BlockingQueue。

    ConcurrentMap介面及其實現ConcurrentHashMap,它們是用於多執行緒機制的。

    CopyOnWriteArrayList和CopyOnWriteArraySet,它們也是用於多執行緒機制的。

    EnumSet和EnumMap,為使用enum而設計的Set和Map的特殊實現。

    在Collections類中的多個便利方法。

  • 虛線框表示abstract類,它們只是部分實現了特定介面的工具。如果你想建立自己的容器,並不用從介面開始並實現其中的全部方法,只需從abstract繼承,然後執行一些建立新類必須的工作。事實上容器類庫包含足夠多的功能,通常可以忽略以Abstract開頭的類。

2、Collection的功能方法

  • 下圖列出可以通過Collection執行的所有操作(不包括從Object繼承而來的方法)。因此,它們也是可通過Set或List執行的所有操作(List還有額外的功能)。Map不是繼承自Collection的,另行介紹。

    153555_dKui_570654.jpg

3、可選操作

  • 執行各種不同的新增和移除的方法在Collection介面中都是可選操作。

  • 將方法定義為可選的原因:防止在設計中出現介面爆炸的情況。

  • 未獲支援的操作(這種方式)是一種特例,可以延遲到需要時再實現。為了讓這種方式能夠工作:

    第一,UnsupportedOperationException必須是一種罕見的事件。

    第二,一個操作是未獲支援的,那麼實現介面時可能就會導致UnsupportedOperationException異常。

  • 未獲支援的操作只有在執行時才能探測到,因此它們表示動態型別檢查。

  • Arrays.asList()返回固定尺寸的List,其中的值還是可以修改的;而Collections.unmodifiableList()產生不可修改列表,任何情況下都是不可修改的。

4、List的功能方法

  • 大多數時候只是呼叫add()新增物件,使用get()一次取出一個元素,以及呼叫iterator()獲取用於該序列的Iterator。

5、Set和儲存順序

  • 當你建立自己的型別時,要意識到Set需要一種方式來維護儲存順序,而儲存順序維護方式在Set不同實現之間會有所變化。

  • 不同的Set實現不僅具有不同的行為,而且它們對於可在特定的Set中放置的元素的型別也有不同要求:

    175542_XXJB_570654.png

注:在HashSet上打星號表示,如沒有其他限制,這就是預設選擇,它對速度進行了優化。

  • SortedSet中的元素可以保證處於排序狀態。

    Comparator comparator():返回當前Set使用的Comparator;或者返回null,表示以自然方式排序。

185330_8xq4_570654.png

6、佇列

  • 除了併發應用,Queue在Java SE5中僅有的兩個實現是LinkedList和PriorityQueue,它們的差異在於排序行為而不是效能。

  • 優先順序佇列

  • 雙向佇列:可以在任何一端新增或移除元素,在LinkedList中包含支援雙向佇列的方法,但在Java標準類庫中沒有任何顯式的用於雙向佇列的介面。由於不太可能在兩端都放入元素並抽取它們,因此雙向佇列不如Queue那樣常用。

7、理解Map

  • 標準Java類庫中包含了Map的幾種基本實現,包括:HashMap、TreeMap、LinkedHashMap、WeakHashMap、ConcurrentHashMap、IdentityHashMap。它們都有同樣的基本介面Map,但是行為特性各不相同,這表現在效率、鍵值對的儲存及呈現次序、物件的儲存週期、對映表如何在多執行緒程式中工作和判定“鍵”等價的策略等方面。

  • 效能是對映表中的一個重要問題。HashMap使用特殊的值,稱作雜湊碼,來取代對鍵的緩慢搜尋。雜湊碼是“相對唯一”的、用以代表物件的int值,它是通過將物件的某些資訊進行轉換而生成的。hashCode()是根類Object中的方法,因此所有接Java物件都能產生雜湊碼。HashMap就是使用物件的hashCode()進行快速查詢的,此方法能夠顯著提高效能。下圖是基本的Map實現,在HashMap上打星號表示如沒有其他限制,它應該成為你的預設選擇,因為它對速度進行了優化。其他實現強調了其他的特性,因此都不如HashMap快。

    005853_c5Ew_570654.png

  • 任何鍵都必須具有一個equals()方法;如果鍵被用於雜湊Map,那麼它必須還具有恰當的hashCode()方法;如果被用於TreeMap,那麼它還必須實現Comparable。

  • 使用SortedMap(TreeMap是其現階段的唯一實現),可以確保鍵處於排序狀態。SortedMap介面中提供的方法:

    011138_qVGt_570654.png

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

8、雜湊與雜湊碼

  • Object的hashCode()方法生成雜湊碼,它預設是使用物件的地址計算雜湊碼。

  • 正確的equals()方法必須滿足下列5個條件:

    123151_u21x_570654.png

    注:預設的Object.equals()只是比較物件的地址。

  • 如果要使用自己的類作為HashMap的鍵,必須同時過載hashCode()和equals()。

  • 理解hashCode(),使用雜湊的目的在於:想要使用一個物件來查詢另一個物件。

  • 為速度而雜湊。雜湊的價值在於速度:雜湊使得查詢得以快速進行。由於瓶頸位於鍵的查詢速度,因此解決方案之一就是保持鍵的排序狀態,然後使用Collections.binarySearch()進行查詢。雜湊則更進一步,它將鍵儲存在某處,以便能夠很快找到。儲存一組元素最快的資料結構是陣列,所以使用它來表示鍵的資訊。陣列不能調整容量,因此陣列並不儲存鍵本身。而是通過鍵物件生成一個數字,將其作為陣列的下標,這個數字就是鍵物件的雜湊碼,由定義在Object中的且可能由你的類覆蓋的hashCode()方法生成。不同的鍵可以產生相同的下標,可能會有衝突。查詢一個值的過程首先就是計算雜湊碼,然後使用雜湊碼查詢陣列。如果能保證沒有衝突(值的數量是固定的),那可就有了一個完美的雜湊函式,但這種情況是特例。通常,衝突由外部連結處理:陣列並不直接儲存值,而是儲存值的List。然後對List中的值使用equals()方法進行線性的查詢,這部分查詢會比較慢,但如果雜湊函式好的話,陣列每個位置就只有較少的值。因此,不是查詢整個List,而是快速地跳到陣列的某個位置,只對很少的元素進行比較。這便是HashMap會如此快的原因。

  • 覆蓋hashCode()。你無法控制陣列的下標值的產生,這個值依賴於具體的HashMap物件的容量,而容量的改變與容器的充滿程度和負載因子有關。設計hashCode()時最重要的因素是:無論何時,對同一個物件呼叫hashCode()都應該生成同樣的值。此外,也不應該使hashCode()依賴於具有唯一性的物件資訊。

  • 下圖是Joshua Bloch給出的一份像樣的hashCode()基本指導:

    152733_gTJY_570654.png

9、選擇介面的不同實現

  • 儘管實際上只有四種容器:Map、List、Set和Queue,但是每種介面都有不止一個實現版本,每種不同的實現都有各自的特徵、優點和缺點。

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

  • Set可被實現為TreeSet、HashSet或LinkedHashSet。每一種都有不同的行為:HashSet最常用,查詢速度最快;LinkedHashSet保持元素插入的次序;TreeSet基於TreeMap,生成一個總是處於排序狀態的Set。

  • 對List的選擇:

    154427_TJvG_570654.png

    注:避免使用Vector,最佳做法是將ArrayList作為預設選擇,需要額外功能時酌情選擇。另外CopyOnWriteArrayList是List的一個特殊實現,專門用於併發程式設計。

  • 對Set的選擇

    155043_u8ai_570654.png

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

  • 對Map的選擇

    155903_9ikL_570654.png

    IdentityHashMap具有完全不同的效能,因為它是使用==而不是equals()來比較元素的。

  • HashMap的效能因子:

    容量:表中的桶位數。

    初始容量:表在建立時所擁有的桶位數。HashMap和HashSet都具有允許指定初始容量的構造器。

    尺寸:表中當前儲存的項數。

    負載因子:尺寸/容量。空表的負載因子是0,半滿表的負載因子是0.5,依次類推。負載輕的表產生衝突的可能性小,因此對於插入和查詢都是最理想的。HashMap和HashSet都具有允許指定負載因子的構造器,表示當負載情況達到該負載因子的水平時,容器將自動增加器容量(桶位數),實現方式是使容量大致加倍,並重新將現有物件分佈到新的桶位數中(這被稱為再雜湊)。HashMap預設的負載因子是0.75,這個因子在時間和空間代價之間達到了平衡。如果你知道將要在HashMap中儲存多少項,那麼建立一個具有恰當大小的初始容量將可以避免自動再雜湊的開銷。

10、實用方法

  • 大量用於容器的方法被表示為java.util.Collections類內部的靜態方法。之前看過例如addAll()、reverseOrder()和binarySearch()之類。下面還將介紹另外一部分(synchronized和unmodifiable的實用方法)

    163815_Eaxm_570654.jpg

    注:min()和max()只能作用於Collection物件,而不能作用於List。

  • 設定Collection或Map為不可修改。unmodifiableXXX(),此方法有大量變種,對應於Collection、List、Set和Map。

  • Collection或Map的同步控制。synchronizedXXX(),此方法有大量變種,對應於Collection、List、Set和Map。使用此類方法可能產生ConcurrentModificationException異常(多個程序同時修改同一個容器內容),ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet都使用了可避免ConcurrentModificationException的技術。

11、持有引用

  • java.lang.ref類庫包含了一組類,這些類為垃圾回收提供了更大靈活性。當存在可能會耗盡記憶體的大物件的時候,這些類顯得特別有用。有三個繼承自抽象類Reference的類:SoftReference、WeakReference和PhantomReference。

  • 物件是可獲得的是指此物件可在程式中某處找到。如果一個物件不是“可獲得的”,那麼你的程式將無法使用到它,所以回收是安全的。

  • 如果想繼續持有對某個物件的引用,希望以後還能夠訪問到該物件,但也希望能夠允許垃圾回收器釋放該物件,這時就應該使用Reference物件。這樣,你可以繼續使用該物件,而在記憶體消耗殆盡時又允許釋放該物件。

  • SoftReference、WeakReference和PhantomReference由強到弱排列,對應不同級別的“可獲得性”。SoftReference用以實現記憶體敏感的快取記憶體。WeakReference是為實現“規範對映”而設計的,它不妨礙垃圾回收器回收對映的“鍵”(或“值”),“規範對映”中的物件例項可以在程式的多處被同時使用,以節省儲存空間。PhantomReference用以排程回收前的清理工作,它比Java終止機制更靈活。

  • 使用SoftReference和WeakReference時,可以選擇是否要將它們放入ReferenceQueue(用作“回收前清理工作”的工具),而PhantomReference只能依賴於ReferenceQueue。

  • WeakHashMap是容器類中一種特殊的Map,它用來儲存WeakReference,使得規範對映更易於使用。在這種對映中,每個值只儲存一份例項以節省空間。WeakHashMap允許垃圾回收器自動清理鍵和值,允許清理元素的觸發條件是:不再需要此鍵了。

12、Java 1.0/1.1的容器

  • 本人只討論BitSet,如果想要高效率地儲存大量“開/關”資訊,BitSet是很好的選擇。不過它的效率僅是對空間而言;如果需要高效的訪問時間,BitSet比本地陣列稍慢一些。BitSet的最小容量是long:64位,如果儲存的內容小,那麼BitSet就浪費了很多空間。BitSet在必要時會進行擴充容量。由於EnumSet允許你按照名字而不是數字位的位置進行操作,可以減少錯誤,因此EnumSet與BitSet相比,通常是一種更好的選擇。使用BitSet而不是EnumSet的理由只包括:只有執行時才知道需要多少個標誌;對標誌命名不合理;需要BitSet中的某種特殊操作。

轉載於:https://my.oschina.net/90liusq/blog/350514