1. HashMap的數據結構
數據結構中有數組和鏈表來實現對數據的存儲,但這兩者基本上是兩個極端。
數組
數組存儲區間是連續的,占用內存嚴重,故空間復雜的很大。但數組的二分查找時間復雜度小,為O(1);數組的特點是:尋址容易,插入和刪除困難;
鏈表
鏈表存儲區間離散,占用內存比較寬松,故空間復雜度很小,但時間復雜度很大,達O(N)。鏈表的特點是:尋址困難,插入和刪除容易。
哈希表
那么我們能不能綜合兩者的特性,做出一種尋址容易,插入刪除也容易的數據結構?答案是肯定的,這就是我們要提起的哈希表。哈希表((Hash table)既滿足了數據的查找方便,同時不占用太多的內容空間,使用也十分方便。
哈希表有多種不同的實現方法,我接下來解釋的是最常用的一種方法—— 拉鏈法,我們可以理解為“鏈表的數組” ,如圖:
從上圖我們可以發現哈希表是由數組+鏈表組成的,一個長度為16的數組中,每個元素存儲的是一個鏈表的頭結點。那么這些元素是按照什么樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標為12的位置。
HashMap其實也是一個線性的數組實現的,所以可以理解為其存儲數據的容器就是一個線性數組。這可能讓我們很不解,一個線性的數組怎么實現按鍵值對來存取數據呢?這里HashMap有做一些處理。
首先HashMap里面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,我們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map里面的內容都保存在Entry[]里面。
transient Entry[] table;
2. HashMap的存取實現
既然是線性數組,為什么能隨機存取?這里HashMap用了一個小算法,大致是這樣實現:
// 存儲時:
int hash = key.hashCode(); // 這個hashCode方法這里不詳述,只要理解每個key的hash是一個固定的int值
int index = hash % Entry[].length; Entry[index] = value; // 取值時:
int hash = key.hashCode(); int index = hash % Entry[].length; return Entry[index];
1)put
這里HashMap里面用到鏈式數據結構的一個概念。上面我們提到過Entry類里面有一個next屬性,作用是指向下一個Entry。打個比方, 第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會后又進來一個鍵值對B,通過計算其index也等於0,現在怎么辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等於0,那么C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性鏈接在一起。所以疑問不用擔心。也就是說數組中存儲的是最后插入的元素。到這里為止,HashMap的大致實現,我們應該已經清楚了。
public V put(K key, V value) { if (key == null) return putForNullKey(value); //null總是放在數組的第一個鏈表中
int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //遍歷鏈表
for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //如果key在鏈表中已存在,則替換為新value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; } void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //參數e, 是Entry.next //如果size超過threshold,則擴充table大小。再散列
if (size++ >= threshold) resize(2 * table.length); }
當然HashMap里面也包含一些優化方面的實現,這里也說一下。比如:Entry[]的長度一定后,隨着map里面數據的越來越長,這樣同一個index的鏈就會很長,會不會影響性能?HashMap里面設置一個因子,隨着map的size越來越大,Entry[]會以一定的規則加長長度。
2)get
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); //先定位到數組元素,再遍歷該元素處的鏈表
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
3)null key的存取
null key總是存放在Entry[]數組的第一個元素。
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; } private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
4)確定數組index:hashcode % table.length取模
HashMap存取時,都需要計算當前key應該對應Entry[]數組哪個元素,即計算數組下標;算法如下:
/** * Returns index for hash code h. */
static int indexFor(int h, int length) { return h & (length-1); }
5)table初始大小
public HashMap(int initialCapacity, float loadFactor) { ..... // Find a power of 2 >= initialCapacity
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }
注意table初始大小並不是構造函數中的initialCapacity!!
而是 >= initialCapacity的2的n次冪!!!!
————為什么這么設計呢?——
3. 解決hash沖突的辦法
- 開放定址法(線性探測再散列,二次探測再散列,偽隨機探測再散列)
- 再哈希法
- 鏈地址法
- 建立一個公共溢出區
Java中hashmap的解決辦法就是采用的鏈地址法。
4. 再散列rehash過程
當哈希表的容量超過默認容量時,必須調整table的大小。當容量已經達到最大可能值時,那么該方法就將容量調整到Integer.MAX_VALUE返回,這時,需要創建一張新表,將原表的映射到新表中。
/** * Rehashes the contents of this map into a new array with a * larger capacity. This method is called automatically when the * number of keys in this map reaches its threshold. * * If current capacity is MAXIMUM_CAPACITY, this method does not * resize the map, but sets threshold to Integer.MAX_VALUE. * This has the effect of preventing future calls. * * @param newCapacity the new capacity, MUST be a power of two; * must be greater than current capacity unless current * capacity is MAXIMUM_CAPACITY (in which case value * is irrelevant). */
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } /** * Transfers all entries from current table to newTable. */
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; //重新計算index
int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
-
第二篇文章
1 數據結構:hash_map原理
這是一節讓你深入理解hash_map的介紹,如果你只是想囫圇吞棗,不想理解其原理,你倒是可以略過這一節,但我還是建議你看看,多了解一些沒有壞處。
hash_map基於hash table(哈希表)。哈希表最大的優點,就是把數據的存儲和查找消耗的時間大大降低,幾乎可以看成是常數時間;而代價僅僅是消耗比較多的內存。然而在當前可利用內存越來越多的情況下,用空間換時間的做法是值得的。另外,編碼比較容易也是它的特點之一。
其基本原理是:使用一個下標范圍比較大的數組來存儲元素。可以設計一個函數(哈希函數,也叫做散列函數),使得每個元素的關鍵字都與一個函數值(即數組下標,hash值)相對應,於是用這個數組單元來存儲這個元素;也可以簡單的理解為,按照關鍵字為每一個元素“分類”,然后將這個元素存儲在相應“類”所對應的地方,稱為桶。
但是,不能夠保證每個元素的關鍵字與函數值是一一對應的,因此極有可能出現對於不同的元素,卻計算出了相同的函數值,這樣就產生了“沖突”,換句話說,就是把不同的元素分在了相同的“類”之中。 總的來說,“直接定址”與“解決沖突”是哈希表的兩大特點。
hash_map,首先分配一大片內存,形成許多桶。是利用hash函數,對key進行映射到不同區域(桶)進行保存。其插入過程是:
1. 得到key
2. 通過hash函數得到hash值
3. 得到桶號(一般都為hash值對桶數求模)
4. 存放key和value在桶內。
其取值過程是:
1. 得到key
2. 通過hash函數得到hash值
3. 得到桶號(一般都為hash值對桶數求模)
4. 比較桶的內部元素是否與key相等,若都不相等,則沒有找到。
5. 取出相等的記錄的value。
hash_map中直接地址用hash函數生成,解決沖突,用比較函數解決。這里可以看出,如果每個桶內部只有一個元素,那么查找的時候只有一次比較。當許多桶內沒有值時,許多查詢就會更快了(指查不到的時候).
由此可見,要實現哈希表, 和用戶相關的是:hash函數(hashcode)和比較函數(equals)。

HashMap的工作原理是近年來常見的Java面試題。幾乎每個Java程序員都知道HashMap,都知道哪里要用HashMap,知道HashTable和HashMap之間的區別,那么為何這道面試題如此特殊呢?是因為這道題考察的深度很深。這題經常出現在高級或中高級面試中。投資銀行更喜歡問這個問題,甚至會要求你實現HashMap來考察你的編程能力。ConcurrentHashMap和其它同步集合的引入讓這道題變得更加復雜。讓我們開始探索的旅程吧!
先來些簡單的問題
“你用過HashMap嗎?” “什么是HashMap?你為什么用到它?”
幾乎每個人都會回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null鍵值和值,而HashTable則不能;HashMap是非synchronized;HashMap很快;以及HashMap儲存的是鍵值對等等。這顯示出你已經用過HashMap,而且對它相當的熟悉。但是面試官來個急轉直下,從此刻開始問出一些刁鑽的問題,關於HashMap的更多基礎的細節。面試官可能會問出下面的問題:
“你知道HashMap的工作原理嗎?” “你知道HashMap的get()方法的工作原理嗎?”
你也許會回答“我沒有詳查標准的Java API,你可以看看Java源代碼或者Open JDK。”“我可以用Google找到答案。”
但一些面試者可能可以給出答案,“HashMap是基於hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象。”這里關鍵點在於指出,HashMap是在bucket中儲存鍵對象和值對象,作為Map.Entry。這一點有助於理解獲取對象的邏輯。如果你沒有意識到這一點,或者錯誤的認為僅僅只在bucket中存儲值的話,你將不會回答如何從HashMap中獲取對象的邏輯。這個答案相當的正確,也顯示出面試者確實知道hashing以及HashMap的工作原理。但是這僅僅是故事的開始,當面試官加入一些Java程序員每天要碰到的實際場景的時候,錯誤的答案頻現。下個問題可能是關於HashMap中的碰撞探測(collision detection)以及碰撞的解決方法:
“當兩個對象的hashcode相同會發生什么?” 從這里開始,真正的困惑開始了,一些面試者會回答因為hashcode相同,所以兩個對象是相等的,HashMap將會拋出異常,或者不會存儲它們。然后面試官可能會提醒他們有equals()和hashCode()兩個方法,並告訴他們兩個對象就算hashcode相同,但是它們可能並不相等。一些面試者可能就此放棄,而另外一些還能繼續挺進,他們回答“因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發生。因為HashMap使用LinkedList存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在LinkedList中。”這個答案非常的合理,雖然有很多種處理碰撞的方法,這種方法是最簡單的,也正是HashMap的處理方法。但故事還沒有完結,面試官會繼續問:
“如果兩個鍵的hashcode相同,你如何獲取值對象?” 面試者會回答:當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,然后獲取值對象。面試官提醒他如果有兩個值對象儲存在同一個bucket,他給出答案:將會遍歷LinkedList直到找到值對象。面試官會問因為你並沒有值對象去比較,你是如何確定確定找到值對象的?除非面試者直到HashMap在LinkedList中存儲的是鍵值對,否則他們不可能回答出這一題。
其中一些記得這個重要知識點的面試者會說,找到bucket位置之后,會調用keys.equals()方法去找到LinkedList中正確的節點,最終找到要找的值對象。完美的答案!
許多情況下,面試者會在這個環節中出錯,因為他們混淆了hashCode()和equals()方法。因為在此之前hashCode()屢屢出現,而equals()方法僅僅在獲取值對象的時候才出現。一些優秀的開發者會指出使用不可變的、聲明作final的對象,並且采用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇。
如果你認為到這里已經完結了,那么聽到下面這個問題的時候,你會大吃一驚。“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦?”除非你真正知道HashMap的工作原理,否則你將回答不出這道題。默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因為它調用hash方法找到新的bucket位置。
如果你能夠回答這道問題,下面的問題來了:“你了解重新調整HashMap大小存在什么問題嗎?”你可能回答不上來,這時面試官會提醒你當多線程的情況下,可能產生條件競爭(race condition)。
當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在LinkedList中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在LinkedList的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那么就死循環了。這個時候,你可以質問面試官,為什么這么奇怪,要在多線程的環境下使用HashMap呢?:)
熱心的讀者貢獻了更多的關於HashMap的問題:
- 為什么String, Interger這樣的wrapper類適合作為鍵? String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那么就不能從HashMap中找到你想要的對象。不可變性還有其他的優點如線程安全。如果你可以僅僅通過將某個field聲明成final就能保證hashCode是不變的,那么請這么做吧。因為獲取對象的時候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個方法是非常重要的。如果兩個不相等的對象返回不同的hashcode的話,那么碰撞的幾率就會小些,這樣就能提高HashMap的性能。
- 我們可以使用自定義的對象作為鍵嗎? 這是前一個問題的延伸。當然你可能使用任何對象作為鍵,只要它遵守了equals()和hashCode()方法的定義規則,並且當對象插入到Map中之后將不會再改變了。如果這個自定義對象時不可變的,那么它已經滿足了作為鍵的條件,因為當它創建之后就已經不能改變了。
- 我們可以使用CocurrentHashMap來代替HashTable嗎?這是另外一個很熱門的面試題,因為ConcurrentHashMap越來越多人用了。我們知道HashTable是synchronized的,但是ConcurrentHashMap同步性能更好,因為它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的線程安全性。看看這篇博客查看Hashtable和ConcurrentHashMap的區別。
我個人很喜歡這個問題,因為這個問題的深度和廣度,也不直接的涉及到不同的概念。讓我們再來看看這些問題設計哪些知識點:
- hashing的概念
- HashMap中解決碰撞的方法
- equals()和hashCode()的應用,以及它們在HashMap中的重要性
- 不可變對象的好處
- HashMap多線程的條件競爭
- 重新調整HashMap的大小
總結
HashMap的工作原理
HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓后找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用LinkedList來解決碰撞問題,當發生碰撞了,對象將會儲存在LinkedList的下一個節點中。 HashMap在每個LinkedList節點中儲存鍵值對對象。
當兩個不同的鍵對象的hashcode相同時會發生什么? 它們會儲存在同一個bucket位置的LinkedList中。鍵對象的equals()方法用來找到鍵值對。
因為HashMap的好處非常多,我曾經在電子商務的應用中使用HashMap作為緩存。因為金融領域非常多的運用Java,也出於性能的考慮,我們會經常用到HashMap和ConcurrentHashMap。你可以查看更多的關於HashMap和HashTable的文章。
-
第三篇文章
hashmap本質數據加鏈表。根據key取得hash值,然后計算出數組下標,如果多個key對應到同一個下標,就用鏈表串起來,新插入的在前面。
看3段重要代碼摘要:
a:
public HashMap(int initialCapacity, float loadFactor) { int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }
有3個關鍵參數: capacity:容量,就是數組大小 loadFactor:比例,用於擴容 threshold:=capacity*loadFactor 最多容納的Entry數,如果當前元素個數多於這個就要擴容(capacity擴大為原來的2倍) b:
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
根據key算hash值,再根據hash值取得數組下標,通過數組下標取出鏈表,遍歷鏈表用equals取出對應key的value。
c:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
從數組(通過hash值)取得鏈表頭,然后通過equals比較key,如果相同,就覆蓋老的值,並返回老的值。(該key在hashmap中已存在)
否則新增一個entry,返回null。新增的元素為鏈表頭,以前相同數組位置的掛在后面。
另外:modCount是為了避免讀取一批數據時,在循環讀取的過程中發生了修改,就拋異常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
下面看添加一個map元素
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
新增后,如果發現size大於threshold了,就resize到原來的2倍
void resize(int newCapacity) { Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
新建一個數組,並將原來數據轉移過去
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
將原來數組中的鏈表一個個取出,然后遍歷鏈表中每個元素,重新計算index並放入新數組。每個處理的也放鏈表頭。
在取出原來數組鏈表后,將原來數組置空(為了大數據量復制時更快的被垃圾回收?)
還有兩點注意:
static class Entry<K,V> implements Map.Entry<K,V>是hashmap的靜態內部類,iterator之類的是內部類,因為不是每個元素都需要持有map的this指針。
HashMap把 transient Entry[] table;等變量置為transient,然后override了readObject和writeObject,自己實現序列化。
-
ConcurrentHashMap:
在hashMap的基礎上,ConcurrentHashMap將數據分為多個segment,默認16個(concurrency level),然后每次操作對一個segment加鎖,避免多線程鎖得幾率,提高並發效率。
public V get(Object key) { int hash = hash(key.hashCode()); return segmentFor(hash).get(key, hash); } final Segment<K,V> segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; }
in class Segment:
V get(Object key, int hash) { if (count != 0) { // read-volatile
HashEntry<K,V> e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); // recheck
} e = e.next; } } return null; }
/** * Reads value field of an entry under lock. Called if value * field ever appears to be null. This is possible only if a * compiler happens to reorder a HashEntry initialization with * its table assignment, which is legal under memory model * but is not known to ever occur. */ V readValueUnderLock(HashEntry<K,V> e) { lock(); try { return e.value; } finally { unlock(); } }
注意,這里在並發讀取時,除了key對應的value為null之外,並沒有使用鎖,如何做到沒有問題的呢,有以下3點:
1. HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
這里如果在讀取時數組大小(tab.length)發生變化,是會導致數據不對的,但transient volatile HashEntry<K,V>[] table;是volatile得,數組大小變化能立刻知道
2. static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
這里next是final的,就保證了一旦HashEntry取出來,整個鏈表就是正確的。
3.value是volatile的,保證了如果有put覆蓋,是可以立刻看到的。
public V put(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, false); } V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) // ensure capacity
rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) e.value = value; } else { oldValue = null; ++modCount; tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // write-volatile
} return oldValue; } finally { unlock(); } }
這里除了加鎖操作,其他和普通HashMap原理上無太大區別。
還有一點不理解的地方:
對於get和put/remove並發發生的時候,如果get的HashEntry<K,V> e = getFirst(hash);鏈表已經取出來了,這個時候put放入一個entry到鏈表頭,如果正好是需要取的key,是否還是會取不出來?
remove時,會先去除需要remove的key,然后把remove的key前面的元素一個個接到鏈表頭,同樣也存在remove后,以前的head到了中間,也會漏掉讀取的元素。
++modCount; HashEntry<K,V> newFirst = e.next; for (HashEntry<K,V> p = first; p != e; p = p.next) newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst; count = c; // write-volatile