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

1、前言

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

1.1 Collection

CollectionListSet抽象出來的接口,它包含了這些集合的基本操做。html

(1) List

List接口一般表示一個列表(數組、隊列、鏈表,棧等),其中的元素能夠重複,經常使用的實現類爲ArrayListLinkedListVectorjava

(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併發包下的對應的集合類。安全

2、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

3、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;
        }
    }
複製代碼

查找和刪除操做

當須要根據位置尋找對應節點的數據時,會先比較待查找位置和鏈表的大小,若是小於一半,那麼從頭節點的後繼節點開始向後尋找,反之則從頭結點的前驅節點開始往前尋找,所以對於查找操做來講,它的效率很低,可是向頭尾節點插入和刪除數據的效率較高。

4、Vector

Vector也是基於數組實現的,其容量可以動態增加。它的許多實現方法都加入了同步語句,所以是 線程安全 的。

Vector繼承於AbstractList類,而且實現了下面四個接口:

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

Vector的實現大致和ArrayList相似,它有如下幾個特色:

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

5、HashSet

HashSet具備如下特色:

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

當向HashSet集合中存入一個元素時,HashSet會調用該對象的hashCode()方法來獲得該對象的hashCode值,而後根據hashCode值來決定該對象在HashSet中存儲位置。 簡單的說,HashSet集合判斷兩個元素相等的標準是兩個對象經過equals方法比較相等,而且兩個對象的hashCode()方法返回值相等。

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

6、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是同樣,因此放入的內容不能重複。可是同一個類的對象能夠放入不一樣的實例 。

7、HashMap

HashMap是基於哈希表實現的,每個元素都是一個key-value對,其內部經過單鏈表解決衝突問題,容量不足時,一樣會自動增加。HashMap是非線程安全的,只是用於單線程環境下,多線程環境下能夠採用併發包下的ConcurrentHashMap

HashMap繼承於AbstractMap,同時實現了CloneableSerializable接口,所以,它支持克隆和序列化。

HashMap 的總體結構

HashMap是基於數組和鏈表來實現的:

它的基本原理爲:

  • 首先根據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。所以,面對併發的修改,迭代器很快就會徹底失敗,而不保證在未來不肯定的時間發生任意不肯定行爲的風險

8、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對象,並把它做爲一個封裝的對象來返回。

9、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的。

10、LinkedHashMap

  • LinkedHashMapHashMap的子類,與HashMap有着一樣的存儲結構,但它加入了一個雙向鏈表的頭結點,將全部putLinkedHashmap的節點一一串成了一個雙向循環鏈表,所以它保留了節點插入的順序,可使節點的輸出順序與輸入順序相同。
  • LinkedHashMap能夠用來實現LRU算法。
  • LinkedHashMap一樣是非線程安全的,只在單線程環境下使用。

11、LinkedHashSet

LinkedHashSet是具備可預知迭代順序的Set接口的哈希表和連接列表實現。此實現與HashSet的不一樣之處在於,後者維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序可爲插入順序或是訪問順序。

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

相關文章
相關標籤/搜索