Java&Android 基礎知識梳理(8) 容器類

澤毛發表於2017-12-21

一、前言

Java 容器集合框架
上面這幅圖是Java集合框架涉及到的類的繼承關係,從集合類的角度來看,它分為兩個大類:CollectionMap

1.1 Collection

CollectionListSet抽象出來的介面,它包含了這些集合的基本操作。

(1) List

List介面通常表示一個列表(陣列、佇列、連結串列,棧等),其中的元素可以重複,常用的實現類為ArrayListLinkedListVector

(2) Set

Set介面通常表示一個集合,集合中的元素不允許重複(通過hashCodeequals函式保證),常用的實現類有HashSetTreeSetHashSet是通過Map中的HashMap來實現的,而TreeSet則是通過Map中的TreeMap實現的,另外TreeSet還實現了SortedSet介面,因此是有序的集合。

(3) List 和 Set 的區別

  • Set介面儲存的是無序的、不重複的資料
  • List介面儲存的是有序的、可以重複的資料
  • Set檢索效率低,刪除和插入效率高,插入和刪除不會引起元素位置改變。
  • List查詢元素效率高,刪除和插入效率低,List和陣列類似,可以動態增長,根據實際儲存的長度自動增長List的長度。

(4) 使用的設計模式

抽象類AbstractCollectionAbstractListAbstractSet分別實現了CollectionListSet介面,這就是在Java集合框架中用的很多的介面卡設計模式,用這些抽象類去實現介面,在抽象類中實現介面中的若干或全部方法,這樣下面的一些類只需直接繼承該抽象類,並實現自己需要的方法即可,而不用實現介面中的全部抽象方法。

1.2 Map

Map是一個對映介面,其中的每個元素都是一個Key-Value鍵值對,同樣抽象類AbstractMap通過介面卡模式實現了Map介面的大部分函式,TreeMapHashMapWeakHashMap等實現類都通過繼承AbstractMap來實現。

1.3 Iterator

Iterator是遍歷集合的迭代器,它可以用來遍歷Collection,但是不能用來遍歷MapCollection的實現類都實現了iterator()函式,它返回一個Iterator物件,用來遍歷集合,ListIterator則專門用來遍歷List。而Enumeration則是JDK 1.0時引入的,作用與Iterator相同,但它的功能比Iterator要少,它只能在HashtableVectorStack中使用。

1.4 Arrays 和 Collections

ArraysCollections是用來運算元組、集合的兩個工具類,例如在ArrayListVector中大量呼叫了Arrays.Copyof()方法,而Collections中有很多靜態方法可以返回各集合類的synchronized版本,即執行緒安全的版本,當然了,如果要用執行緒安全的集合類,首選concurrent併發包下的對應的集合類。

二、ArrayList

ArrayList是基於一個能動態增長的陣列實現,ArrayList並不是執行緒安全的,在多執行緒的情況下可以考慮使用Collections.synchronizedList(List T)函式返回一個執行緒安全的ArrayList類,也可以使用併發包下的CopyOnWriteArrayList類。

ArrayList<T>類繼承於AbstractList<T>,並實現了以下四個介面:

  • List<T>
  • RandomAccess:支援快速隨機訪問
  • Cloneable:能夠被克隆
  • Serializable:支援序列化

ArrayList 的擴容

由於ArrayList是基於陣列實現的,因此當我們通過addXX方法向陣列中新增元素之前,都要保證有足夠的空間容納新的元素,這一過程是通過ensureCapacityInternal來實現的,傳入的引數為所要求的陣列容量:

  • 如果當前陣列為空,並且要求的容量小於10,那麼將要求的容量設為10
  • 接著嘗試將陣列大小擴充為當前大小的2.5
  • 如果仍然無法滿足要求,那麼將陣列大小設為要求的容量
  • 如果要求的容量大於預設的整型的最大值減8,那麼呼叫hugeCapacity方法,將陣列的容量設為整型的最大值
  • 最後,呼叫Arrays.copyOf將原有陣列中的元素複製到新的陣列中。

Arrays.copyOf最終會呼叫到System.arraycopy()方法。該Native函式實際上最終呼叫了C語言的memmove()函式,因此它可以保證同一個陣列內元素的正確複製和移動,比一般的複製方法的實現效率要高很多,很適合用來批量處理陣列,Java強烈推薦在複製大量陣列元素時用該方法,以取得更高的效率。

ArrayList 轉換為靜態陣列

ArrayList中提供了兩種轉換為靜態陣列的方法:

  • Object[] toArray() 該方法有可能會丟擲java.lang.ClassCastException異常,如果直接用向下轉型的方法,將整個ArrayList集合轉變為指定型別的Array陣列,便會丟擲該異常,而如果轉化為Array陣列時不向下轉型,而是將每個元素向下轉型,則不會丟擲該異常,顯然對陣列中的元素一個個進行向下轉型,效率不高,且不太方便。
  • T[] toArray(T[] a) 該方法可以直接將ArrayList轉換得到的Array進行整體向下轉型,且從該方法的原始碼中可以看出,引數a的大小不足時,內部會呼叫Arrays.copyOf方法,該方法內部建立一個新的陣列返回,因此對該方法的常用形式如下:
public static Integer[] vectorToArray2(ArrayList<Integer> v) {    
    Integer[] newText = (Integer[])v.toArray(new Integer[0]);    
    return newText;    
}   
複製程式碼

元素訪問方式

ArrayList基於陣列實現,可以通過下標索引直接查詢到指定位置的元素,因此查詢效率高,但每次插入或刪除元素,就要大量地移動元素,插入刪除元素的效率低。

在查詢給定元素索引值等的方法中,原始碼都將該元素的值分為null和不為null兩種情況處理,ArrayList中允許元素為null

三、LinkedList

LinkedList是基於雙向迴圈連結串列實現的,除了可以當作連結串列來操作外,它還可以當作棧,佇列和雙端佇列來使用。

LinkedList同樣是非執行緒安全的,在多執行緒的情況下可以考慮使用Collections.synchronizedList(List T)函式返回一個執行緒安全的LinkedList類,LinkedList繼承於AbstractSequentialList類,同時實現了以下四個介面:

  • List<T>
  • DequeQueue:雙端佇列
  • Cloneable:支援克隆操作
  • Serializable:支援序列化

連結串列節點

LinkedList的實現是基於雙向迴圈連結串列的,且頭結點voidLink中不存放資料,所以它也不存在擴容的方法,只需改變節點的指向即可,每個連結串列節點包含該節點的資料,以及前驅和後繼節點的引用,其定義如下所示:

    private static final class Link<ET> {
        //該節點的資料。
        ET data;
        //前驅節點和後繼節點。
        Link<ET> previous, next;
        Link(ET o, Link<ET> p, Link<ET> n) {
            data = o;
            previous = p;
            next = n;
        }
    }
複製程式碼

查詢和刪除操作

當需要根據位置尋找對應節點的資料時,會先比較待查詢位置和連結串列的大小,如果小於一半,那麼從頭節點的後繼節點開始向後尋找,反之則從頭結點的前驅節點開始往前尋找,因此對於查詢操作來說,它的效率很低,但是向頭尾節點插入和刪除資料的效率較高。

四、Vector

Vector也是基於陣列實現的,其容量能夠動態增長。它的許多實現方法都加入了同步語句,因此是 執行緒安全 的。

Vector繼承於AbstractList類,並且實現了下面四個介面:

  • List<E>
  • RandomAccess:支援隨機訪問
  • Cloneable, java.io.Serializable:支援Clone和序列化。

Vector的實現大體和ArrayList類似,它有以下幾個特點:

  • Vector有四個不同的構造方法,無參構造方法的容量為預設值10,僅包含容量的構造方法則將容量增長量置為0
  • Vector的容量不足以容納新的元素時,將進行擴容操作。首先判斷容量增長值是否為0,如果為0,那麼就將新容量設為舊容量的兩倍,否則就設定新容量為舊容量加上容量增長值。假如新容量還不夠,那麼就直接設定新量容量為傳入的引數。
  • 在存入和讀取元素時,會根據元素值是否為null進行處理,也就是說,Vector允許元素為null

五、HashSet

HashSet具有以下特點:

  • 不能保證元素的排列順序,順序有可能發生變化
  • 不是同步的
  • 集合元素可以是null,但只能放入一個null

當向HashSet集合中存入一個元素時,HashSet會呼叫該物件的hashCode()方法來得到該物件的hashCode值,然後根據hashCode值來決定該物件在HashSet中儲存位置。 簡單的說,HashSet集合判斷兩個元素相等的標準是兩個物件通過equals方法比較相等,並且兩個物件的hashCode()方法返回值相等。

注意,如果要把一個物件放入HashSet中,重寫該物件對應類的equals方法,也應該重寫其hashCode()方法。其規則是如果兩個物件通過equals方法比較返回true時,其hashCode也應該相同。另外,物件中用作equals比較標準的屬性,都應該用來計算hashCode的值。

六、TreeSet

TreeSetSortedSet介面的唯一實現類,TreeSet可以確保集合元素處於排序狀態。TreeSet支援兩種排序方式,自然排序定製排序,其中自然排序為預設的排序方式。

TreeSet中加入的應該是同一個類的物件。TreeSet判斷兩個物件不相等的方式是兩個物件通過equals方法返回false,或者通過CompareTo方法比較沒有返回0

自然排序

自然排序使用要排序元素的CompareTo(Object obj)方法來比較元素之間大小關係,然後將元素按照升序排列。 Java提供了一個Comparable介面,該介面裡定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現了該介面的物件就可以比較大小。

obj1.compareTo(obj2)方法如果返回0,則說明被比較的兩個物件相等,如果返回一個正數,則表明obj1大於obj2,如果是負數,則表明obj1小於obj2。如果我們將兩個物件的equals方法總是返回true,則這兩個物件的compareTo方法返回應該返回0.

定製排序

自然排序是根據集合元素的大小,以升序排列,如果要定製排序,應該使用Comparator介面,實現int compare(T o1,T o2)方法。

  • TreeSet是二叉樹實現的,Treeset中的資料是自動排好序的,不允許放入null值。
  • HashSet是雜湊表實現的,HashSet中的資料是無序的,可以放入null,但只能放入一個null,兩者中的值都不能重複,就如資料庫中唯一約束。
  • HashSet要求放入的物件必須實現hashCode()方法,放入的物件,是以hashcode()碼作為標識的,而具有相同內容的String物件,hashcode是一樣,所以放入的內容不能重複。但是同一個類的物件可以放入不同的例項 。

七、HashMap

HashMap是基於雜湊表實現的,每一個元素都是一個key-value對,其內部通過單連結串列解決衝突問題,容量不足時,同樣會自動增長。HashMap是非執行緒安全的,只是用於單執行緒環境下,多執行緒環境下可以採用併發包下的ConcurrentHashMap

HashMap繼承於AbstractMap,同時實現了CloneableSerializable介面,因此,它支援克隆和序列化。

HashMap 的整體結構

HashMap是基於陣列和連結串列來實現的:

Java&Android 基礎知識梳理(8)   容器類
它的基本原理為:

  • 首先根據KeyhashCode方法,計算出在陣列中的座標。
//計算 Key 的 hash 值。
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//根據 Key 的 hash 值和連結串列的長度來計算下標。
int i = indexFor(hash, table.length);
複製程式碼
  • 判斷在陣列的當前位置是否已經有元素,如果沒有,那麼就將Key/Value封裝成HashMapEntry資料結構,並將其作為陣列在該位置上的元素。否則就先從頭節點開始遍歷該連結串列,如果 滿足下面的兩個條件,那麼就替換連結串列該節點的Value
//Value 替換的條件
//條件1:hash 值完全相同
//條件2:key 指向同一塊記憶體地址 或者 key 的 equals 方法返回為 true
(e.hash == hash && ((k = e.key) == key || key.equals(k)))
複製程式碼
  • 遍歷完整個連結串列都沒有找到可替代的節點,那麼將這個新的HashMapEntry作為連結串列的頭節點,並且也是陣列在該位置上的元素,原先的頭節點則作為它的後繼節點。

HashMapEntry 的資料結構

HashMapEntry的定義如下:

static class HashMapEntry<K,V> implements Map.Entry<K,V> {
        //Key
        final K key;
        //Value
        V value;
        //後繼節點。
        HashMapEntry<K,V> next;
        //如果 Key 不為 null ,那麼就是它的雜湊值,否則為0。
        int hash;
        //....
}
複製程式碼

元素寫入

在第一小節中,我們簡要的計算了HashMap的整體結構,由此我們可以推斷出在設計的時候應當儘可能地使元素均勻分佈,使得陣列每個位置上的連結串列儘可能地短,避免從連結串列頭結點開始遍歷的過程。

而元素是否分佈均勻就取決於根據KeyHash值計算陣列下標的過程,首先我們看一下Hash值的計算,這裡首先呼叫物件的hashCode方法,再通過二次Hash演算法獲得一個Hash值:

    public static int secondaryHash(Object key) {
        return secondaryHash(key.hashCode());
    }

    private static int secondaryHash(int h) {
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }
複製程式碼

之後,再通過這個計算出來Hash與上當前陣列長度減一 進行取餘,獲得對應的陣列下標:

hash & (tab.length - 1)
複製程式碼

由於HashMap在擴容的時候,保證了陣列的長度適中為2n冪,因此length - 1的二進位制表示始終為全1,因此進行&操作的結果既保證了最終的結果不會超過陣列的長度範圍,同時也保證了兩個Hash值相同的元素不會對映到陣列的同一位置,再加上上面二次Hash的過程加上了高位的計算優化,從而使得資料的分佈儘可能地平均。

元素讀取

理解了上面儲存的過程,讀取自然也就很好理解了,其實通過Key計算陣列下標,遍歷該位置上陣列元素的連結串列進行查詢的過程。

擴容

HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因為陣列的長度是固定的,所以為了提高查詢的效率,就要對HashMap的陣列進行擴容。

HashMap中的元素個數超過陣列大小 * loadFactor時,loadFactor的預設值為0.75,就會進行陣列擴容,擴容後的大小為原先的2倍,然後重新計算每個元素在陣列中的位置,原陣列中的資料必須重新計算其在新陣列中的位置,並放進去。

擴容是一個相當耗費效能的操作,因此如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。

Fail-Fast 機制

HashMap並不是執行緒安全的,因此如果在使用迭代器的過程中有其他執行緒修改了map,那麼將丟擲ConcurrentModificationException,這就是所謂fail-fast策略。

這一策略在原始碼中的實現是通過modCount域,modCount顧名思義就是修改次數,對HashMap內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的expectedModCount

在迭代過程中,判斷modCountexpectedModCount是否相等,如果不相等就表示已經有其他執行緒修改了Map,那麼就會通過下面的方法丟擲異常:

    HashMapEntry<K, V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
           //省略...
    }
複製程式碼

modCount宣告為volatile,保證了多執行緒情況下的記憶體可見性。

在迭代器建立之後,如果從結構上對對映進行修改,除非通過迭代器本身的remove方法,其他任何時間任何方式的修改,迭代器都將丟擲ConcurrentModificationException。因此,面對併發的修改,迭代器很快就會完全失敗,而不保證在將來不確定的時間發生任意不確定行為的風險

八、HashTable

HashTable經常用來和HashMap進行比較,前者是執行緒安全的,而後者則不是,其實HashTable要比HashMap出現得要早,它實現執行緒安全的原理並沒有什麼高階的地方,只不過是在寫入和讀取時加上了synchronized關鍵字用於同步,並且也不推薦使用了,因為在併發包中提供了更好的解決方案ConcurrentHashMap,它內部的實現比較複雜,之後我們再通過一篇文章進行分析。

這裡簡單地總結一下它和HashMap之間的區別:

  • HashTable基於Dictionary類,而HashMap是基於AbstractMapDictionary是任何可將鍵對映到相應值的類的抽象父類,而AbstractMap基於 Map介面的實現,它以最大限度地減少實現此介面所需的工作。
  • HashMapkeyvalue都允許為null,而Hashtablekeyvalue都不允許為nullHashMap遇到keynull的時候,呼叫putForNullKey方法進行處理,而對value沒有處理,Hashtable遇到null,直接返回 NullPointerException
  • Hashtable方法是同步,而HashMap則不是。我們可以看一下原始碼,Hashtable中的幾乎所有的public的方法都是synchronized的,而有些方法也是在內部通過synchronized程式碼塊來實現。所以有人一般都建議如果是涉及到多執行緒同步時採用HashTable,沒有涉及就採用HashMap,但是在 Collections類中存在一個靜態方法:synchronizedMap(),該方法建立了一個執行緒安全的Map物件,並把它作為一個封裝的物件來返回。

九、TreeMap

TreeMap是一個有序的key-value集合,它是通過 紅黑樹 實現的。TreeMap繼承於AbstractMap,所以它是一個Map,即一個key-value集合。TreeMap實現了NavigableMap介面,意味著它支援一系列的導航方法,比如返回有序的key集合。TreeMap實現了CloneableSerializable介面,意味著它可以被Clone和序列化。

TreeMap基於紅黑樹實現,該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator進行排序,具體取決於使用的構造方法。TreeMap的基本操作containsKeygetputremove的時間複雜度是log(n) ,另外,TreeMap是非同步的, 它的iterator方法返回的迭代器是Fail-Fastl的。

十、LinkedHashMap

  • LinkedHashMapHashMap的子類,與HashMap有著同樣的儲存結構,但它加入了一個雙向連結串列的頭結點,將所有putLinkedHashmap的節點一一串成了一個雙向迴圈連結串列,因此它保留了節點插入的順序,可以使節點的輸出順序與輸入順序相同。
  • LinkedHashMap可以用來實現LRU演算法。
  • LinkedHashMap同樣是非執行緒安全的,只在單執行緒環境下使用。

十一、LinkedHashSet

LinkedHashSet是具有可預知迭代順序的Set介面的雜湊表和連結列表實現。此實現與HashSet的不同之處在於,後者維護著一個執行於所有條目的雙重連結列表。此連結列表定義了迭代順序,該迭代順序可為插入順序或是訪問順序。

LinkedHashSet的實現:對於LinkedHashSet而言,它繼承與HashSet、又基於LinkedHashMap來實現的。

相關文章