PS:整理的稍微有點急,不足之處,望各路道友指正,List相關可以查看前一篇隨筆!
HashMap的工作原理是近年來常見的Java面試題,幾乎每個Java程序員都知道HashMap,都知道哪里要用HashMap,知道HashTable和HashMap之間的區別,那么為何這道面試題如此特殊呢?是因為這道題考察的深度很深,關於HashMap的相關題目經常出現在java各層次(低級、中級、中高級或高級)面試中,甚至有些公司會要求你實現HashMap來考察你的編程能力。ConcurrentHashMap和其它同步集合的引入讓這道題變得更加復雜!說到HashMap,在這里首先借來一張圖來展現哈希表的結構:

看過了哈希表的結構之后,我們再回到HashMap,HashMap是基於哈希表實現的Map,所以我們首先要了解到,HashMap里面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,我們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map里面的內容都保存在Entry[]里面。好了,摘來一些前面的話,接下來我們來回到HashMap面試題的這個問題上來!
1、“你用過HashMap嗎?”
答:在這里,相信幾乎所有人的回答都會是 yes!
2、 “HashMap的數據結構?”“什么是HashMap?你為什么用到它?”
答:查看百科,我們得知,HashMap是基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同)此類不保證映射的順序,特別是它不保證該順序恆久不變!好,說到這,那么你為何使用HashMap呢,相信大多數人在這里都會回答HashMap的一些特性,諸如上面提到的HashMap可以允許null鍵值和value(這里切記,HashMap的key為null的情況只能出現一個,而value為null可以有多個),而hashtable不能;HashMap是非synchronized(非線程安全);HashMap很快;以及HashMap存儲的是鍵值對等等(hashtable線程安全)。是的,回答上面這些關於HashMap和hashtable之間的區別就基本上夠了,為什么用到,無外乎就是因為他提供了一些hashtable所沒有的而已,好的,到這里,已經能夠顯示出你已經用過HashMap,而且對它相當的熟悉了。那么好了,說了這么多,HashMap的數據結構又是怎樣的呢?相信不少人對這個問題並沒有深入的了解,要知道,在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造,HashMap也不例外。Hashmap實際上是一個數組和鏈表的結合體(在數據結構中,一般稱之為“鏈表散列“),請看下圖(橫排表示數組,縱排表示數組元素【實際上是一個鏈表】)。

當我們往HashMap中put元素的時候,先根據key的hash值得到這個元素在數組中的位置(即下標),然后就可以把這個元素放到對應的位置中了。如果這個元素所在的位子上已經存放有其他元素了,那么在同一個位子上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。從HashMap中get元素時,首先計算key的hashcode,找到數組中對應位置的某一元素,然后通過key的equals方法在對應位置的鏈表中找到需要的元素。從這里我們可以想象得到,如果每個位置上的鏈表只有一個元素,那么HashMap的get效率將是最高的。。。。
3、“你知道HashMap的工作原理嗎?” “你知道HashMap的get()方法的工作原理嗎?”
答:其實就這個問題,在上一題中已經有了簡單的涉獵,好了,這里,我們大致可以做出這樣的回答,HashMap是基於hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象(value)。當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象。這里關鍵點在於指出,HashMap是在bucket中儲存鍵對象和值對象,作為Map.Entry。嗯,如此回答,基本上算得上相當正確了,也顯示出面試者確實知道hashing以及HashMap的工作原理,那么接下來我們還需要知道的是HashMap與別的Map之間的區別以及一些涉及場景的問題了!
嗯,既然說到HashMap的get()方法,那么我在這里順帶就拿來put()和get()方法的源代碼分析分析:
首先看看put()方法的實現,
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);
}
再看看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;
}
上面的兩個方法都是常規的時候,相信用過HashMap的小伙伴都知道HashMap可以存儲null鍵值對,我們知道,null key總是存放在Entry[]數組第一個元素,好,接下來我們來看看,HashMap中是如何存取null鍵值對的:
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;
}
根據源代碼我可知,在HashMap中,null可以作為鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值為null。當get()方法返回null值時,即可以表示HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。
4、“HashMap Hashtable LinkedHashMap 和TreeMap?”
答:首先我們知道,java為數據結構中的映射定義了一個接口java.util.Map;它有四個實現類,分別是HashMap Hashtable LinkedHashMap 和TreeMap.
Map主要用於存儲健值對,根據鍵得到值,因此不允許鍵重復(重復了覆蓋了),但允許值重復!
Hashmap 是一個最常用的Map,它根據鍵的HashCode值存儲數據,根據鍵可以直接獲取它的值,具有很快的訪問速度,遍歷時,取得數據的順序是完全隨機的。 HashMap最多只允許一條記錄的鍵為Null,允許多條記錄的值為 Null;HashMap不支持線程的同步,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
Hashtable與 HashMap類似,它繼承自Dictionary類,不同的是:它不允許記錄的鍵或者值為空;它支持線程的同步,即任一時刻只有一個線程能寫Hashtable,因此也導致了 Hashtable在寫入時會比較慢。(主要區別就是以上兩點是相反的,HashMap進一步改進了)
LinkedHashMap 是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的。也可以在構造時用帶參數,按照應用次數排序。在遍歷的時候會比HashMap慢,不過有種情況例外,當HashMap容量很大,實際數據較少時,遍歷起來可能會比 LinkedHashMap慢,因為LinkedHashMap的遍歷速度只和實際數據有關,和容量無關,而HashMap的遍歷速度和他的容量有關。
TreeMap實現了SortMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator 遍歷TreeMap時,得到的記錄是排過序的。
一般情況下,我們用的最多的是HashMap,在Map 中插入、刪除和定位元素,HashMap 是最好的選擇。但如果您要按自然順序或自定義順序遍歷鍵,那么TreeMap會更好。如果需要輸出的順序和輸入的相同,那么用LinkedHashMap 可以實現,它還可以按讀取順序來排列。
4、“SynchronizedMap和ConcurrentHashMap的區別?”
答:在上面我們已經提到過,HashMap和hashtable之間的一大區別就是是否線程安全,而在上一題也說到過,如果HashMap需要同步,可以使用Collections類中提供的SynchronizedMap方法使HashMap具有同步的能力,或者也可以使用ConcurrentHashMap方法,好,既然我們知道可以用這兩個方法實現,那么我們也應該了解到這兩個方法的區別所在:
首先來看下java中Collections工具類中的SynchronizedMap方法:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<K,V>(m);
}
該方法返回的是一個SynchronizedMap的實例。SynchronizedMap類是定義在Collections中的一個靜態內部類,它實現了Map接口,並對其中的每一個方法實現,通過synchronized關鍵字進行了同步控制。(PS:hashtable容器就是使用的Synchronized方法進行同步控制來保證線程安全的,效率十分低下)
顯而易見,在這個類中,需要對每個方法進行同步控制,當需要迭代時,這種操作效率無疑是十分低下的,所以我們不得不考慮別的方法了,於是就有了更好的選擇 ConcurrentHashMap,對於這個具體不贅述,總結如下。
Collections.synchronizedMap()與ConcurrentHashMap主要區別是:Collections.synchronizedMap()和Hashtable一樣,實現上在調用map所有方法時,都對整個map進行同步,而ConcurrentHashMap的實現卻更加精細,它對map中的所有桶加了鎖。所以,只要要有一個線程訪問map,其他線程就無法進入map,而如果一個線程在訪問ConcurrentHashMap某個桶時,其他線程,仍然可以對map執行某些操作。這樣,ConcurrentHashMap在性能以及安全性方面,明顯比Collections.synchronizedMap()更加有優勢。另外,ConcurrentHashMap必然是個HashMap,而Collections.synchronizedMap()可以接收任意Map實例,實現Map的同步。
4、“在HashMap中,當兩個不同的鍵對象的hashcode相同會發生什么?”
答:這里我們首先要知道的是,HashMap中有hashcode()和equals()兩個方法,所以兩個對象就算hashcode相同,但是它們可能並不相等,在HashMap的處理中,因為hashcode相同,所以它們的bucket位置相同,‘碰撞’就會發生。因為HashMap使用LinkedList存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在LinkedList中。【這里提供一個標准的回答,當兩個不同的鍵對象的hashcode相同時,它們會儲存在同一個bucket位置的LinkedList中。鍵對象的equals()方法可以用來找到鍵值對!】接着這個問題,面試官可能還會更一步問下去,“如果兩個鍵的hashcode相同,你如何獲取值對象?” 這里,我們可以嘗試這樣回答:當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,然后獲取值對象。(面試官提醒他如果有兩個值對象儲存在同一個bucket),我們可以補充回答,在找到bucket位置之后,會調用keys.equals()方法去找到鏈表(LinkedList)中正確的節點,將會遍歷鏈表直到找到值對象,最終找到要找的值對象!
5、“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦?”
答:首先看看java定義負載因子:
static final float DEFAULT_LOAD_FACTOR = 0.75F;
可以看出,默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因為它調用hash方法找到新的bucket位置。
6、“你了解重新調整HashMap大小存在什么問題嗎?”
答:(當多線程的情況下,可能產生條件競爭(race condition))
當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那么就死循環了。這個時候,你可以質問面試官,為什么這么奇怪,要在多線程的環境下使用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提供更強的線程安全性。
能否讓HashMap同步?HashMap可以通過下面的語句進行同步:
Map m = Collections.synchronizeMap(hashMap);
結束語:關於HashMap的問題當然不是這么一篇小小的隨筆和匯總能夠說清楚的,更多的相關知識還需要我們不停的在實踐中使用,去比較才能發現,這里關於List和Set並沒有做過多的涉獵,因為在之前面試的總結中有基本內容的涉及,大家可以稍微借鑒,當然如果要閱讀源代碼的同道可以自己查看相關源代碼即可了,這里最后對HashMap的工作原理稍微做個簡單的總結:
HashMap是基於hashing原理的,它是一種數組和鏈表的結合體,在實現對象的存取時,我們通過put()和get()方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,然后找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用LinkedList來解決碰撞問題,當發生碰撞了,對象將會儲存在LinkedList的下一個節點中。 HashMap在每個LinkedList節點中儲存鍵值對對象。
在最后再補充一個問題:
什么是hash,什么是碰撞,什么是equals ?
Hash:是一種信息摘要算法,它還叫做哈希,或者散列。我們平時使用的MD5,SHA1都屬於Hash算法,通過輸入key進行Hash計算,就可以獲取key的HashCode(),比如我們通過校驗MD5來驗證文件的完整性。
碰撞:好的Hash算法可以出計算幾乎出獨一無二的HashCode,如果出現了重復的hashCode,就稱作碰撞;就算是MD5這樣優秀的算法也會發生碰撞,即兩個不同的key也有可能生成相同的MD5。
HashCode,它是一個本地方法,實質就是地址取樣運算;
==是用於比較指針是否在同一個地址;
equals與==是相同的。
如何減少碰撞?
使用不可變的、聲明作final的對象,並且采用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇
-by 小仇哥 整理於23:51:28
