HashMap 是基於哈希表的 Map 接口的非同步實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。html
此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操做(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的「容量」(桶的數量)及其大小(鍵-值映射關係數)成比例。因此,若是迭代性能很重要,則不要將初始容量設置得過高或將加載因子設置得過低。也許你們開始對這段話有一點不太懂,不過不用擔憂,當你讀完這篇文章後,就能深切理解這其中的含義了。java
須要注意的是:Hashmap 不是同步的,若是多個線程同時訪問一個 HashMap,而其中至少一個線程從結構上(指添加或者刪除一個或多個映射關係的任何操做)修改了,則必須保持外部同步,以防止對映射進行意外的非同步訪問。node
在 Java 編程語言中,最基本的結構就是兩種,一個是數組,另一個是指針(引用),HashMap 就是經過這兩個數據結構進行實現。HashMap其實是一個「鏈表散列」的數據結構,即數組和鏈表的結合體。git
從上圖中能夠看出,HashMap 底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個 HashMap 的時候,就會初始化一個數組。github
咱們經過 JDK 中的 HashMap 源碼進行一些學習,首先看一下構造函數:面試
咱們着重看一下第 18 行代碼table = new Entry[capacity];
。這不就是 Java 中數組的建立方式嗎?也就是說在構造函數中,其建立了一個 Entry 的數組,其大小爲 capacity(目前咱們還不須要太瞭解該變量含義),那麼 Entry 又是什麼結構呢?看一下源碼:算法
咱們目前仍是隻着重核心的部分,Entry 是一個 static class,其中包含了 key 和 value,也就是鍵值對,另外還包含了一個 next 的 Entry 指針。咱們能夠總結出:Entry 就是數組中的元素,每一個 Entry 其實就是一個 key-value 對,它持有一個指向下一個元素的引用,這就構成了鏈表。編程
咱們看一下方法的標準註釋:在註釋中首先提到了,當咱們 put 的時候,若是 key 存在了,那麼新的 value 會代替舊的 value,而且若是 key 存在的狀況下,該方法返回的是舊的 value,若是 key 不存在,那麼返回 null。數組
從上面的源代碼中能夠看出:當咱們往 HashMap 中 put 元素的時候,先根據 key 的 hashCode 從新計算 hash 值,根據 hash 值獲得這個元素在數組中的位置(即下標),若是數組該位置上已經存放有其餘元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。若是數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。緩存
addEntry(hash, key, value, i)方法根據計算出的 hash 值,將 key-value 對放在數組 table 的 i 索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼以下:
當系統決定存儲 HashMap 中的 key-value 對時,徹底沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每一個 Entry 的存儲位置。咱們徹底能夠把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置以後,value 隨之保存在那裏便可。
hash(int h)方法根據 key 的 hashCode 從新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,形成的 hash 衝突。
咱們能夠看到在 HashMap 中要找到某個元素,須要根據 key 的 hash 值來求得對應數組中的位置。如何計算這個位置就是 hash 算法。前面說過 HashMap 的數據結構是數組和鏈表的結合,因此咱們固然但願這個 HashMap 裏面的 元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,那麼當咱們用 hash 算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表,這樣就大大優化了查詢的效率。
對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 hash 碼值老是相同的。咱們首先想到的就是把 hash 值對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的。可是,「模」運算的消耗仍是比較大的,在 HashMap 中是這樣作的:調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。indexFor(int h, int length) 方法的代碼以下:
這個方法很是巧妙,它經過 h & (table.length -1) 來獲得該對象的保存位,而 HashMap 底層數組的長度老是 2 的 n 次方,這是 HashMap 在速度上的優化。在 HashMap 構造器中有以下代碼:
這段代碼保證初始化時 HashMap 的容量老是 2 的 n 次方,即底層數組的長度老是爲 2 的 n 次方。
當 length 老是 2 的 n 次方時,h& (length-1)運算等價於對 length 取模,也就是 h%length,可是 & 比 % 具備更高的效率。這看上去很簡單,其實比較有玄機的,咱們舉個例子來講明:
假設數組長度分別爲 15 和 16,優化後的 hash 碼分別爲 8 和 9,那麼 & 運算後的結果以下:
h & (table.length-1) | hash | table.length-1 | ||
---|---|---|---|---|
8 & (15-1): | 0100 | & | 1110 | = 0100 |
9 & (15-1): | 0101 | & | 1110 | = 0100 |
8 & (16-1): | 0100 | & | 1111 | = 0100 |
9 & (16-1): | 0101 | & | 1111 | = 0101 |
從上面的例子中能夠看出:當它們和 15-1(1110)「與」的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8 和 9 會被放到數組中的同一個位置上造成鏈表,那麼查詢的時候就須要遍歷這個鏈 表,獲得8或者9,這樣就下降了查詢的效率。同時,咱們也能夠發現,當數組長度爲 15 的時候,hash 值會與 15-1(1110)進行「與」,那麼最後一位永遠是 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費至關大,更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!而當數組長度爲16時,即爲2的n次方時,2n-1 獲得的二進制數的每一個位上的值都爲 1,這使得在低位上&時,獲得的和原 hash 的低位相同,加之 hash(int h)方法對 key 的 hashCode 的進一步優化,加入了高位計算,就使得只有相同的 hash 值的兩個值纔會被放到數組中的同一個位置上造成鏈表。
因此說,當數組長度爲 2 的 n 次冪的時候,不一樣的 key 算得得 index 相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
根據上面 put 方法的源代碼能夠看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。
有了上面存儲時的 hash 算法做爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中能夠看出:從 HashMap 中 get 元素時,首先計算 key 的 hashCode,找到數組中對應位置的某一元素,而後經過 key 的 equals 方法在對應位置的鏈表中找到須要的元素。
簡單地說,HashMap 在底層將 key-value 當成一個總體進行處理,這個總體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存全部的 key-value 對,當須要存儲一個 Entry 對象時,會根據 hash 算法來決定其在數組中的存儲位置,在根據 equals 方法決定其在該數組位置上的鏈表中的存儲位置;當須要取出一個Entry 時,也會根據 hash 算法找到其在數組中的存儲位置,再根據 equals 方法從該位置上的鏈表中取出該Entry。
當 HashMap 中的元素愈來愈多的時候,hash 衝突的概率也就愈來愈高,由於數組的長度是固定的。因此爲了提升查詢的效率,就要對 HashMap 的數組進行擴容,數組擴容這個操做也會出如今 ArrayList 中,這是一個經常使用的操做,而在 HashMap 數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這就是 resize。
那麼 HashMap 何時進行擴容呢?當 HashMap 中的元素個數超過數組大小 *loadFactor
時,就會進行數組擴容,loadFactor的默認值爲 0.75,這是一個折中的取值。也就是說,默認狀況下,數組大小爲 16,那麼當 HashMap 中元素個數超過 16*0.75=12
的時候,就把數組的大小擴展爲 2*16=32
,即擴大一倍,而後從新計算每一個元素在數組中的位置,而這是一個很是消耗性能的操做,因此若是咱們已經預知 HashMap 中元素的個數,那麼預設元素的個數可以有效的提升 HashMap 的性能。
HashMap 包含以下幾個構造器:
HashMap 的基礎構造器 HashMap(int initialCapacity, float loadFactor) 帶有兩個參數,它們是初始容量 initialCapacity 和負載因子 loadFactor。
負載因子 loadFactor 衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來講,查找一個元素的平均時間是 O(1+a),所以若是負載因子越大,對空間的利用更充分,然然後果是查找效率的下降;若是負載因子過小,那麼散列表的數據將過於稀疏,對空間形成嚴重浪費。
HashMap 的實現中,經過 threshold 字段來判斷 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
結合負載因子的定義公式可知,threshold 就是在此 loadFactor 和 capacity 對應下容許的最大元素數目,超過這個數目就從新 resize,以下降實際的負載因子。默認的的負載因子 0.75 是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize 後的 HashMap 容量是容量的兩倍:
咱們知道 java.util.HashMap 不是線程安全的,所以若是在使用迭代器的過程當中有其餘線程修改了 map,那麼將拋出 ConcurrentModificationException,這就是所謂 fail-fast 策略。
ail-fast 機制是 java 集合(Collection)中的一種錯誤機制。 當多個線程對同一個集合的內容進行操做時,就可能會產生 fail-fast 事件。
例如:當某一個線程 A 經過 iterator去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變了;那麼線程 A 訪問集合時,就會拋出 ConcurrentModificationException 異常,產生 fail-fast 事件。
這一策略在源碼中的實現是經過 modCount 域,modCount 顧名思義就是修改次數,對 HashMap 內容(固然不只僅是 HashMap 纔會有,其餘例如 ArrayList 也會)的修改都將增長這個值(你們能夠再回頭看一下其源碼,在不少操做中都有 modCount++ 這句),那麼在迭代器初始化過程當中會將這個值賦給迭代器的 expectedModCount。
在迭代過程當中,判斷 modCount 跟 expectedModCount 是否相等,若是不相等就表示已經有其餘線程修改了 Map:
注意到 modCount 聲明爲 volatile,保證線程之間修改的可見性。
在 HashMap 的 API 中指出:
由全部 HashMap 類的「collection 視圖方法」所返回的迭代器都是快速失敗的:在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自己的 remove 方法,其餘任什麼時候間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。所以,面對併發的修改,迭代器很快就會徹底失敗,而不冒在未來不肯定的時間發生任意不肯定行爲的風險。
注意,迭代器的快速失敗行爲不能獲得保證,通常來講,存在非同步的併發修改時,不可能做出任何堅定的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。所以,編寫依賴於此異常的程序的作法是錯誤的,正確作法是:迭代器的快速失敗行爲應該僅用於檢測程序錯誤。
在上文中也提到,fail-fast 機制,是一種錯誤檢測機制。它只能被用來檢測錯誤,由於 JDK 並不保證 fail-fast 機制必定會發生。若在多線程環境下使用 fail-fast 機制的集合,建議使用「java.util.concurrent 包下的類」去取代「java.util 包下的類」。
效率高,之後必定要使用此種方式!
效率低,之後儘可能少使用!
對於 HashSet 而言,它是基於 HashMap 實現的,底層採用 HashMap 來保存元素,因此若是對 HashMap 比較熟悉了,那麼學習 HashSet 也是很輕鬆的。
咱們先經過 HashSet 最簡單的構造函數和幾個成員變量來看一下,證實我們上邊說的,其底層是 HashMap:
其實在英文註釋中已經說的比較明確了。首先有一個HashMap的成員變量,咱們在 HashSet 的構造函數中將其初始化,默認狀況下采用的是 initial capacity爲16,load factor 爲 0.75。
對於 HashSet 而言,它是基於 HashMap 實現的,HashSet 底層使用 HashMap 來保存全部元素,所以 HashSet 的實現比較簡單,相關 HashSet 的操做,基本上都是直接調用底層 HashMap 的相關方法來完成,咱們應該爲保存到 HashSet 中的對象覆蓋 hashCode() 和 equals()
若是此 set 中還沒有包含指定元素,則添加指定元素。更確切地講,若是此 set 沒有包含知足(e==null ? e2==null : e.equals(e2)) 的元素 e2,則向此 set 添加指定的元素 e。若是此 set 已包含該元素,則該調用不更改 set 並返回 false。但底層實際將將該元素做爲 key 放入 HashMap。思考一下爲何?
因爲 HashMap 的 put() 方法添加 key-value 對時,當新放入 HashMap 的 Entry 中 key 與集合中原有 Entry 的 key 相同(hashCode()返回值相等,經過 equals 比較也返回 true),新添加的 Entry 的 value 會將覆蓋原來 Entry 的 value(HashSet 中的 value 都是PRESENT
),但 key 不會有任何改變,所以若是向 HashSet 中添加一個已經存在的元素時,新添加的集合元素將不會被放入 HashMap中,原來的元素也不會有任何改變,這也就知足了 Set 中元素不重複的特性。
該方法若是添加的是在 HashSet 中不存在的,則返回 true;若是添加的元素已經存在,返回 false。其緣由在於咱們以前提到的關於 HashMap 的 put 方法。該方法在添加 key 不重複的鍵值對的時候,會返回 null。
和 HashMap 同樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對。
Hashtable 在 Java 中的定義爲:
從源碼中,咱們能夠看出,Hashtable 繼承於 Dictionary 類,實現了 Map, Cloneable, java.io.Serializable接口。其中Dictionary類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類,每一個鍵和值都是對象(源碼註釋爲:The Dictionary
class is the abstract parent of any class, such as Hashtable
, which maps keys to values. Every key and every value is an object.)。但在這一點我開始有點懷疑,由於我查看了HashMap以及TreeMap的源碼,都沒有繼承於這個類。不過當我看到註釋中的解釋也就明白了,其 Dictionary 源碼註釋是這樣的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. 該話指出 Dictionary 這個類過期了,新的實現類應該實現Map接口。
Hashtable是經過"拉鍊法"實現的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。
關於變量的解釋在源碼註釋中都有,最好仍是應該看英文註釋。
Hashtable 一共提供了 4 個構造方法:
public Hashtable(int initialCapacity, float loadFactor)
: 用指定初始容量和指定加載因子構造一個新的空哈希表。useAltHashing 爲 boolean,其若是爲真,則執行另外一散列的字符串鍵,以減小因爲弱哈希計算致使的哈希衝突的發生。public Hashtable(int initialCapacity)
:用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。public Hashtable()
:默認構造函數,容量爲 11,加載因子爲 0.75。public Hashtable(Map<? extends K, ? extends V> t)
:構造一個與給定的 Map 具備相同映射關係的新哈希表。put 方法的整個流程爲:
我在下面的代碼中也進行了一些註釋:
經過一個實際的例子來演示一下這個過程:
假設咱們如今Hashtable的容量爲5,已經存在了(5,5),(13,13),(16,16),(17,17),(21,21)這 5 個鍵值對,目前他們在Hashtable中的位置以下:
如今,咱們插入一個新的鍵值對,put(16,22),假設key=16的索引爲1.但如今索引1的位置有兩個Entry了,因此程序會對鏈表進行迭代。迭代的過程當中,發現其中有一個Entry的key和咱們要插入的鍵值對的key相同,因此如今會作的工做就是將newValue=22替換oldValue=16,而後返回oldValue=16.
而後咱們如今再插入一個,put(33,33),key=33的索引爲3,而且在鏈表中也不存在key=33的Entry,因此將該節點插入鏈表的第一個位置。
相比較於 put 方法,get 方法則簡單不少。其過程就是首先經過 hash()方法求得 key 的哈希值,而後根據 hash 值獲得 index 索引(上述兩步所用的算法與 put 方法都相同)。而後迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。
Hashtable 有多種遍歷方式:
HashMap 是無序的,HashMap 在 put 的時候是根據 key 的 hashcode 進行 hash 而後放入對應的地方。因此在按照必定順序 put 進 HashMap 中,而後遍歷出 HashMap 的順序跟 put 的順序不一樣(除非在 put 的時候 key 已經按照 hashcode 排序號了,這種概率很是小)
JAVA 在 JDK1.4 之後提供了 LinkedHashMap 來幫助咱們實現了有序的 HashMap!
LinkedHashMap 是 HashMap 的一個子類,它保留插入的順序,若是須要輸出的順序和輸入時的相同,那麼就選用 LinkedHashMap。
LinkedHashMap 是 Map 接口的哈希表和連接列表實現,具備可預知的迭代順序。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
LinkedHashMap 實現與 HashMap 的不一樣之處在於,LinkedHashMap 維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序能夠是插入順序或者是訪問順序。
注意,此實現不是同步的。若是多個線程同時訪問連接的哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。
根據鏈表中元素的順序能夠分爲:按插入順序的鏈表,和按訪問順序(調用 get 方法)的鏈表。默認是按插入順序排序,若是指定按訪問順序排序,那麼調用get方法後,會將此次訪問的元素移至鏈表尾部,不斷訪問能夠造成按訪問順序排序的鏈表。
我在最開始學習 LinkedHashMap 的時候,看到訪問順序、插入順序等等,有點暈了,隨着後續的學習才慢慢懂得其中原理,因此我會先在進行作幾個 demo 來演示一下 LinkedHashMap 的使用。看懂了其效果,而後再來研究其原理。
看下面這個代碼:
一個比較簡單的測試 HashMap 的代碼,經過控制檯的輸出,咱們能夠看到 HashMap 是沒有順序的。
咱們如今將 map 的實現換成 LinkedHashMap,其餘代碼不變:Map<String, String> map = new LinkedHashMap<String, String>();
看一下控制檯的輸出:
咱們能夠看到,其輸出順序是完成按照插入順序的!也就是咱們上面所說的保留了插入的順序。咱們不是在上面還提到過其能夠按照訪問順序進行排序麼?好的,咱們仍是經過一個例子來驗證一下:
代碼與以前的都差很少,但咱們多了兩行代碼,而且初始化 LinkedHashMap 的時候,用的構造函數也不相同,看一下控制檯的輸出結果:
這也就是咱們以前提到過的,LinkedHashMap 能夠選擇按照訪問順序進行排序。
對於 LinkedHashMap 而言,它繼承與 HashMap(public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
)、底層使用哈希表與雙向鏈表來保存全部元素。其基本操做與父類 HashMap 類似,它經過重寫父類相關的方法,來實現本身的連接列表特性。下面咱們來分析 LinkedHashMap 的源代碼:
LinkedHashMap 採用的 hash 算法和 HashMap 相同,可是它從新定義了數組中保存的元素 Entry,該 Entry 除了保存當前對象的引用外,還保存了其上一個元素 before 和下一個元素 after 的引用,從而在哈希表的基礎上又構成了雙向連接列表。看源代碼:
LinkedHashMap 中的 Entry 集成與 HashMap 的 Entry,可是其增長了 before 和 after 的引用,指的是上一個元素和下一個元素的引用。
經過源代碼能夠看出,在 LinkedHashMap 的構造方法中,實際調用了父類 HashMap 的相關構造方法來構造一個底層存放的 table 數組,但額外能夠增長 accessOrder 這個參數,若是不設置,默認爲 false,表明按照插入順序進行迭代;固然能夠顯式設置爲 true,表明以訪問順序進行迭代。如:
咱們已經知道 LinkedHashMap 的 Entry 元素繼承 HashMap 的 Entry,提供了雙向鏈表的功能。在上述 HashMap 的構造器中,最後會調用 init() 方法,進行相關的初始化,這個方法在 HashMap 的實現中並沒有意義,只是提供給子類實現相關的初始化調用。
但在 LinkedHashMap 重寫了 init() 方法,在調用父類的構造方法完成構造後,進一步實現了對其元素 Entry 的初始化操做。
LinkedHashMap 並未重寫父類 HashMap 的 put 方法,而是重寫了父類 HashMap 的 put 方法調用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了本身特有的雙向連接列表的實現。咱們在以前的文章中已經講解了HashMap的put方法,咱們在這裏從新貼一下 HashMap 的 put 方法的源代碼:
HashMap.put:
重寫方法:
LinkedHashMap 重寫了父類 HashMap 的 get 方法,實際在調用父類 getEntry() 方法取得查找的元素後,再判斷當排序模式 accessOrder 爲 true 時,記錄訪問順序,將最新訪問的元素添加到雙向鏈表的表頭,並從原來的位置刪除。因爲的鏈表的增長、刪除操做是常量級的,故並不會帶來性能的損失。
LinkedHashMap 定義了排序模式 accessOrder,該屬性爲 boolean 型變量,對於訪問順序,爲 true;對於插入順序,則爲 false。通常狀況下,沒必要指定排序模式,其迭代順序即爲默認爲插入順序。
這些構造方法都會默認指定排序模式爲插入順序。若是你想構造一個 LinkedHashMap,並打算按從近期訪問最少到近期訪問最多的順序(即訪問順序)來保存元素,那麼請使用下面的構造方法構造 LinkedHashMap:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
該哈希映射的迭代順序就是最後訪問其條目的順序,這種映射很適合構建 LRU 緩存。LinkedHashMap 提供了 removeEldestEntry(Map.Entry<K,V> eldest) 方法。該方法能夠提供在每次添加新條目時移除最舊條目的實現程序,默認返回 false,這樣,此映射的行爲將相似於正常映射,即永遠不能移除最舊的元素。
咱們會在後面的文章中詳細介紹關於如何用 LinkedHashMap 構建 LRU 緩存。
其實 LinkedHashMap 幾乎和 HashMap 同樣:從技術上來講,不一樣的是它定義了一個 Entry<K,V> header,這個 header 不是放在 Table 裏,它是額外獨立出來的。LinkedHashMap 經過繼承 hashMap 中的 Entry<K,V>,並添加兩個屬性 Entry<K,V> before,after,和 header 結合起來組成一個雙向鏈表,來實現按插入順序或訪問順序排序。
在寫關於 LinkedHashMap 的過程當中,記起來以前面試的過程當中遇到的一個問題,也是問我 Map 的哪一種實現能夠作到按照插入順序進行迭代?當時腦子是忽然短路的,但如今想一想,也只能怪本身對這個知識點仍是掌握的不夠紮實,因此又從頭認真的把代碼看了一遍。
不過,個人建議是,你們首先首先須要記住的是:LinkedHashMap 可以作到按照插入順序或者訪問順序進行迭代,這樣在咱們之後的開發中遇到類似的問題,才能想到用 LinkedHashMap 來解決,不然就算對其內部結構很是瞭解,不去使用也是沒有什麼用的。
思考了很久,到底要不要總結 LinkedHashSet 的內容 = = 我在以前的博文中,分別寫了 HashMap 和 HashSet,而後咱們能夠看到 HashSet 的方法基本上都是基於 HashMap 來實現的,說白了,HashSet內部的數據結構就是一個 HashMap,其方法的內部幾乎就是在調用 HashMap 的方法。
LinkedHashSet 首先咱們須要知道的是它是一個 Set 的實現,因此它其中存的確定不是鍵值對,而是值。此實現與 HashSet 的不一樣之處在於,LinkedHashSet 維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序可爲插入順序或是訪問順序。
看到上面的介紹,是否是感受其與 HashMap 和 LinkedHashMap 的關係很像?
注意,此實現不是同步的。若是多個線程同時訪問連接的哈希Set,而其中至少一個線程修改了該 Set,則它必須保持外部同步。
在LinkedHashMap的實現原理中,經過例子演示了 HashMap 和 LinkedHashMap 的區別。觸類旁通,咱們如今學習的LinkedHashSet與以前的很相同,只不過以前存的是鍵值對,而如今存的只有值。
因此我就再也不具體的貼代碼在這邊了,但咱們能夠確定的是,LinkedHashSet 是能夠按照插入順序或者訪問順序進行迭代。
對於 LinkedHashSet 而言,它繼承與 HashSet、又基於 LinkedHashMap 來實現的。
LinkedHashSet 底層使用 LinkedHashMap 來保存全部元素,它繼承與 HashSet,其全部的方法操做上又與 HashSet 相同,所以 LinkedHashSet 的實現上很是簡單,只提供了四個構造方法,並經過傳遞一個標識參數,調用父類的構造器,底層構造一個 LinkedHashMap 來實現,在相關操做上與父類 HashSet 的操做相同,直接調用父類 HashSet 的方法便可。LinkedHashSet 的源代碼以下:
以上幾乎就是 LinkedHashSet 的所有代碼了,那麼讀者可能就會懷疑了,不是說 LinkedHashSet 是基於 LinkedHashMap 實現的嗎?那我爲何在源碼中甚至都沒有看到出現過 LinkedHashMap。不要着急,咱們能夠看到在 LinkedHashSet 的構造方法中,其調用了父類的構造方法。咱們能夠進去看一下:
在父類 HashSet 中,專爲 LinkedHashSet 提供的構造方法以下,該方法爲包訪問權限,並未對外公開。
由上述源代碼可見,LinkedHashSet 經過繼承 HashSet,底層使用 LinkedHashMap,以很簡單明瞭的方式來實現了其自身的全部功能。
以上就是關於 LinkedHashSet 的內容,咱們只是從概述上以及構造方法這幾個方面介紹了,並非咱們不想去深刻其讀取或者寫入方法,而是其自己沒有實現,只是繼承於父類 HashSet 的方法。
因此咱們須要注意的點是:
ArrayList 能夠理解爲動態數組,用 MSDN 中的說法,就是 Array 的複雜版本。與 Java 中的數組相比,它的容量能動態增加。ArrayList 是 List 接口的可變數組的實現。實現了全部可選列表操做,並容許包括 null 在內的全部元素。除了實現 List 接口外,此類還提供一些方法來操做內部用來存儲列表的數組的大小。(此類大體上等同於 Vector 類,除了此類是不一樣步的。)
每一個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它老是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增加。自動增加會帶來數據向新數組的從新拷貝,所以,若是可預知數據量的多少,可在構造 ArrayList 時指定其容量。在添加大量元素前,應用程序也可使用 ensureCapacity 操做來增長 ArrayList 實例的容量,這能夠減小遞增式再分配的數量。
注意,此實現不是同步的。若是多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步。(結構上的修改是指任何添加或刪除一個或多個元素的操做,或者顯式調整底層數組的大小;僅僅設置元素的值不是結構上的修改。)
咱們先學習瞭解其內部的實現原理,才能更好的理解其應用。
對於 ArrayList 而言,它實現 List 接口、底層使用數組保存全部元素。其操做基本上是對數組的操做。下面咱們來分析 ArrayList 的源代碼:
ArrayList 繼承了 AbstractList,實現了 List。它是一個數組隊列,提供了相關的添加、刪除、修改、遍歷等功能。
ArrayList 實現了 RandmoAccess 接口,即提供了隨機訪問功能。RandmoAccess 是 java 中用來被 List 實現,爲 List 提供快速訪問功能的。在 ArrayList 中,咱們便可以經過元素的序號快速獲取元素對象;這就是快速隨機訪問。
ArrayList 實現了 Cloneable 接口,即覆蓋了函數 clone(),能被克隆。 ArrayList 實現 java.io.Serializable 接口,這意味着 ArrayList 支持序列化,能經過序列化去傳輸。
ArrayList 提供了三種方式的構造器:
public ArrayList()
能夠構造一個默認初始容量爲10的空列表;public ArrayList(int initialCapacity)
構造一個指定初始容量的空列表;public ArrayList(Collection<? extends E> c)
構造一個包含指定 collection 的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。ArrayList 中提供了多種添加元素的方法,下面將一一進行講解:
1.set(int index, E element):該方法首先調用rangeCheck(index)
來校驗 index 變量是否超出數組範圍,超出則拋出異常。然後,取出原 index 位置的值,而且將新的 element 放入 Index 位置,返回 oldValue。
2.add(E e):該方法是將指定的元素添加到列表的尾部。當容量不足時,會調用 grow 方法增加容量。
3.add(int index, E element):在 index 位置插入 element。
4.addAll(Collection<? extends E> c)
和 addAll(int index, Collection<? extends E> c)
:將特定 Collection 中的元素添加到 Arraylist 末尾。
在 ArrayList 的存儲方法,其核心本質是在數組的某個位置將元素添加進入。但其中又會涉及到關於數組容量不夠而增加等因素。
這個方法就比較簡單了,ArrayList 可以支持隨機訪問的緣由也是很顯然的,由於它內部的數據結構是數組,而數組自己就是支持隨機訪問。該方法首先會判斷輸入的index值是否越界,而後將數組的 index 位置的元素返回便可。
ArrayList 提供了根據下標或者指定對象兩種方式的刪除功能。須要注意的是該方法的返回值並不相同,以下:
注意:從數組中移除元素的操做,也會致使被移除的元素之後的全部元素的向左移動一個位置。
從上面介紹的向 ArrayList 中存儲元素的代碼中,咱們看到,每當向數組中添加元素時,都要去檢查添加後元素的個數是否會超出當前數組的長度,若是超出,數組將會進行擴容,以知足添加數據的需求。數組擴容有兩個方法,其中開發者能夠經過一個 public 的方法ensureCapacity(int minCapacity)
來增長 ArrayList 的容量,而在存儲元素等操做過程當中,若是遇到容量不足,會調用priavte方法private void ensureCapacityInternal(int minCapacity)
實現。
從上述代碼中能夠看出,數組進行擴容時,會將老數組中的元素從新拷貝一份到新的數組中,每次數組容量的增加大約是其原容量的 1.5 倍(從int newCapacity = oldCapacity + (oldCapacity >> 1)
這行代碼得出)。這種操做的代價是很高的,所以在實際使用時,咱們應該儘可能避免數組容量的擴張。當咱們可預知要保存的元素的多少時,要在構造 ArrayList 實例時,就指定其容量,以免數組擴容的發生。或者根據實際需求,經過調用ensureCapacity 方法來手動增長 ArrayList 實例的容量。
ArrayList 也採用了快速失敗的機制,經過記錄 modCount 參數來實現。在面對併發的修改時,迭代器很快就會徹底失敗,而不是冒着在未來某個不肯定時間發生任意不肯定行爲的風險。 關於 Fail-Fast 的更詳細的介紹,我在以前將 HashMap 中已經提到。
LinkedList 和 ArrayList 同樣,都實現了 List 接口,但其內部的數據結構有本質的不一樣。LinkedList 是基於鏈表實現的(經過名字也能區分開來),因此它的插入和刪除操做比 ArrayList 更加高效。但也是因爲其爲基於鏈表的,因此隨機訪問的效率要比 ArrayList 差。
看一下 LinkedList 的類的定義:
LinkedList 繼承自 AbstractSequenceList,實現了 List、Deque、Cloneable、java.io.Serializable 接口。AbstractSequenceList 提供了List接口骨幹性的實現以減小實現 List 接口的複雜度,Deque 接口定義了雙端隊列的操做。
在 LinkedList 中除了自己本身的方法外,還提供了一些可使其做爲棧、隊列或者雙端隊列的方法。這些方法可能彼此之間只是名字不一樣,以使得這些名字在特定的環境中顯得更加合適。
LinkedList 也是 fail-fast 的(前邊提過不少次了)。
LinkedList 是基於鏈表結構實現,因此在類中包含了 first 和 last 兩個指針(Node)。Node 中包含了上一個節點和下一個節點的引用,這樣就構成了雙向的鏈表。每一個 Node 只能知道本身的前一個節點和後一個節點,但對於鏈表來講,這已經足夠了。
該方法是在鏈表的 end 添加元素,其調用了本身的方法 linkLast(E e)。
該方法首先將 last 的 Node 引用指向了一個新的 Node(l),而後根據l新建了一個 newNode,其中的元素就爲要添加的 e;然後,咱們讓 last 指向了 newNode。接下來是自身進行維護該鏈表。
該方法是在指定 index 位置插入元素。若是 index 位置正好等於 size,則調用 linkLast(element) 將其插入末尾;不然調用 linkBefore(element, node(index))方法進行插入。該方法的實如今下面,你們能夠本身仔細的分析一下。(分析鏈表的時候最好可以邊畫圖邊分析)
LinkedList 的方法實在是太多,在這無法一一舉例分析。但不少方法其實都只是在調用別的方法而已,因此建議你們將其幾個最核心的添加的方法搞懂就能夠了,好比 linkBefore、linkLast。其本質也就是鏈表之間的刪除添加等。
咱們在以前的博文中瞭解到關於 HashMap 和 Hashtable 這兩種集合。其中 HashMap 是非線程安全的,當咱們只有一個線程在使用 HashMap 的時候,天然不會有問題,但若是涉及到多個線程,而且有讀有寫的過程當中,HashMap 就不能知足咱們的須要了(fail-fast)。在不考慮性能問題的時候,咱們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap),這兩種方式基本都是對整個 hash 表結構作鎖定操做的,這樣在鎖表的期間,別的線程就須要等待了,無疑性能不高。
因此咱們在本文中學習一個 util.concurrent 包的重要成員,ConcurrentHashMap。
ConcurrentHashMap 的實現是依賴於 Java 內存模型,因此咱們在瞭解 ConcurrentHashMap 的前提是必須瞭解Java 內存模型。但 Java 內存模型並非本文的重點,因此我假設讀者已經對 Java 內存模型有所瞭解。
ConcurrentHashMap 的結構是比較複雜的,都深究去本質,其實也就是數組和鏈表而已。咱們由淺入深慢慢的分析其結構。
先簡單分析一下,ConcurrentHashMap 的成員變量中,包含了一個 Segment 的數組(final Segment<K,V>[] segments;
),而 Segment 是 ConcurrentHashMap 的內部類,而後在 Segment 這個類中,包含了一個 HashEntry 的數組(transient volatile HashEntry<K,V>[] table;
)。而 HashEntry 也是 ConcurrentHashMap 的內部類。HashEntry 中,包含了 key 和 value 以及 next 指針(相似於 HashMap 中 Entry),因此 HashEntry 能夠構成一個鏈表。
因此通俗的講,ConcurrentHashMap 數據結構爲一個 Segment 數組,Segment 的數據結構爲 HashEntry 的數組,而 HashEntry 存的是咱們的鍵值對,能夠構成鏈表。
首先,咱們看一下 HashEntry 類。
HashEntry 用來封裝散列映射表中的鍵值對。在 HashEntry 類中,key,hash 和 next 域都被聲明爲 final 型,value 域被聲明爲 volatile 型。其類的定義爲:
HashEntry 的學習能夠類比着 HashMap 中的 Entry。咱們的存儲鍵值對的過程當中,散列的時候若是發生「碰撞」,將採用「分離鏈表法」來處理碰撞:把碰撞的 HashEntry 對象連接成一個鏈表。
以下圖,咱們在一個空桶中插入 A、B、C 兩個 HashEntry 對象後的結構圖(其實應該爲鍵值對,在這進行了簡化以方便更容易理解):
Segment 的類定義爲static final class Segment<K,V> extends ReentrantLock implements Serializable
。其繼承於 ReentrantLock 類,從而使得 Segment 對象能夠充當鎖的角色。Segment 中包含HashEntry 的數組,其能夠守護其包含的若干個桶(HashEntry的數組)。Segment 在某些意義上有點相似於 HashMap了,都是包含了一個數組,而數組中的元素能夠是一個鏈表。
table:table 是由 HashEntry 對象組成的數組若是散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式連接成一個鏈表table數組的數組成員表明散列映射表的一個桶每一個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分若是併發級別爲 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16。
count 變量是計算器,表示每一個 Segment 對象管理的 table 數組(若干個 HashEntry 的鏈表)包含的HashEntry 對象的個數。之因此在每一個Segment對象中包含一個 count 計數器,而不在 ConcurrentHashMap 中使用全局的計數器,是爲了不出現「熱點域」而影響併發性。
咱們經過下圖來展現一下插入 ABC 三個節點後,Segment 的示意圖:
其實從我我的角度來講,Segment結構是與HashMap很像的。
ConcurrentHashMap 的結構中包含的 Segment 的數組,在默認的併發級別會建立包含 16 個 Segment 對象的數組。經過咱們上面的知識,咱們知道每一個 Segment 又包含若干個散列表的桶,每一個桶是由 HashEntry 連接起來的一個鏈表。若是 key 可以均勻散列,每一個 Segment 大約守護整個散列表桶總數的 1/16。
下面咱們還有經過一個圖來演示一下 ConcurrentHashMap 的結構:
在 ConcurrentHashMap 中,當執行 put 方法的時候,會須要加鎖來完成。咱們經過代碼來解釋一下具體過程: 當咱們 new 一個 ConcurrentHashMap 對象,而且執行put操做的時候,首先會執行 ConcurrentHashMap 類中的 put 方法,該方法源碼爲:
咱們經過註釋能夠了解到,ConcurrentHashMap 不容許空值。該方法首先有一個 Segment 的引用 s,而後會經過 hash() 方法對 key 進行計算,獲得哈希值;繼而經過調用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進行存儲操做。該方法源碼爲:
關於該方法的某些關鍵步驟,在源碼上加上了註釋。
須要注意的是:加鎖操做是針對的 hash 值對應的某個 Segment,而不是整個 ConcurrentHashMap。由於 put 操做只是在這個 Segment 中完成,因此並不須要對整個 ConcurrentHashMap 加鎖。因此,此時,其餘的線程也能夠對另外的 Segment 進行 put 操做,由於雖然該 Segment 被鎖住了,但其餘的 Segment 並無加鎖。同時,讀線程並不會由於本線程的加鎖而阻塞。
正是由於其內部的結構以及機制,因此 ConcurrentHashMap 在併發訪問的性能上要比Hashtable和同步包裝以後的HashMap的性能提升不少。在理想狀態下,ConcurrentHashMap 能夠支持 16 個線程執行併發寫操做(若是併發級別設置爲 16),及任意數量線程的讀操做。
在實際的應用中,散列表通常的應用場景是:除了少數插入操做和刪除操做外,絕大多數都是讀取操做,並且讀操做在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操做作了大量的優化。經過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操做不須要加鎖就能夠正確得到值。這個特性使得 ConcurrentHashMap 的併發性能在分離鎖的基礎上又有了近一步的提升。
ConcurrentHashMap 是一個併發散列映射表的實現,它容許徹底併發的讀取,而且支持給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不一樣線程間的併發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全併發訪問,但同時也致使對容器的訪問變成串行化的了。
ConcurrentHashMap 的高併發性主要來自於三個方面:
使用分離鎖,減少了請求 同一個鎖的頻率。
經過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操做大多數時候不須要加鎖就能成功獲取到須要的值。因爲散列映射表在實際應用中大多數操做都是成功的 讀操做,因此 2 和 3 既能夠減小請求同一個鎖的頻率,也能夠有效減小持有鎖的時間。經過減少請求同一個鎖的頻率和儘可能減小持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提升。
咱們平時總會有一個電話本記錄全部朋友的電話,可是,若是有朋友常常聯繫,那些朋友的電話號碼不用翻電話本咱們也能記住,可是,若是長時間沒有聯繫了,要再次聯繫那位朋友的時候,咱們又不得不求助電話本,可是,經過電話本查找仍是很費時間的。可是,咱們大腦可以記住的東西是必定的,咱們只能記住本身最熟悉的,而長時間不熟悉的天然就忘記了。
其實,計算機也用到了一樣的一個概念,咱們用緩存來存放之前讀取的數據,而不是直接丟掉,這樣,再次讀取的時候,能夠直接在緩存裏面取,而不用再從新查找一遍,這樣系統的反應能力會有很大提升。可是,當咱們讀取的個數特別大的時候,咱們不可能把全部已經讀取的數據都放在緩存裏,畢竟內存大小是必定的,咱們通常把最近常讀取的放在緩存裏(至關於咱們把最近聯繫的朋友的姓名和電話放在大腦裏同樣)。
LRU 緩存利用了這樣的一種思想。LRU 是 Least Recently Used 的縮寫,翻譯過來就是「最近最少使用」,也就是說,LRU 緩存把最近最少使用的數據移除,讓給最新讀取的數據。而每每最常讀取的,也是讀取次數最多的,因此,利用 LRU 緩存,咱們可以提升系統的 performance。
要實現 LRU 緩存,咱們首先要用到一個類 LinkedHashMap。
用這個類有兩大好處:一是它自己已經實現了按照訪問順序的存儲,也就是說,最近讀取的會放在最前面,最最不常讀取的會放在最後(固然,它也能夠實現按照插入順序存儲)。第二,LinkedHashMap 自己有一個方法用於判斷是否須要移除最不常讀取的數,可是,原始方法默認不須要移除(這是,LinkedHashMap 至關於一個linkedlist),因此,咱們須要 override 這樣一個方法,使得當緩存裏存放的數據個數超過規定個數後,就把最不經常使用的移除掉。關於 LinkedHashMap 中已經有詳細的介紹。
代碼以下:(可直接複製,也能夠經過LRUcache-Java下載)
HashMap 和 HashSet 都是 collection 框架的一部分,它們讓咱們可以使用對象的集合。collection 框架有本身的接口和實現,主要分爲 Set 接口,List 接口和 Queue 接口。它們有各自的特色,Set 的集合裏不容許對象有重複的值,List 容許有重複,它對集合中的對象進行索引,Queue 的工做原理是 FCFS 算法(First Come, First Serve)。
首先讓咱們來看看什麼是 HashMap 和 HashSet,而後再來比較它們之間的分別。
HashSet 實現了 Set 接口,它不容許集合中有重複的值,當咱們提到 HashSet 時,第一件事情就是在將對象存儲在 HashSet 以前,要先確保對象重寫 equals()和 hashCode()方法,這樣才能比較對象的值是否相等,以確保set中沒有儲存相等的對象。若是咱們沒有重寫這兩個方法,將會使用這個方法的默認實現。
public boolean add(Obje
HashMap 是基於哈希表的 Map 接口的非同步實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操做(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的「容量」(桶的數量)及其大小(鍵-值映射關係數)成比例。因此,若是迭代性能很重要,則不要將初始容量設置得過高或將加載因子設置得過低。也許你們開始對這段話有一點不太懂,不過不用擔憂,當你讀完這篇文章後,就能深切理解這其中的含義了。
須要注意的是:Hashmap 不是同步的,若是多個線程同時訪問一個 HashMap,而其中至少一個線程從結構上(指添加或者刪除一個或多個映射關係的任何操做)修改了,則必須保持外部同步,以防止對映射進行意外的非同步訪問。
在 Java 編程語言中,最基本的結構就是兩種,一個是數組,另一個是指針(引用),HashMap 就是經過這兩個數據結構進行實現。HashMap其實是一個「鏈表散列」的數據結構,即數組和鏈表的結合體。
從上圖中能夠看出,HashMap 底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個 HashMap 的時候,就會初始化一個數組。
咱們經過 JDK 中的 HashMap 源碼進行一些學習,首先看一下構造函數:
咱們着重看一下第 18 行代碼table = new Entry[capacity];
。這不就是 Java 中數組的建立方式嗎?也就是說在構造函數中,其建立了一個 Entry 的數組,其大小爲 capacity(目前咱們還不須要太瞭解該變量含義),那麼 Entry 又是什麼結構呢?看一下源碼:
咱們目前仍是隻着重核心的部分,Entry 是一個 static class,其中包含了 key 和 value,也就是鍵值對,另外還包含了一個 next 的 Entry 指針。咱們能夠總結出:Entry 就是數組中的元素,每一個 Entry 其實就是一個 key-value 對,它持有一個指向下一個元素的引用,這就構成了鏈表。
咱們看一下方法的標準註釋:在註釋中首先提到了,當咱們 put 的時候,若是 key 存在了,那麼新的 value 會代替舊的 value,而且若是 key 存在的狀況下,該方法返回的是舊的 value,若是 key 不存在,那麼返回 null。
從上面的源代碼中能夠看出:當咱們往 HashMap 中 put 元素的時候,先根據 key 的 hashCode 從新計算 hash 值,根據 hash 值獲得這個元素在數組中的位置(即下標),若是數組該位置上已經存放有其餘元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。若是數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。
addEntry(hash, key, value, i)方法根據計算出的 hash 值,將 key-value 對放在數組 table 的 i 索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼以下:
當系統決定存儲 HashMap 中的 key-value 對時,徹底沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每一個 Entry 的存儲位置。咱們徹底能夠把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置以後,value 隨之保存在那裏便可。
hash(int h)方法根據 key 的 hashCode 從新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,形成的 hash 衝突。
咱們能夠看到在 HashMap 中要找到某個元素,須要根據 key 的 hash 值來求得對應數組中的位置。如何計算這個位置就是 hash 算法。前面說過 HashMap 的數據結構是數組和鏈表的結合,因此咱們固然但願這個 HashMap 裏面的 元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,那麼當咱們用 hash 算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表,這樣就大大優化了查詢的效率。
對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 hash 碼值老是相同的。咱們首先想到的就是把 hash 值對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的。可是,「模」運算的消耗仍是比較大的,在 HashMap 中是這樣作的:調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。indexFor(int h, int length) 方法的代碼以下:
這個方法很是巧妙,它經過 h & (table.length -1) 來獲得該對象的保存位,而 HashMap 底層數組的長度老是 2 的 n 次方,這是 HashMap 在速度上的優化。在 HashMap 構造器中有以下代碼:
這段代碼保證初始化時 HashMap 的容量老是 2 的 n 次方,即底層數組的長度老是爲 2 的 n 次方。
當 length 老是 2 的 n 次方時,h& (length-1)運算等價於對 length 取模,也就是 h%length,可是 & 比 % 具備更高的效率。這看上去很簡單,其實比較有玄機的,咱們舉個例子來講明:
假設數組長度分別爲 15 和 16,優化後的 hash 碼分別爲 8 和 9,那麼 & 運算後的結果以下:
h & (table.length-1) | hash | table.length-1 | ||
---|---|---|---|---|
8 & (15-1): | 0100 | & | 1110 | = 0100 |
9 & (15-1): | 0101 | & | 1110 | = 0100 |
8 & (16-1): | 0100 | & | 1111 | = 0100 |
9 & (16-1): | 0101 | & | 1111 | = 0101 |
從上面的例子中能夠看出:當它們和 15-1(1110)「與」的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8 和 9 會被放到數組中的同一個位置上造成鏈表,那麼查詢的時候就須要遍歷這個鏈 表,獲得8或者9,這樣就下降了查詢的效率。同時,咱們也能夠發現,當數組長度爲 15 的時候,hash 值會與 15-1(1110)進行「與」,那麼最後一位永遠是 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費至關大,更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!而當數組長度爲16時,即爲2的n次方時,2n-1 獲得的二進制數的每一個位上的值都爲 1,這使得在低位上&時,獲得的和原 hash 的低位相同,加之 hash(int h)方法對 key 的 hashCode 的進一步優化,加入了高位計算,就使得只有相同的 hash 值的兩個值纔會被放到數組中的同一個位置上造成鏈表。
因此說,當數組長度爲 2 的 n 次冪的時候,不一樣的 key 算得得 index 相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
根據上面 put 方法的源代碼能夠看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。
有了上面存儲時的 hash 算法做爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中能夠看出:從 HashMap 中 get 元素時,首先計算 key 的 hashCode,找到數組中對應位置的某一元素,而後經過 key 的 equals 方法在對應位置的鏈表中找到須要的元素。
簡單地說,HashMap 在底層將 key-value 當成一個總體進行處理,這個總體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存全部的 key-value 對,當須要存儲一個 Entry 對象時,會根據 hash 算法來決定其在數組中的存儲位置,在根據 equals 方法決定其在該數組位置上的鏈表中的存儲位置;當須要取出一個Entry 時,也會根據 hash 算法找到其在數組中的存儲位置,再根據 equals 方法從該位置上的鏈表中取出該Entry。
當 HashMap 中的元素愈來愈多的時候,hash 衝突的概率也就愈來愈高,由於數組的長度是固定的。因此爲了提升查詢的效率,就要對 HashMap 的數組進行擴容,數組擴容這個操做也會出如今 ArrayList 中,這是一個經常使用的操做,而在 HashMap 數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這就是 resize。
那麼 HashMap 何時進行擴容呢?當 HashMap 中的元素個數超過數組大小 *loadFactor
時,就會進行數組擴容,loadFactor的默認值爲 0.75,這是一個折中的取值。也就是說,默認狀況下,數組大小爲 16,那麼當 HashMap 中元素個數超過 16*0.75=12
的時候,就把數組的大小擴展爲 2*16=32
,即擴大一倍,而後從新計算每一個元素在數組中的位置,而這是一個很是消耗性能的操做,因此若是咱們已經預知 HashMap 中元素的個數,那麼預設元素的個數可以有效的提升 HashMap 的性能。
HashMap 包含以下幾個構造器:
HashMap 的基礎構造器 HashMap(int initialCapacity, float loadFactor) 帶有兩個參數,它們是初始容量 initialCapacity 和負載因子 loadFactor。
負載因子 loadFactor 衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來講,查找一個元素的平均時間是 O(1+a),所以若是負載因子越大,對空間的利用更充分,然然後果是查找效率的下降;若是負載因子過小,那麼散列表的數據將過於稀疏,對空間形成嚴重浪費。
HashMap 的實現中,經過 threshold 字段來判斷 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
結合負載因子的定義公式可知,threshold 就是在此 loadFactor 和 capacity 對應下容許的最大元素數目,超過這個數目就從新 resize,以下降實際的負載因子。默認的的負載因子 0.75 是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize 後的 HashMap 容量是容量的兩倍:
咱們知道 java.util.HashMap 不是線程安全的,所以若是在使用迭代器的過程當中有其餘線程修改了 map,那麼將拋出 ConcurrentModificationException,這就是所謂 fail-fast 策略。
ail-fast 機制是 java 集合(Collection)中的一種錯誤機制。 當多個線程對同一個集合的內容進行操做時,就可能會產生 fail-fast 事件。
例如:當某一個線程 A 經過 iterator去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變了;那麼線程 A 訪問集合時,就會拋出 ConcurrentModificationException 異常,產生 fail-fast 事件。
這一策略在源碼中的實現是經過 modCount 域,modCount 顧名思義就是修改次數,對 HashMap 內容(固然不只僅是 HashMap 纔會有,其餘例如 ArrayList 也會)的修改都將增長這個值(你們能夠再回頭看一下其源碼,在不少操做中都有 modCount++ 這句),那麼在迭代器初始化過程當中會將這個值賦給迭代器的 expectedModCount。
在迭代過程當中,判斷 modCount 跟 expectedModCount 是否相等,若是不相等就表示已經有其餘線程修改了 Map:
注意到 modCount 聲明爲 volatile,保證線程之間修改的可見性。
在 HashMap 的 API 中指出:
由全部 HashMap 類的「collection 視圖方法」所返回的迭代器都是快速失敗的:在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自己的 remove 方法,其餘任什麼時候間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。所以,面對併發的修改,迭代器很快就會徹底失敗,而不冒在未來不肯定的時間發生任意不肯定行爲的風險。
注意,迭代器的快速失敗行爲不能獲得保證,通常來講,存在非同步的併發修改時,不可能做出任何堅定的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。所以,編寫依賴於此異常的程序的作法是錯誤的,正確作法是:迭代器的快速失敗行爲應該僅用於檢測程序錯誤。
在上文中也提到,fail-fast 機制,是一種錯誤檢測機制。它只能被用來檢測錯誤,由於 JDK 並不保證 fail-fast 機制必定會發生。若在多線程環境下使用 fail-fast 機制的集合,建議使用「java.util.concurrent 包下的類」去取代「java.util 包下的類」。
效率高,之後必定要使用此種方式!
效率低,之後儘可能少使用!
對於 HashSet 而言,它是基於 HashMap 實現的,底層採用 HashMap 來保存元素,因此若是對 HashMap 比較熟悉了,那麼學習 HashSet 也是很輕鬆的。
咱們先經過 HashSet 最簡單的構造函數和幾個成員變量來看一下,證實我們上邊說的,其底層是 HashMap:
其實在英文註釋中已經說的比較明確了。首先有一個HashMap的成員變量,咱們在 HashSet 的構造函數中將其初始化,默認狀況下采用的是 initial capacity爲16,load factor 爲 0.75。
對於 HashSet 而言,它是基於 HashMap 實現的,HashSet 底層使用 HashMap 來保存全部元素,所以 HashSet 的實現比較簡單,相關 HashSet 的操做,基本上都是直接調用底層 HashMap 的相關方法來完成,咱們應該爲保存到 HashSet 中的對象覆蓋 hashCode() 和 equals()
若是此 set 中還沒有包含指定元素,則添加指定元素。更確切地講,若是此 set 沒有包含知足(e==null ? e2==null : e.equals(e2)) 的元素 e2,則向此 set 添加指定的元素 e。若是此 set 已包含該元素,則該調用不更改 set 並返回 false。但底層實際將將該元素做爲 key 放入 HashMap。思考一下爲何?
因爲 HashMap 的 put() 方法添加 key-value 對時,當新放入 HashMap 的 Entry 中 key 與集合中原有 Entry 的 key 相同(hashCode()返回值相等,經過 equals 比較也返回 true),新添加的 Entry 的 value 會將覆蓋原來 Entry 的 value(HashSet 中的 value 都是PRESENT
),但 key 不會有任何改變,所以若是向 HashSet 中添加一個已經存在的元素時,新添加的集合元素將不會被放入 HashMap中,原來的元素也不會有任何改變,這也就知足了 Set 中元素不重複的特性。
該方法若是添加的是在 HashSet 中不存在的,則返回 true;若是添加的元素已經存在,返回 false。其緣由在於咱們以前提到的關於 HashMap 的 put 方法。該方法在添加 key 不重複的鍵值對的時候,會返回 null。
和 HashMap 同樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對。
Hashtable 在 Java 中的定義爲:
從源碼中,咱們能夠看出,Hashtable 繼承於 Dictionary 類,實現了 Map, Cloneable, java.io.Serializable接口。其中Dictionary類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類,每一個鍵和值都是對象(源碼註釋爲:The Dictionary
class is the abstract parent of any class, such as Hashtable
, which maps keys to values. Every key and every value is an object.)。但在這一點我開始有點懷疑,由於我查看了HashMap以及TreeMap的源碼,都沒有繼承於這個類。不過當我看到註釋中的解釋也就明白了,其 Dictionary 源碼註釋是這樣的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. 該話指出 Dictionary 這個類過期了,新的實現類應該實現Map接口。
Hashtable是經過"拉鍊法"實現的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。
關於變量的解釋在源碼註釋中都有,最好仍是應該看英文註釋。
Hashtable 一共提供了 4 個構造方法:
public Hashtable(int initialCapacity, float loadFactor)
: 用指定初始容量和指定加載因子構造一個新的空哈希表。useAltHashing 爲 boolean,其若是爲真,則執行另外一散列的字符串鍵,以減小因爲弱哈希計算致使的哈希衝突的發生。public Hashtable(int initialCapacity)
:用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。public Hashtable()
:默認構造函數,容量爲 11,加載因子爲 0.75。public Hashtable(Map<? extends K, ? extends V> t)
:構造一個與給定的 Map 具備相同映射關係的新哈希表。put 方法的整個流程爲:
我在下面的代碼中也進行了一些註釋:
經過一個實際的例子來演示一下這個過程:
假設咱們如今Hashtable的容量爲5,已經存在了(5,5),(13,13),(16,16),(17,17),(21,21)這 5 個鍵值對,目前他們在Hashtable中的位置以下:
如今,咱們插入一個新的鍵值對,put(16,22),假設key=16的索引爲1.但如今索引1的位置有兩個Entry了,因此程序會對鏈表進行迭代。迭代的過程當中,發現其中有一個Entry的key和咱們要插入的鍵值對的key相同,因此如今會作的工做就是將newValue=22替換oldValue=16,而後返回oldValue=16.
而後咱們如今再插入一個,put(33,33),key=33的索引爲3,而且在鏈表中也不存在key=33的Entry,因此將該節點插入鏈表的第一個位置。
相比較於 put 方法,get 方法則簡單不少。其過程就是首先經過 hash()方法求得 key 的哈希值,而後根據 hash 值獲得 index 索引(上述兩步所用的算法與 put 方法都相同)。而後迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。
Hashtable 有多種遍歷方式:
HashMap 是無序的,HashMap 在 put 的時候是根據 key 的 hashcode 進行 hash 而後放入對應的地方。因此在按照必定順序 put 進 HashMap 中,而後遍歷出 HashMap 的順序跟 put 的順序不一樣(除非在 put 的時候 key 已經按照 hashcode 排序號了,這種概率很是小)
JAVA 在 JDK1.4 之後提供了 LinkedHashMap 來幫助咱們實現了有序的 HashMap!
LinkedHashMap 是 HashMap 的一個子類,它保留插入的順序,若是須要輸出的順序和輸入時的相同,那麼就選用 LinkedHashMap。
LinkedHashMap 是 Map 接口的哈希表和連接列表實現,具備可預知的迭代順序。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
LinkedHashMap 實現與 HashMap 的不一樣之處在於,LinkedHashMap 維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序能夠是插入順序或者是訪問順序。
注意,此實現不是同步的。若是多個線程同時訪問連接的哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。
根據鏈表中元素的順序能夠分爲:按插入順序的鏈表,和按訪問順序(調用 get 方法)的鏈表。默認是按插入順序排序,若是指定按訪問順序排序,那麼調用get方法後,會將此次訪問的元素移至鏈表尾部,不斷訪問能夠造成按訪問順序排序的鏈表。
我在最開始學習 LinkedHashMap 的時候,看到訪問順序、插入順序等等,有點暈了,隨着後續的學習才慢慢懂得其中原理,因此我會先在進行作幾個 demo 來演示一下 LinkedHashMap 的使用。看懂了其效果,而後再來研究其原理。
看下面這個代碼:
一個比較簡單的測試 HashMap 的代碼,經過控制檯的輸出,咱們能夠看到 HashMap 是沒有順序的。
咱們如今將 map 的實現換成 LinkedHashMap,其餘代碼不變:Map<String, String> map = new LinkedHashMap<String, String>();
看一下控制檯的輸出:
咱們能夠看到,其輸出順序是完成按照插入順序的!也就是咱們上面所說的保留了插入的順序。咱們不是在上面還提到過其能夠按照訪問順序進行排序麼?好的,咱們仍是經過一個例子來驗證一下:
代碼與以前的都差很少,但咱們多了兩行代碼,而且初始化 LinkedHashMap 的時候,用的構造函數也不相同,看一下控制檯的輸出結果:
這也就是咱們以前提到過的,LinkedHashMap 能夠選擇按照訪問順序進行排序。
對於 LinkedHashMap 而言,它繼承與 HashMap(public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
)、底層使用哈希表與雙向鏈表來保存全部元素。其基本操做與父類 HashMap 類似,它經過重寫父類相關的方法,來實現本身的連接列表特性。下面咱們來分析 LinkedHashMap 的源代碼:
LinkedHashMap 採用的 hash 算法和 HashMap 相同,可是它從新定義了數組中保存的元素 Entry,該 Entry 除了保存當前對象的引用外,還保存了其上一個元素 before 和下一個元素 after 的引用,從而在哈希表的基礎上又構成了雙向連接列表。看源代碼:
LinkedHashMap 中的 Entry 集成與 HashMap 的 Entry,可是其增長了 before 和 after 的引用,指的是上一個元素和下一個元素的引用。
經過源代碼能夠看出,在 LinkedHashMap 的構造方法中,實際調用了父類 HashMap 的相關構造方法來構造一個底層存放的 table 數組,但額外能夠增長 accessOrder 這個參數,若是不設置,默認爲 false,表明按照插入順序進行迭代;固然能夠顯式設置爲 true,表明以訪問順序進行迭代。如:
咱們已經知道 LinkedHashMap 的 Entry 元素繼承 HashMap 的 Entry,提供了雙向鏈表的功能。在上述 HashMap 的構造器中,最後會調用 init() 方法,進行相關的初始化,這個方法在 HashMap 的實現中並沒有意義,只是提供給子類實現相關的初始化調用。
但在 LinkedHashMap 重寫了 init() 方法,在調用父類的構造方法完成構造後,進一步實現了對其元素 Entry 的初始化操做。
LinkedHashMap 並未重寫父類 HashMap 的 put 方法,而是重寫了父類 HashMap 的 put 方法調用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了本身特有的雙向連接列表的實現。咱們在以前的文章中已經講解了HashMap的put方法,咱們在這裏從新貼一下 HashMap 的 put 方法的源代碼:
HashMap.put:
重寫方法:
LinkedHashMap 重寫了父類 HashMap 的 get 方法,實際在調用父類 getEntry() 方法取得查找的元素後,再判斷當排序模式 accessOrder 爲 true 時,記錄訪問順序,將最新訪問的元素添加到雙向鏈表的表頭,並從原來的位置刪除。因爲的鏈表的增長、刪除操做是常量級的,故並不會帶來性能的損失。
LinkedHashMap 定義了排序模式 accessOrder,該屬性爲 boolean 型變量,對於訪問順序,爲 true;對於插入順序,則爲 false。通常狀況下,沒必要指定排序模式,其迭代順序即爲默認爲插入順序。
這些構造方法都會默認指定排序模式爲插入順序。若是你想構造一個 LinkedHashMap,並打算按從近期訪問最少到近期訪問最多的順序(即訪問順序)來保存元素,那麼請使用下面的構造方法構造 LinkedHashMap:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
該哈希映射的迭代順序就是最後訪問其條目的順序,這種映射很適合構建 LRU 緩存。LinkedHashMap 提供了 removeEldestEntry(Map.Entry<K,V> eldest) 方法。該方法能夠提供在每次添加新條目時移除最舊條目的實現程序,默認返回 false,這樣,此映射的行爲將相似於正常映射,即永遠不能移除最舊的元素。
咱們會在後面的文章中詳細介紹關於如何用 LinkedHashMap 構建 LRU 緩存。
其實 LinkedHashMap 幾乎和 HashMap 同樣:從技術上來講,不一樣的是它定義了一個 Entry<K,V> header,這個 header 不是放在 Table 裏,它是額外獨立出來的。LinkedHashMap 經過繼承 hashMap 中的 Entry<K,V>,並添加兩個屬性 Entry<K,V> before,after,和 header 結合起來組成一個雙向鏈表,來實現按插入順序或訪問順序排序。
在寫關於 LinkedHashMap 的過程當中,記起來以前面試的過程當中遇到的一個問題,也是問我 Map 的哪一種實現能夠作到按照插入順序進行迭代?當時腦子是忽然短路的,但如今想一想,也只能怪本身對這個知識點仍是掌握的不夠紮實,因此又從頭認真的把代碼看了一遍。
不過,個人建議是,你們首先首先須要記住的是:LinkedHashMap 可以作到按照插入順序或者訪問順序進行迭代,這樣在咱們之後的開發中遇到類似的問題,才能想到用 LinkedHashMap 來解決,不然就算對其內部結構很是瞭解,不去使用也是沒有什麼用的。
思考了很久,到底要不要總結 LinkedHashSet 的內容 = = 我在以前的博文中,分別寫了 HashMap 和 HashSet,而後咱們能夠看到 HashSet 的方法基本上都是基於 HashMap 來實現的,說白了,HashSet內部的數據結構就是一個 HashMap,其方法的內部幾乎就是在調用 HashMap 的方法。
LinkedHashSet 首先咱們須要知道的是它是一個 Set 的實現,因此它其中存的確定不是鍵值對,而是值。此實現與 HashSet 的不一樣之處在於,LinkedHashSet 維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序可爲插入順序或是訪問順序。
看到上面的介紹,是否是感受其與 HashMap 和 LinkedHashMap 的關係很像?
注意,此實現不是同步的。若是多個線程同時訪問連接的哈希Set,而其中至少一個線程修改了該 Set,則它必須保持外部同步。
在LinkedHashMap的實現原理中,經過例子演示了 HashMap 和 LinkedHashMap 的區別。觸類旁通,咱們如今學習的LinkedHashSet與以前的很相同,只不過以前存的是鍵值對,而如今存的只有值。
因此我就再也不具體的貼代碼在這邊了,但咱們能夠確定的是,LinkedHashSet 是能夠按照插入順序或者訪問順序進行迭代。
對於 LinkedHashSet 而言,它繼承與 HashSet、又基於 LinkedHashMap 來實現的。
LinkedHashSet 底層使用 LinkedHashMap 來保存全部元素,它繼承與 HashSet,其全部的方法操做上又與 HashSet 相同,所以 LinkedHashSet 的實現上很是簡單,只提供了四個構造方法,並經過傳遞一個標識參數,調用父類的構造器,底層構造一個 LinkedHashMap 來實現,在相關操做上與父類 HashSet 的操做相同,直接調用父類 HashSet 的方法便可。LinkedHashSet 的源代碼以下:
以上幾乎就是 LinkedHashSet 的所有代碼了,那麼讀者可能就會懷疑了,不是說 LinkedHashSet 是基於 LinkedHashMap 實現的嗎?那我爲何在源碼中甚至都沒有看到出現過 LinkedHashMap。不要着急,咱們能夠看到在 LinkedHashSet 的構造方法中,其調用了父類的構造方法。咱們能夠進去看一下:
在父類 HashSet 中,專爲 LinkedHashSet 提供的構造方法以下,該方法爲包訪問權限,並未對外公開。
由上述源代碼可見,LinkedHashSet 經過繼承 HashSet,底層使用 LinkedHashMap,以很簡單明瞭的方式來實現了其自身的全部功能。
以上就是關於 LinkedHashSet 的內容,咱們只是從概述上以及構造方法這幾個方面介紹了,並非咱們不想去深刻其讀取或者寫入方法,而是其自己沒有實現,只是繼承於父類 HashSet 的方法。
因此咱們須要注意的點是:
ArrayList 能夠理解爲動態數組,用 MSDN 中的說法,就是 Array 的複雜版本。與 Java 中的數組相比,它的容量能動態增加。ArrayList 是 List 接口的可變數組的實現。實現了全部可選列表操做,並容許包括 null 在內的全部元素。除了實現 List 接口外,此類還提供一些方法來操做內部用來存儲列表的數組的大小。(此類大體上等同於 Vector 類,除了此類是不一樣步的。)
每一個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它老是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增加。自動增加會帶來數據向新數組的從新拷貝,所以,若是可預知數據量的多少,可在構造 ArrayList 時指定其容量。在添加大量元素前,應用程序也可使用 ensureCapacity 操做來增長 ArrayList 實例的容量,這能夠減小遞增式再分配的數量。
注意,此實現不是同步的。若是多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步。(結構上的修改是指任何添加或刪除一個或多個元素的操做,或者顯式調整底層數組的大小;僅僅設置元素的值不是結構上的修改。)
咱們先學習瞭解其內部的實現原理,才能更好的理解其應用。
對於 ArrayList 而言,它實現 List 接口、底層使用數組保存全部元素。其操做基本上是對數組的操做。下面咱們來分析 ArrayList 的源代碼:
ArrayList 繼承了 AbstractList,實現了 List。它是一個數組隊列,提供了相關的添加、刪除、修改、遍歷等功能。
ArrayList 實現了 RandmoAccess 接口,即提供了隨機訪問功能。RandmoAccess 是 java 中用來被 List 實現,爲 List 提供快速訪問功能的。在 ArrayList 中,咱們便可以經過元素的序號快速獲取元素對象;這就是快速隨機訪問。
ArrayList 實現了 Cloneable 接口,即覆蓋了函數 clone(),能被克隆。 ArrayList 實現 java.io.Serializable 接口,這意味着 ArrayList 支持序列化,能經過序列化去傳輸。
ArrayList 提供了三種方式的構造器:
public ArrayList()
能夠構造一個默認初始容量爲10的空列表;public ArrayList(int initialCapacity)
構造一個指定初始容量的空列表;public ArrayList(Collection<? extends E> c)
構造一個包含指定 collection 的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。ArrayList 中提供了多種添加元素的方法,下面將一一進行講解:
1.set(int index, E element):該方法首先調用rangeCheck(index)
來校驗 index 變量是否超出數組範圍,超出則拋出異常。然後,取出原 index 位置的值,而且將新的 element 放入 Index 位置,返回 oldValue。
2.add(E e):該方法是將指定的元素添加到列表的尾部。當容量不足時,會調用 grow 方法增加容量。
3.add(int index, E element):在 index 位置插入 element。
4.addAll(Collection<? extends E> c)
和 addAll(int index, Collection<? extends E> c)
:將特定 Collection 中的元素添加到 Arraylist 末尾。
在 ArrayList 的存儲方法,其核心本質是在數組的某個位置將元素添加進入。但其中又會涉及到關於數組容量不夠而增加等因素。
這個方法就比較簡單了,ArrayList 可以支持隨機訪問的緣由也是很顯然的,由於它內部的數據結構是數組,而數組自己就是支持隨機訪問。該方法首先會判斷輸入的index值是否越界,而後將數組的 index 位置的元素返回便可。
ArrayList 提供了根據下標或者指定對象兩種方式的刪除功能。須要注意的是該方法的返回值並不相同,以下:
注意:從數組中移除元素的操做,也會致使被移除的元素之後的全部元素的向左移動一個位置。
從上面介紹的向 ArrayList 中存儲元素的代碼中,咱們看到,每當向數組中添加元素時,都要去檢查添加後元素的個數是否會超出當前數組的長度,若是超出,數組將會進行擴容,以知足添加數據的需求。數組擴容有兩個方法,其中開發者能夠經過一個 public 的方法ensureCapacity(int minCapacity)
來增長 ArrayList 的容量,而在存儲元素等操做過程當中,若是遇到容量不足,會調用priavte方法private void ensureCapacityInternal(int minCapacity)
實現。
從上述代碼中能夠看出,數組進行擴容時,會將老數組中的元素從新拷貝一份到新的數組中,每次數組容量的增加大約是其原容量的 1.5 倍(從int newCapacity = oldCapacity + (oldCapacity >> 1)
這行代碼得出)。這種操做的代價是很高的,所以在實際使用時,咱們應該儘可能避免數組容量的擴張。當咱們可預知要保存的元素的多少時,要在構造 ArrayList 實例時,就指定其容量,以免數組擴容的發生。或者根據實際需求,經過調用ensureCapacity 方法來手動增長 ArrayList 實例的容量。
ArrayList 也採用了快速失敗的機制,經過記錄 modCount 參數來實現。在面對併發的修改時,迭代器很快就會徹底失敗,而不是冒着在未來某個不肯定時間發生任意不肯定行爲的風險。 關於 Fail-Fast 的更詳細的介紹,我在以前將 HashMap 中已經提到。
LinkedList 和 ArrayList 同樣,都實現了 List 接口,但其內部的數據結構有本質的不一樣。LinkedList 是基於鏈表實現的(經過名字也能區分開來),因此它的插入和刪除操做比 ArrayList 更加高效。但也是因爲其爲基於鏈表的,因此隨機訪問的效率要比 ArrayList 差。
看一下 LinkedList 的類的定義:
LinkedList 繼承自 AbstractSequenceList,實現了 List、Deque、Cloneable、java.io.Serializable 接口。AbstractSequenceList 提供了List接口骨幹性的實現以減小實現 List 接口的複雜度,Deque 接口定義了雙端隊列的操做。
在 LinkedList 中除了自己本身的方法外,還提供了一些可使其做爲棧、隊列或者雙端隊列的方法。這些方法可能彼此之間只是名字不一樣,以使得這些名字在特定的環境中顯得更加合適。
LinkedList 也是 fail-fast 的(前邊提過不少次了)。
LinkedList 是基於鏈表結構實現,因此在類中包含了 first 和 last 兩個指針(Node)。Node 中包含了上一個節點和下一個節點的引用,這樣就構成了雙向的鏈表。每一個 Node 只能知道本身的前一個節點和後一個節點,但對於鏈表來講,這已經足夠了。
該方法是在鏈表的 end 添加元素,其調用了本身的方法 linkLast(E e)。
該方法首先將 last 的 Node 引用指向了一個新的 Node(l),而後根據l新建了一個 newNode,其中的元素就爲要添加的 e;然後,咱們讓 last 指向了 newNode。接下來是自身進行維護該鏈表。
該方法是在指定 index 位置插入元素。若是 index 位置正好等於 size,則調用 linkLast(element) 將其插入末尾;不然調用 linkBefore(element, node(index))方法進行插入。該方法的實如今下面,你們能夠本身仔細的分析一下。(分析鏈表的時候最好可以邊畫圖邊分析)
LinkedList 的方法實在是太多,在這無法一一舉例分析。但不少方法其實都只是在調用別的方法而已,因此建議你們將其幾個最核心的添加的方法搞懂就能夠了,好比 linkBefore、linkLast。其本質也就是鏈表之間的刪除添加等。
咱們在以前的博文中瞭解到關於 HashMap 和 Hashtable 這兩種集合。其中 HashMap 是非線程安全的,當咱們只有一個線程在使用 HashMap 的時候,天然不會有問題,但若是涉及到多個線程,而且有讀有寫的過程當中,HashMap 就不能知足咱們的須要了(fail-fast)。在不考慮性能問題的時候,咱們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap),這兩種方式基本都是對整個 hash 表結構作鎖定操做的,這樣在鎖表的期間,別的線程就須要等待了,無疑性能不高。
因此咱們在本文中學習一個 util.concurrent 包的重要成員,ConcurrentHashMap。
ConcurrentHashMap 的實現是依賴於 Java 內存模型,因此咱們在瞭解 ConcurrentHashMap 的前提是必須瞭解Java 內存模型。但 Java 內存模型並非本文的重點,因此我假設讀者已經對 Java 內存模型有所瞭解。
ConcurrentHashMap 的結構是比較複雜的,都深究去本質,其實也就是數組和鏈表而已。咱們由淺入深慢慢的分析其結構。
先簡單分析一下,ConcurrentHashMap 的成員變量中,包含了一個 Segment 的數組(final Segment<K,V>[] segments;
),而 Segment 是 ConcurrentHashMap 的內部類,而後在 Segment 這個類中,包含了一個 HashEntry 的數組(transient volatile HashEntry<K,V>[] table;
)。而 HashEntry 也是 ConcurrentHashMap 的內部類。HashEntry 中,包含了 key 和 value 以及 next 指針(相似於 HashMap 中 Entry),因此 HashEntry 能夠構成一個鏈表。
因此通俗的講,ConcurrentHashMap 數據結構爲一個 Segment 數組,Segment 的數據結構爲 HashEntry 的數組,而 HashEntry 存的是咱們的鍵值對,能夠構成鏈表。
首先,咱們看一下 HashEntry 類。
HashEntry 用來封裝散列映射表中的鍵值對。在 HashEntry 類中,key,hash 和 next 域都被聲明爲 final 型,value 域被聲明爲 volatile 型。其類的定義爲:
HashEntry 的學習能夠類比着 HashMap 中的 Entry。咱們的存儲鍵值對的過程當中,散列的時候若是發生「碰撞」,將採用「分離鏈表法」來處理碰撞:把碰撞的 HashEntry 對象連接成一個鏈表。
以下圖,咱們在一個空桶中插入 A、B、C 兩個 HashEntry 對象後的結構圖(其實應該爲鍵值對,在這進行了簡化以方便更容易理解):
Segment 的類定義爲static final class Segment<K,V> extends ReentrantLock implements Serializable
。其繼承於 ReentrantLock 類,從而使得 Segment 對象能夠充當鎖的角色。Segment 中包含HashEntry 的數組,其能夠守護其包含的若干個桶(HashEntry的數組)。Segment 在某些意義上有點相似於 HashMap了,都是包含了一個數組,而數組中的元素能夠是一個鏈表。
table:table 是由 HashEntry 對象組成的數組若是散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式連接成一個鏈表table數組的數組成員表明散列映射表的一個桶每一個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分若是併發級別爲 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16。
count 變量是計算器,表示每一個 Segment 對象管理的 table 數組(若干個 HashEntry 的鏈表)包含的HashEntry 對象的個數。之因此在每一個Segment對象中包含一個 count 計數器,而不在 ConcurrentHashMap 中使用全局的計數器,是爲了不出現「熱點域」而影響併發性。
咱們經過下圖來展現一下插入 ABC 三個節點後,Segment 的示意圖:
其實從我我的角度來講,Segment結構是與HashMap很像的。
ConcurrentHashMap 的結構中包含的 Segment 的數組,在默認的併發級別會建立包含 16 個 Segment 對象的數組。經過咱們上面的知識,咱們知道每一個 Segment 又包含若干個散列表的桶,每一個桶是由 HashEntry 連接起來的一個鏈表。若是 key 可以均勻散列,每一個 Segment 大約守護整個散列表桶總數的 1/16。
下面咱們還有經過一個圖來演示一下 ConcurrentHashMap 的結構:
在 ConcurrentHashMap 中,當執行 put 方法的時候,會須要加鎖來完成。咱們經過代碼來解釋一下具體過程: 當咱們 new 一個 ConcurrentHashMap 對象,而且執行put操做的時候,首先會執行 ConcurrentHashMap 類中的 put 方法,該方法源碼爲:
咱們經過註釋能夠了解到,ConcurrentHashMap 不容許空值。該方法首先有一個 Segment 的引用 s,而後會經過 hash() 方法對 key 進行計算,獲得哈希值;繼而經過調用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進行存儲操做。該方法源碼爲:
關於該方法的某些關鍵步驟,在源碼上加上了註釋。
須要注意的是:加鎖操做是針對的 hash 值對應的某個 Segment,而不是整個 ConcurrentHashMap。由於 put 操做只是在這個 Segment 中完成,因此並不須要對整個 ConcurrentHashMap 加鎖。因此,此時,其餘的線程也能夠對另外的 Segment 進行 put 操做,由於雖然該 Segment 被鎖住了,但其餘的 Segment 並無加鎖。同時,讀線程並不會由於本線程的加鎖而阻塞。
正是由於其內部的結構以及機制,因此 ConcurrentHashMap 在併發訪問的性能上要比Hashtable和同步包裝以後的HashMap的性能提升不少。在理想狀態下,ConcurrentHashMap 能夠支持 16 個線程執行併發寫操做(若是併發級別設置爲 16),及任意數量線程的讀操做。
在實際的應用中,散列表通常的應用場景是:除了少數插入操做和刪除操做外,絕大多數都是讀取操做,並且讀操做在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操做作了大量的優化。經過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操做不須要加鎖就能夠正確得到值。這個特性使得 ConcurrentHashMap 的併發性能在分離鎖的基礎上又有了近一步的提升。
ConcurrentHashMap 是一個併發散列映射表的實現,它容許徹底併發的讀取,而且支持給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不一樣線程間的併發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全併發訪問,但同時也致使對容器的訪問變成串行化的了。
ConcurrentHashMap 的高併發性主要來自於三個方面:
使用分離鎖,減少了請求 同一個鎖的頻率。
經過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操做大多數時候不須要加鎖就能成功獲取到須要的值。因爲散列映射表在實際應用中大多數操做都是成功的 讀操做,因此 2 和 3 既能夠減小請求同一個鎖的頻率,也能夠有效減小持有鎖的時間。經過減少請求同一個鎖的頻率和儘可能減小持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提升。
咱們平時總會有一個電話本記錄全部朋友的電話,可是,若是有朋友常常聯繫,那些朋友的電話號碼不用翻電話本咱們也能記住,可是,若是長時間沒有聯繫了,要再次聯繫那位朋友的時候,咱們又不得不求助電話本,可是,經過電話本查找仍是很費時間的。可是,咱們大腦可以記住的東西是必定的,咱們只能記住本身最熟悉的,而長時間不熟悉的天然就忘記了。
其實,計算機也用到了一樣的一個概念,咱們用緩存來存放之前讀取的數據,而不是直接丟掉,這樣,再次讀取的時候,能夠直接在緩存裏面取,而不用再從新查找一遍,這樣系統的反應能力會有很大提升。可是,當咱們讀取的個數特別大的時候,咱們不可能把全部已經讀取的數據都放在緩存裏,畢竟內存大小是必定的,咱們通常把最近常讀取的放在緩存裏(至關於咱們把最近聯繫的朋友的姓名和電話放在大腦裏同樣)。
LRU 緩存利用了這樣的一種思想。LRU 是 Least Recently Used 的縮寫,翻譯過來就是「最近最少使用」,也就是說,LRU 緩存把最近最少使用的數據移除,讓給最新讀取的數據。而每每最常讀取的,也是讀取次數最多的,因此,利用 LRU 緩存,咱們可以提升系統的 performance。
要實現 LRU 緩存,咱們首先要用到一個類 LinkedHashMap。
用這個類有兩大好處:一是它自己已經實現了按照訪問順序的存儲,也就是說,最近讀取的會放在最前面,最最不常讀取的會放在最後(固然,它也能夠實現按照插入順序存儲)。第二,LinkedHashMap 自己有一個方法用於判斷是否須要移除最不常讀取的數,可是,原始方法默認不須要移除(這是,LinkedHashMap 至關於一個linkedlist),因此,咱們須要 override 這樣一個方法,使得當緩存裏存放的數據個數超過規定個數後,就把最不經常使用的移除掉。關於 LinkedHashMap 中已經有詳細的介紹。
代碼以下:(可直接複製,也能夠經過LRUcache-Java下載)
HashMap 和 HashSet 都是 collection 框架的一部分,它們讓咱們可以使用對象的集合。collection 框架有本身的接口和實現,主要分爲 Set 接口,List 接口和 Queue 接口。它們有各自的特色,Set 的集合裏不容許對象有重複的值,List 容許有重複,它對集合中的對象進行索引,Queue 的工做原理是 FCFS 算法(First Come, First Serve)。
首先讓咱們來看看什麼是 HashMap 和 HashSet,而後再來比較它們之間的分別。
HashSet 實現了 Set 接口,它不容許集合中有重複的值,當咱們提到 HashSet 時,第一件事情就是在將對象存儲在 HashSet 以前,要先確保對象重寫 equals()和 hashCode()方法,這樣才能比較對象的值是否相等,以確保set中沒有儲存相等的對象。若是咱們沒有重寫這兩個方法,將會使用這個方法的默認實現。
public boolean add(Object o)
方法用來在 Set 中添加元素,當元素值重複時則會當即返回 false,若是成功添加的話會返回 true。
HashMap 實現了 Map 接口,Map 接口對鍵值對進行映射。Map 中不容許重複的鍵。Map 接口有兩個基本的實現,HashMap 和 TreeMap。TreeMap 保存了對象的排列次序,而 HashMap 則不能。HashMap 容許鍵和值爲 null。HashMap 是非 synchronized 的,但 collection 框架提供方法能保證 HashMap synchronized,這樣多個線程同時訪問 HashMap 時,能保證只有一個線程更改 Map。
public Object put(Object Key,Object value)
方法用來將元素添加到 map 中。
HashMap | HashSet |
---|---|
HashMap實現了Map接口 | HashSet實現了Set接口 |
HashMap儲存鍵值對 | HashSet僅僅存儲對象 |
使用put()方法將元素放入map中 | 使用add()方法將元素放入set中 |
HashMap中使用鍵對象來計算hashcode值 | HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false |
HashMap比較快,由於是使用惟一的鍵來獲取對象 | HashSet較HashMap來講比較慢 |
ct o)
方法用來在 Set 中添加元素,當元素值重複時則會當即返回 false,若是成功添加的話會返回 true。
HashMap 實現了 Map 接口,Map 接口對鍵值對進行映射。Map 中不容許重複的鍵。Map 接口有兩個基本的實現,HashMap 和 TreeMap。TreeMap 保存了對象的排列次序,而 HashMap 則不能。HashMap 容許鍵和值爲 null。HashMap 是非 synchronized 的,但 collection 框架提供方法能保證 HashMap synchronized,這樣多個線程同時訪問 HashMap 時,能保證只有一個線程更改 Map。
public Object put(Object Key,Object value)
方法用來將元素添加到 map 中。
HashMap | HashSet |
---|---|
HashMap實現了Map接口 | HashSet實現了Set接口 |
HashMap儲存鍵值對 | HashSet僅僅存儲對象 |
使用put()方法將元素放入map中 | 使用add()方法將元素放入set中 |
HashMap中使用鍵對象來計算hashcode值 | HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false |
HashMap比較快,由於是使用惟一的鍵來獲取對象 | HashSet較HashMap來講比較慢 |