JavaAndroid 基础知识梳理(8) 容器类
一、前言
上面這幅圖是Java集合框架涉及到的類的繼承關系,從集合類的角度來看,它分為兩個大類:Collection和Map。1.1 Collection
Collection是List和Set抽象出來的接口,它包含了這些集合的基本操作。
(1) List
List接口通常表示一個列表(數組、隊列、鏈表,棧等),其中的元素可以重復,常用的實現類為ArrayList、LinkedList和Vector。
(2) Set
Set接口通常表示一個集合,集合中的元素不允許重復(通過hashCode和equals函數保證),常用的實現類有HashSet和TreeSet,HashSet是通過Map中的HashMap來實現的,而TreeSet則是通過Map中的TreeMap實現的,另外TreeSet還實現了SortedSet接口,因此是有序的集合。
(3) List 和 Set 的區別
- Set接口存儲的是無序的、不重復的數據
- List接口存儲的是有序的、可以重復的數據
- Set檢索效率低,刪除和插入效率高,插入和刪除不會引起元素位置改變。
- List查找元素效率高,刪除和插入效率低,List和數組類似,可以動態增長,根據實際存儲的長度自動增長List的長度。
(4) 使用的設計模式
抽象類AbstractCollection、AbstractList和AbstractSet分別實現了Collection、List和Set接口,這就是在Java集合框架中用的很多的適配器設計模式,用這些抽象類去實現接口,在抽象類中實現接口中的若干或全部方法,這樣下面的一些類只需直接繼承該抽象類,并實現自己需要的方法即可,而不用實現接口中的全部抽象方法。
1.2 Map
Map是一個映射接口,其中的每個元素都是一個Key-Value鍵值對,同樣抽象類AbstractMap通過適配器模式實現了Map接口的大部分函數,TreeMap、HashMap和WeakHashMap等實現類都通過繼承AbstractMap來實現。
1.3 Iterator
Iterator是遍歷集合的迭代器,它可以用來遍歷Collection,但是不能用來遍歷Map。Collection的實現類都實現了iterator()函數,它返回一個Iterator對象,用來遍歷集合,ListIterator則專門用來遍歷List。而Enumeration則是JDK 1.0時引入的,作用與Iterator相同,但它的功能比Iterator要少,它只能在Hashtable、Vector和Stack中使用。
1.4 Arrays 和 Collections
Arrays和Collections是用來操作數組、集合的兩個工具類,例如在ArrayList和Vector中大量調用了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方法,該方法內部創建一個新的數組返回,因此對該方法的常用形式如下:
元素訪問方式
ArrayList基于數組實現,可以通過下標索引直接查找到指定位置的元素,因此查找效率高,但每次插入或刪除元素,就要大量地移動元素,插入刪除元素的效率低。
在查找給定元素索引值等的方法中,源碼都將該元素的值分為null和不為null兩種情況處理,ArrayList中允許元素為null。
三、LinkedList
LinkedList是基于雙向循環鏈表實現的,除了可以當作鏈表來操作外,它還可以當作棧,隊列和雙端隊列來使用。
LinkedList同樣是非線程安全的,在多線程的情況下可以考慮使用Collections.synchronizedList(List T)函數返回一個線程安全的LinkedList類,LinkedList繼承于AbstractSequentialList類,同時實現了以下四個接口:
- List<T>
- Deque和Queue:雙端隊列
- 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
TreeSet是SortedSet接口的唯一實現類,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,同時實現了Cloneable和Serializable接口,因此,它支持克隆和序列化。
HashMap 的整體結構
HashMap是基于數組和鏈表來實現的:
它的基本原理為:- 首先根據Key的hashCode方法,計算出在數組中的坐標。
- 判斷在數組的當前位置是否已經有元素,如果沒有,那么就將Key/Value封裝成HashMapEntry數據結構,并將其作為數組在該位置上的元素。否則就先從頭節點開始遍歷該鏈表,如果 滿足下面的兩個條件,那么就替換鏈表該節點的Value
- 遍歷完整個鏈表都沒有找到可替代的節點,那么將這個新的HashMapEntry作為鏈表的頭節點,并且也是數組在該位置上的元素,原先的頭節點則作為它的后繼節點。
HashMapEntry 的數據結構
HashMapEntry的定義如下:
static class HashMapEntry<K,V> implements Map.Entry<K,V> {//Keyfinal K key;//ValueV value;//后繼節點。HashMapEntry<K,V> next;//如果 Key 不為 null ,那么就是它的哈希值,否則為0。int hash;//.... } 復制代碼元素寫入
在第一小節中,我們簡要的計算了HashMap的整體結構,由此我們可以推斷出在設計的時候應當盡可能地使元素均勻分布,使得數組每個位置上的鏈表盡可能地短,避免從鏈表頭結點開始遍歷的過程。
而元素是否分布均勻就取決于根據Key的Hash值計算數組下標的過程,首先我們看一下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在擴容的時候,保證了數組的長度適中為2的n冪,因此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。
在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他線程修改了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是基于AbstractMap。Dictionary是任何可將鍵映射到相應值的類的抽象父類,而AbstractMap基于 Map接口的實現,它以最大限度地減少實現此接口所需的工作。
- HashMap的key和value都允許為null,而Hashtable的key和value都不允許為null。HashMap遇到key為null的時候,調用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實現了Cloneable和Serializable接口,意味著它可以被Clone和序列化。
TreeMap基于紅黑樹實現,該映射根據其鍵的自然順序進行排序,或者根據創建映射時提供的 Comparator進行排序,具體取決于使用的構造方法。TreeMap的基本操作containsKey、get、put和remove的時間復雜度是log(n) ,另外,TreeMap是非同步的, 它的iterator方法返回的迭代器是Fail-Fastl的。
十、LinkedHashMap
- LinkedHashMap是HashMap的子類,與HashMap有著同樣的存儲結構,但它加入了一個雙向鏈表的頭結點,將所有put到LinkedHashmap的節點一一串成了一個雙向循環鏈表,因此它保留了節點插入的順序,可以使節點的輸出順序與輸入順序相同。
- LinkedHashMap可以用來實現LRU算法。
- LinkedHashMap同樣是非線程安全的,只在單線程環境下使用。
十一、LinkedHashSet
LinkedHashSet是具有可預知迭代順序的Set接口的哈希表和鏈接列表實現。此實現與HashSet的不同之處在于,后者維護著一個運行于所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可為插入順序或是訪問順序。
LinkedHashSet的實現:對于LinkedHashSet而言,它繼承與HashSet、又基于LinkedHashMap來實現的。
總結
以上是生活随笔為你收集整理的JavaAndroid 基础知识梳理(8) 容器类的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 地理坐标系_GCS汇总
- 下一篇: 【BUG解决】 RuntimeError