带你深入浅出的分析 HashTable 源码
Hashtable 一個(gè)元老級(jí)的集合類,早在 JDK 1.0 就誕生了,今天小編想和大家一起來(lái)揭開(kāi)它的面紗!
01、摘要
在集合系列的第一章,咱們了解到,Map 的實(shí)現(xiàn)類有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、HashTable、Properties 等等。
本文主要從數(shù)據(jù)結(jié)構(gòu)和算法層面,探討 Hashtable 的實(shí)現(xiàn),如果有理解不當(dāng)之處,歡迎指正。
02、簡(jiǎn)介
“Hashtable 一個(gè)元老級(jí)的集合類,早在 JDK 1.0 就誕生了,而 HashMap 誕生于 JDK 1.2,在實(shí)現(xiàn)上,HashMap 吸收了很多 Hashtable 的思想,雖然二者的底層數(shù)據(jù)結(jié)構(gòu)都是 數(shù)組 + 鏈表 結(jié)構(gòu),具有查詢、插入、刪除快的特點(diǎn),但是二者又有很多的不同。
打開(kāi) Hashtable 的源碼可以看到,Hashtable 繼承自 Dictionary,而 HashMap 繼承自 AbstractMap。
public class Hashtable<K,V>extends Dictionary<K,V>implements Map<K,V>, Cloneable, java.io.Serializable {..... }HashMap 繼承自 AbstractMap,HashMap 類的定義如下:
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {..... }其中 Dictionary 類是一個(gè)已經(jīng)被廢棄的類,翻譯過(guò)來(lái)的意思是這個(gè)類已經(jīng)過(guò)時(shí),新的實(shí)現(xiàn)應(yīng)該實(shí)現(xiàn) Map 接口而不是擴(kuò)展此類,這一點(diǎn)我們可以從它代碼的注釋中可以看到:
/*** <strong>NOTE: This class is obsolete. New implementations should* implement the Map interface, rather than extending this class.</strong>*/ public abstract class Dictionary<K,V> {...... }Hashtable 和 HashMap 的底層是以數(shù)組來(lái)存儲(chǔ),同時(shí),在存儲(chǔ)數(shù)據(jù)通過(guò)key計(jì)算數(shù)組下標(biāo)的時(shí)候,是以哈希算法為主,因此可能會(huì)產(chǎn)生哈希沖突的可能性。
通俗的說(shuō)呢,就是不同的key,在計(jì)算的時(shí)候,可能會(huì)產(chǎn)生相同的數(shù)組下標(biāo),這個(gè)時(shí)候,如何將兩個(gè)對(duì)象放入一個(gè)數(shù)組中呢?
而解決哈希沖突的辦法,有兩種,一種開(kāi)放地址方式(當(dāng)發(fā)生 hash 沖突時(shí),就繼續(xù)以此繼續(xù)尋找,直到找到?jīng)]有沖突的hash值),另一種是拉鏈方式(將沖突的元素放入鏈表)。
Java Hashtable 采用的就是第二種方式,拉鏈法!
于是,當(dāng)發(fā)生不同的key通過(guò)一系列的哈希算法計(jì)算獲取到相同的數(shù)組下標(biāo)的時(shí)候,會(huì)將對(duì)象放入一個(gè)數(shù)組容器中,然后將對(duì)象以單向鏈表的形式存儲(chǔ)在同一個(gè)數(shù)組下標(biāo)容器中,就像鏈子一樣,掛在某個(gè)節(jié)點(diǎn)上,如下圖:
與 HashMap 類似,Hashtable 也包括五個(gè)成員變量:
/**由Entry對(duì)象組成的數(shù)組*/ private transient Entry[] table;/**Hashtable中Entry對(duì)象的個(gè)數(shù)*/ private transient int count;/**Hashtable進(jìn)行擴(kuò)容的閾值*/ private int threshold;/**負(fù)載因子,默認(rèn)0.75*/ private float loadFactor;/**記錄修改的次數(shù)*/ private transient int modCount = 0;具體各個(gè)變量含義如下:
table:表示一個(gè)由 Entry 對(duì)象組成的鏈表數(shù)組,Entry 是一個(gè)單向鏈表,哈希表的key-value鍵值對(duì)都是存儲(chǔ)在 Entry 數(shù)組中的;
count:表示 Hashtable 的大小,用于記錄保存的鍵值對(duì)的數(shù)量;
threshold:表示 Hashtable 的閾值,用于判斷是否需要調(diào)整 Hashtable 的容量,threshold 等于容量 * 加載因子;
loadFactor:表示負(fù)載因子,默認(rèn)為 0.75;
modCount:表示記錄 Hashtable 修改的次數(shù),用來(lái)實(shí)現(xiàn)快速失敗拋異常處理;
接著來(lái)看看Entry這個(gè)內(nèi)部類,Entry用于存儲(chǔ)鏈表數(shù)據(jù),實(shí)現(xiàn)了Map.Entry接口,本質(zhì)是就是一個(gè)映射(鍵值對(duì)),源碼如下:
private static class Entry<K,V> implements Map.Entry<K,V> {/**hash值*/final int hash;/**key表示鍵*/final K key;/**value表示值*/V value;/**節(jié)點(diǎn)下一個(gè)元素*/Entry<K,V> next;...... }我們?cè)俳又鴣?lái)看看 Hashtable 初始化過(guò)程,核心源碼如下:
public Hashtable() {this(11, 0.75f); }this 調(diào)用了自己的構(gòu)造方法,核心源碼如下:
public Hashtable(int initialCapacity, float loadFactor) {.....//默認(rèn)的初始大小為 11//并且計(jì)算擴(kuò)容的閾值this.loadFactor = loadFactor;table = new Entry<?,?>[initialCapacity];threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); }可以看到 HashTable 默認(rèn)的初始大小為 11,如果在初始化給定容量大小,那么 HashTable 會(huì)直接使用你給定的大小;
擴(kuò)容的閾值threshold等于initialCapacity * loadFactor,我們?cè)趤?lái)看看 HashTable 擴(kuò)容,方法如下:
protected void rehash() {int oldCapacity = table.length;//將舊數(shù)組長(zhǎng)度進(jìn)行位運(yùn)算,然后 +1//等同于每次擴(kuò)容為原來(lái)的 2n+1int newCapacity = (oldCapacity << 1) + 1;//省略部分代碼......Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; }可以看到,HashTable 每次擴(kuò)充為原來(lái)的 2n+1。
我們?cè)賮?lái)看看 HashMap,如果是執(zhí)行默認(rèn)構(gòu)造方法,會(huì)在擴(kuò)容那一步,進(jìn)行初始化大小,核心源碼如下:
final Node<K,V>[] resize() {int newCap = 0;//部分代碼省略......newCap = DEFAULT_INITIAL_CAPACITY;//默認(rèn)容量為 16Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; }可以看出 HashMap 的默認(rèn)初始化大小為 16,我們?cè)賮?lái)看看,HashMap 擴(kuò)容方法,核心源碼如下:
final Node<K,V>[] resize() {//獲取舊數(shù)組的長(zhǎng)度Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int newCap = 0;//部分代碼省略......//當(dāng)進(jìn)行擴(kuò)容的時(shí)候,容量為 2 的倍數(shù)newCap = oldCap << 1;Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; }可以看出 HashMap 的擴(kuò)容后的數(shù)組數(shù)量為原來(lái)的 2 倍;
也就是說(shuō) HashTable 會(huì)盡量使用素?cái)?shù)、奇數(shù)來(lái)做數(shù)組的容量,而 HashMap 則總是使用 2 的冪作為數(shù)組的容量。
我們知道當(dāng)哈希表的大小為素?cái)?shù)時(shí),簡(jiǎn)單的取模哈希的結(jié)果會(huì)更加均勻,所以單從這一點(diǎn)上看,HashTable 的哈希表大小選擇,似乎更高明些。
Hashtable 的 hash 算法,核心代碼如下:
//直接計(jì)算key.hashCode() int hash = key.hashCode();//通過(guò)除法取余計(jì)算數(shù)組存放下標(biāo) // 0x7FFFFFFF 是最大的 int 型數(shù)的二進(jìn)制表示 int index = (hash & 0x7FFFFFFF) % tab.length;從源碼部分可以看出,HashTable 的 key 不能為空,否則報(bào)空指針錯(cuò)誤!
但另一方面我們又知道,在取模計(jì)算時(shí),如果模數(shù)是 2 的冪,那么我們可以直接使用位運(yùn)算來(lái)得到結(jié)果,效率要大大高于做除法。所以在 hash 計(jì)算數(shù)組下標(biāo)的效率上,HashMap 卻更勝一籌,但是這也會(huì)引入了哈希分布不均勻的問(wèn)題, HashMap 為解決這問(wèn)題,又對(duì) hash 算法做了一些改動(dòng),具體我們來(lái)看看。
HashMap 的 hash 算法,核心代碼如下:
/**獲取hash值方法*/ static final int hash(Object key) {int h;// h = key.hashCode() 為第一步 取hashCode值(jdk1.7)// h ^ (h >>> 16) 為第二步 高位參與運(yùn)算(jdk1.7)return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//jdk1.8 }/**獲取數(shù)組下標(biāo)方法*/ static int indexFor(int h, int length) {//jdk1.7的源碼,jdk1.8沒(méi)有這個(gè)方法,但是實(shí)現(xiàn)原理一樣的return h & (length-1); //第三步 取模運(yùn)算 }HashMap 由于使用了2的冪次方,所以在取模運(yùn)算時(shí)不需要做除法,只需要位的與運(yùn)算就可以了。但是由于引入的 hash 沖突加劇問(wèn)題,HashMap 在調(diào)用了對(duì)象的 hashCode 方法之后,又做了一些高位運(yùn)算,也就是第二步方法,來(lái)打散數(shù)據(jù),讓哈希的結(jié)果更加均勻。
與此同時(shí),在 jdk1.8 中 HashMap 還引進(jìn)來(lái)紅黑樹(shù)實(shí)現(xiàn),當(dāng)沖突鏈表長(zhǎng)度大于 8 的時(shí)候,會(huì)將鏈表結(jié)構(gòu)改變成紅黑樹(shù)結(jié)構(gòu),讓查詢變得更快,具體實(shí)現(xiàn)可以參見(jiàn)《集合系列》中的 HashMap 分析。
03、常用方法介紹
3.1、put方法
“put 方法是將指定的 key, value 對(duì)添加到 map 里。
put 流程圖如下:
打開(kāi) HashTable 的 put 方法,源碼如下:
public synchronized V put(K key, V value) {//當(dāng) value 值為空的時(shí)候,拋異常!if (value == null) {throw new NullPointerException();}Entry<?,?> tab[] = table;//通過(guò)key 計(jì)算存儲(chǔ)下標(biāo)int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;//循環(huán)遍歷數(shù)組鏈表//如果有相同的key并且hash相同,進(jìn)行覆蓋處理Entry<K,V> entry = (Entry<K,V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}//加入數(shù)組鏈表中addEntry(hash, key, value, index);return null; }put 方法中的 addEntry 方法,源碼如下:
private void addEntry(int hash, K key, V value, int index) {//新增修改次數(shù)modCount++;Entry<?,?> tab[] = table;if (count >= threshold) {//數(shù)組容量大于擴(kuò)容閥值,進(jìn)行擴(kuò)容rehash();tab = table;//重新計(jì)算對(duì)象存儲(chǔ)下標(biāo)hash = key.hashCode();index = (hash & 0x7FFFFFFF) % tab.length;}//將對(duì)象存儲(chǔ)在數(shù)組中Entry<K,V> e = (Entry<K,V>) tab[index];tab[index] = new Entry<>(hash, key, value, e);count++; }addEntry 方法中的 rehash 方法,源碼如下:
protected void rehash() {int oldCapacity = table.length;Entry<?,?>[] oldMap = table;//每次擴(kuò)容為原來(lái)的 2n+1int newCapacity = (oldCapacity << 1) + 1;if (newCapacity - MAX_ARRAY_SIZE > 0) {if (oldCapacity == MAX_ARRAY_SIZE)//大于最大閥值,不再擴(kuò)容return;newCapacity = MAX_ARRAY_SIZE;}Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];modCount++;//重新計(jì)算擴(kuò)容閥值threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);table = newMap;//將舊數(shù)組中的數(shù)據(jù)復(fù)制到新數(shù)組中for (int i = oldCapacity ; i-- > 0 ;) {for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {Entry<K,V> e = old;old = old.next;int index = (e.hash & 0x7FFFFFFF) % newCapacity;e.next = (Entry<K,V>)newMap[index];newMap[index] = e;}} }總結(jié)流程如下:
1、通過(guò) key 計(jì)算對(duì)象存儲(chǔ)在數(shù)組中的下標(biāo);
2、如果鏈表中有 key,直接進(jìn)行新舊值覆蓋處理;
3、如果鏈表中沒(méi)有 key,判斷是否需要擴(kuò)容,如果需要擴(kuò)容,先擴(kuò)容,再插入數(shù)據(jù);
有一個(gè)值得注意的地方是 put 方法加了synchronized關(guān)鍵字,所以,在同步操作的時(shí)候,是線程安全的。
3.2、get方法
“get 方法根據(jù)指定的 key 值返回對(duì)應(yīng)的 value。
get 流程圖如下:
打開(kāi) HashTable 的 get 方法,源碼如下:
public synchronized V get(Object key) {Entry<?,?> tab[] = table;//通過(guò)key計(jì)算節(jié)點(diǎn)存儲(chǔ)下標(biāo)int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {if ((e.hash == hash) && e.key.equals(key)) {return (V)e.value;}}return null; }同樣,有一個(gè)值得注意的地方是 get 方法加了synchronized關(guān)鍵字,所以,在同步操作的時(shí)候,是線程安全的。
3.3、remove方法
“remove 的作用是通過(guò) key 刪除對(duì)應(yīng)的元素。
remove 流程圖如下:
打開(kāi) HashTable 的 remove 方法,源碼如下:
public synchronized V remove(Object key) {Entry<?,?> tab[] = table;//通過(guò)key計(jì)算節(jié)點(diǎn)存儲(chǔ)下標(biāo)int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;Entry<K,V> e = (Entry<K,V>)tab[index];//循環(huán)遍歷鏈表,通過(guò)hash和key判斷鍵是否存在//如果存在,直接將改節(jié)點(diǎn)設(shè)置為空,并從鏈表上移除for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {if ((e.hash == hash) && e.key.equals(key)) {modCount++;if (prev != null) {prev.next = e.next;} else {tab[index] = e.next;}count--;V oldValue = e.value;e.value = null;return oldValue;}}return null; }同樣,有一個(gè)值得注意的地方是 remove 方法加了synchronized關(guān)鍵字,所以,在同步操作的時(shí)候,是線程安全的。
04、總結(jié)
總結(jié)一下 Hashtable 與 HashMap 的聯(lián)系與區(qū)別,內(nèi)容如下:
1、雖然 HashMap 和 Hashtable 都實(shí)現(xiàn)了 Map 接口,但 Hashtable 繼承于 Dictionary 類,而 HashMap 是繼承于 AbstractMap;
2、HashMap 可以允許存在一個(gè)為 null 的 key 和任意個(gè)為 null 的 value,但是 HashTable 中的 key 和 value 都不允許為 null;
3、Hashtable 的方法是同步的,因?yàn)樵诜椒ㄉ霞恿?synchronized 同步鎖,而 HashMap 是非線程安全的;
盡管,Hashtable 雖然是線程安全的,但是我們一般不推薦使用它,因?yàn)橛斜人咝А⒏玫倪x擇 ConcurrentHashMap,在后面我們也會(huì)講到它。
最后,引入來(lái)自 HashTable 的注釋描述:
“If a thread-safe implementation is not needed, it is recommended to use HashMap in place of Hashtable. If a thread-safe highly-concurrent implementation is desired, then it is recommended to use java.util.concurrent.ConcurrentHashMap in place of Hashtable.
簡(jiǎn)單來(lái)說(shuō)就是,如果你不需要線程安全,那么使用 HashMap,如果需要線程安全,那么使用 ConcurrentHashMap。
HashTable 已經(jīng)被淘汰了,不要在新的代碼中再使用它。
05、參考
1、JDK1.7 & JDK1.8 源碼
2、博客園 - 程序員趙鑫 ?- HashMap 和 HashTable 到底哪不同??
【END】
關(guān)注下方二維碼,訂閱更多精彩內(nèi)容
總結(jié)
以上是生活随笔為你收集整理的带你深入浅出的分析 HashTable 源码的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 漫话:为什么计算机起始时间是1970年1
- 下一篇: 实战:RediSearch 高性能的全文