HashMap和Hashtable理解與對比
一、概述
HashMap和Hashtable的區別在面試的時候經常會被問到,那麼它們有什麼區別呢?這裡談一下它們各自的特點以及它們的區別在哪裡。
二、HashMap
1、HashMap是鍵值對key-value形式雙列集合。它的底層儲存原理是雜湊表。為了簡明描述雜湊表(陣列+連結串列),我畫了一個圖(不專業,輕噴)。
2、對應HashMap採用雜湊表儲存鍵值對元素的方式, 配合著上圖做一些說明:
1)E*代表一個Node節點,每個Node節點就是我們理解的一個key-value的mapping對映。
2)每個Node除了儲存了key和value的對映外呢,還儲存了它下一Node的引用。例如圖中,Eb儲存了Ebb
的引用,而Ebb儲存了Ebbb的引用。
3)HashMap的put(key,value)方法介紹
每一個連結串列,如Eb-->Ebb-->Ebbb,這三個節點的key是不相等的。那麼你可能會問,為什麼它們三個會被放
在一個連結串列中呢?是這樣的。當你呼叫put(key,value)方法時,會根據key計算出一個hash值,然後再通
過這個hash值和map當前的長度計算出一個數值,然後將這個數值作為圖中陣列tab腳標而獲取這個腳標
對應的Node節點。如果這個節點不存在,則直接建立一個新的Node節點,插入到陣列中的你計算出的那個
腳標的位置。如果存在,則判斷key和你put進來的key是否相等(注意相等的判定:hash值相等且equal
相等),如果相等,那麼直接更新其值value,也就是我們常見的覆蓋舊value操作,如果舊value為null,則
直接將null值設定為你傳進來的value值,如果不相等(此處不介紹LinkedHashMap)則去以遍歷的方式尋
找這個節點的next節點,如果和這個連結串列的每個節點的next節點都不相等,則在連結串列的最後一個Node節點
後建立新節點。如果其中判定有一個相等,那麼進行覆蓋值並返回舊值操作。當然,這只是put方法的概要
解讀,更詳細的解讀需要自己看一下原始碼。
HashMap.put(key,value)方法的注意點:
(1)沒有synchronized關鍵字修飾,意味著它是非執行緒安全的。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
(2)key和value可以是null-null、null-value、key-null的形式。如果Map中不存在將要新增的元素那麼返
回值為null,如果已經存在且value不為null,那麼新value覆蓋舊value,返回舊value,如果舊value為null,
那麼將新value和key形成一個key-value對映,並返回null。key為null的時候,null也是一個鍵,鍵具有唯
一性,value可以重複。
3、HashMap的resize()方法理解
這個方法時HashMap容量不夠時進行擴容的方法,覺得有必要說一下。
當HashMap的容量不夠用時,再往容器中新增元素時,HashMap會進行擴容操作。當HashMap的容量為最大的
時,則不擴容,但是容器閾值會設定為Integer型別的最大值。當不是最大的時,容器會進行擴容,容量會變為
原來的二倍(此時也不大於最大容量),並且閾值也會隨之變化,變為原閾值的二倍。擴容就是在堆中新
建立一個HashMap容器,然後將原來就的HashMap中的元素放到新容器中,放置的時候會重新計算每個Node節
點在雜湊表中的位置。
4、HashMap的構造
我們常用的都是空參構造。空參構造一個HashMap,那麼它的構造方法使用的是預設的初始化容量16和
載入因子0.75。
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
當我們使用帶參構造:
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
此時,閾值(threshold )也不是簡單的容量*載入因子獲取,而是需要通過一個演算法,如下:
this.threshold = tableSizeFor(initialCapacity);
tableSizeFor(initialCapacity):
/**
* Returns a power of two size for the given target capacity.
* 根據給定的目標容量,返回一個2的整數倍的數(自己翻譯,水平有限,輕噴)。
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
這裡探討這個構造的本意在於,如果我們能預估出Map大概能儲存多個少鍵值對,那麼我們可以直接通過指
定容量這種帶參構造穿件Map例項,這樣可以避免Map的擴容造成的資源和效能浪費。對應載入因子0.75的
預設值,我們一般是不做修改的,這涉及到Map的存取效能問題。
HashMap是非執行緒安全的,是因為HashMap的方法都是沒有用synchronized關鍵字修飾的。
三、Hashtable
Hashtable已經被棄用的一個類,效能比較低,它有一些自己的特點,不知道你發現沒有,它不符合大小駝
峰命名規則,這點很討厭。
1、Hashtable的方法幾乎都是同步的,都有synchronized關鍵字修飾,因此和HashMap相比,它是執行緒安全的。
public synchronized V put(K key, V value) {...}
2、Hashtable中key-value的對映,key和value 都是不允許為null的,如果為null了呢?對不起,空指標異常丟擲。
3、Hashtable在計算節點元素在雜湊表中的位置使用的演算法稍有區別,它有它的好處,但和HashMap的演算法比
起來明顯效能低一些。
4、Hashtable的擴容是原來容量的二倍加1(2n+1),原始碼參考:int newCapacity = (oldCapacity << 1) + 1;
四、HashMap和Hashtable有哪些主要區別呢?
1、HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類。不過它們都實現了同時實現了map、Cloneable(可複製)、Serializable(可序列化)這三個介面。
2、Hashtable比HashMap多提供了elments() 和contains() 兩個方法。
3、HashMap的key-value支援key-value,null-null,key-null,null-value四種。而Hashtable只支援key-value一種(即
key和value都不為null這種形式)。既然HashMap支援帶有null的形式,那麼在HashMap中不能由get()方法來判斷
HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷,因為使用get的時候,當返回null時,你無法判斷到底
是不存在這個key,還是這個key就是null,還是key存在但value是null。
4、執行緒安全性不同,HashMap的方法都沒有使用synchronized關鍵字修飾,都是非執行緒安全的,而Hashtable的方法幾乎
都是被synchronized關鍵字修飾的。但是,當我們需要HashMap是執行緒安全的時,怎麼辦呢?我們可以通過Collections.synchronizedMap(hashMap)來進行處理,亦或者我們使用執行緒安全的ConcurrentHashMap。ConcurrentHashMap雖然也是執行緒安全的,但是它的效率比Hashtable要高好多倍。因為ConcurrentHashMap使用了分段鎖,並不對整個資料進行鎖定。
5、初始容量大小和每次擴充容量大小的不同
Hashtable預設的初始大小為11,之後每次擴充,容量變為原來的2n+1。HashMap預設的初始化大小為16。之後每次擴充,容量變為原來的2倍。
6、計算hash值的方法不同
為了得到元素的位置,首先需要根據元素的 KEY計算出一個hash值,然後再用這個hash值來計算得到最終的位置。
Hashtable直接使用物件的hashCode。hashCode是JDK根據物件的地址或者字串或者數字算出來的int型別的數值。然後再使用除留餘數發來獲得最終的位置。
Hashtable在計算元素的位置時需要進行一次除法運算,而除法運算是比較耗時的。
HashMap為了提高計算效率,將雜湊表的大小固定為了2的冪,這樣在取模預算時,不需要做除法,只需要做位運算。位運算比除法的效率要高很多。
HashMap的效率雖然提高了,但是hash衝突卻也增加了。因為它得出的hash值的低位相同的概率比較高,而計算位運算
為了解決這個問題,HashMap重新根據hashcode計算hash值後,又對hash值做了一些運算來打散資料。使得取得的位置更加分散,從而減少了hash衝突。當然了,為了高效,HashMap只做了一些簡單的位處理。從而不至於把使用2 的冪次方帶來的效率提升給抵消掉。
本文參考了以下內容:https://blog.csdn.net/wangxing233/article/details/79452946