Java的HashTable源碼解讀
一、介紹
在往期文章中,我們從源碼分析了java集合框架中Map一族的HashMap,它為我們提供了保存 <key, value>鍵值對(duì)的一系列方法,底層是基于哈希表+鏈表+紅黑樹實(shí)現(xiàn)的,但是在多線程并發(fā)的環(huán)境下,表現(xiàn)出線程不安全的特性,今天我們介紹另一個(gè)同樣是保存 <key, value>鍵值對(duì)但底層是基于哈希表+鏈表 且 線程安全的HashTable。
下面看一下HashTable的UML圖
二、類的聲明
我們來看一下HashMap的聲明,可以大致了解他的功能。
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
- 繼承了Dictionary類,提供了一些字典相關(guān)的基本功能如添加、刪除、判空、獲取元素?cái)?shù)量等。
- 實(shí)現(xiàn)了Map接口,說明HashMap是一個(gè)以<K,V>鍵值對(duì)存儲(chǔ)數(shù)據(jù)的結(jié)構(gòu)
- 實(shí)現(xiàn)了Cloneable接口,提供了對(duì)象克隆方法,但請(qǐng)注意,是淺克隆。
- 實(shí)現(xiàn)了Serializable接口,支持序列化。
Dictionary類是我們?cè)趈ava集合框架學(xué)習(xí)過程中首次見到的類,我們看一下他是什么
從下面源碼中可以看到,Dictionary是一個(gè)抽象類,該類中聲明的方法全是抽象方法,沒有默認(rèn)實(shí)現(xiàn),且這些抽象方法在Map接口中都有體現(xiàn),因此我們可以忽略Dictionary類,將注意點(diǎn)都放在HashTable本身。
另外還需要注意到一個(gè)事實(shí),在Dictionary的文檔中我們看到,jdk已經(jīng)建議我們忽略它,并且將用Map接口代替它。
/** * NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. * **/ public abstract class Dictionary<K,V> { public Dictionary() { } abstract public int size(); abstract public boolean isEmpty(); abstract public Enumeration<K> keys(); abstract public Enumeration<V> elements(); abstract public V get(Object key); abstract public V put(K key, V value); abstract public V remove(Object key); }
三、底層實(shí)現(xiàn)
HashTable的底層實(shí)現(xiàn)為哈希表+鏈表,相較于底層為哈希表+鏈表+紅黑樹實(shí)現(xiàn)的HashMap,少了紅黑樹的結(jié)構(gòu),因此并沒有那么復(fù)雜,如下圖所示
四、內(nèi)部類Entry
在HashTable中,將鍵值對(duì)封裝成節(jié)點(diǎn)的類為其內(nèi)部類Entry,該內(nèi)部類繼承于Map接口的內(nèi)部接口Entry,從上面HashTable的UML圖中也有所體現(xiàn)。我們看一下該內(nèi)部類的源碼:
private static class Entry<K,V> implements Map.Entry<K,V> { // 鍵值對(duì)節(jié)點(diǎn)中key的哈希值 final int hash; // 鍵值對(duì)節(jié)點(diǎn)中的key final K key; // 鍵值對(duì)節(jié)點(diǎn)中的value V value; // 鏈表中當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn) Entry<K,V> next; protected Entry(int hash, K key, V value, Entry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } }
從源碼可以看出,HashTable中保存鍵值對(duì)的節(jié)點(diǎn)類Entry其實(shí)與HashMap中保存鍵值對(duì)的節(jié)點(diǎn)類Node是完全相同的。
五、成員變量
// 哈希表中的數(shù)組 private transient Entry<?,?>[] table; // 哈希表中鍵值對(duì)節(jié)點(diǎn)數(shù)量 private transient int count; // 擴(kuò)容閾值 private int threshold; // 加載因子,默認(rèn)0.75 private float loadFactor; // 結(jié)構(gòu)性修改次數(shù),用于快速失敗 private transient int modCount = 0;
六、構(gòu)造方法
HashMap提供了以下四個(gè)構(gòu)造方法來創(chuàng)建實(shí)例
無參構(gòu)造
通過默認(rèn)初始容量11 和 默認(rèn)加載因子為0.75 構(gòu)造實(shí)例
public Hashtable() { this(11, 0.75f); }
指定初始容量和加載因子
對(duì)指定的初始容量和加載因子進(jìn)行校驗(yàn)后,設(shè)置初始容量大小的數(shù)組和加載因子,并計(jì)算出擴(kuò)容閾值。
還記得在HashMap中,數(shù)組和擴(kuò)容閾值都是在第一次擴(kuò)容時(shí)初始化的。而在HashTable中,取消了這種延時(shí)初始化。
public Hashtable(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: "+loadFactor); if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; table = new Entry<?,?>[initialCapacity]; threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); }
指定初始容量
指定初始容量,并使用默認(rèn)的加載因子0.75。
public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); }
通過傳入一個(gè)Map對(duì)象實(shí)例化
通過這種方式構(gòu)造HashTable實(shí)例時(shí),會(huì)先創(chuàng)建實(shí)例,再調(diào)用putAll()方法將map中的數(shù)據(jù)批量保存到實(shí)例中。
注意:在構(gòu)造HashTable實(shí)例時(shí),初始容量為 map容量*2+1 和 默認(rèn)初始容量11的較小值,
public Hashtable(Map<? extends K, ? extends V> t) { this(Math.max(2*t.size(), 11), 0.75f); putAll(t); }
七、擴(kuò)容方法rehash()
該方法的源碼如下,相較于HashMap那么長的擴(kuò)容resize()方法,是否簡單了一點(diǎn)呢?在HashMap的擴(kuò)容resize()方法中,還要一堆判斷去確定數(shù)組容量、擴(kuò)容閾值等信息,而且由于HashMap中數(shù)組長度為2的n次方,還需要判斷哪些鍵值對(duì)在擴(kuò)容前后的數(shù)組下標(biāo)是否不變等等。
而在HashTable中,由于在擴(kuò)容前哈希表就已經(jīng)完成初始化了,且由于哈希表數(shù)組長度可能為任意值,也不存在擴(kuò)容后鍵值對(duì)位于數(shù)組的下標(biāo)不變的情況,因此,簡單粗暴的,直接擴(kuò)容,然后將原哈希表中的鍵值對(duì)重新計(jì)算數(shù)組下標(biāo),放在擴(kuò)容后的哈希表中。
要注意一點(diǎn):在HashMap中,原哈希表中鏈表上的元素采用尾插法放在新哈希表。而在HashTable中采用的是頭插法。
protected void rehash() { // 擴(kuò)容前的哈希數(shù)組容量 int oldCapacity = table.length; // 擴(kuò)容前的哈希數(shù)組 Entry<?,?>[] oldMap = table; // 1.計(jì)算擴(kuò)容后的容量,新容量= 原容量*2 +1 int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; } Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; // 2. 遍歷原哈希表,將原哈希表中的鍵值對(duì)重新計(jì)算數(shù)組下標(biāo)后,通過頭插法,放至新的哈希表中。 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; // 重新計(jì)算數(shù)組下標(biāo) int index = (e.hash & 0x7FFFFFFF) % newCapacity; // 頭插法 e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } } }
八、addEntry()
該方法采用頭插法的方式向哈希表中添加元素,在添加元素前,判斷是否需要先調(diào)用rehash()方法進(jìn)行擴(kuò)容。
private void addEntry(int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; if (count >= threshold) { // 擴(kuò)容 rehash(); tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. @SuppressWarnings("unchecked") // 頭插法 Entry<K,V> e = (Entry<K,V>) tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; }
有一點(diǎn)需要注意,HashTable不像HashMap那樣對(duì)hashcode()值的高16位和低16位按位與得到hash值,再對(duì)數(shù)組長度取余,從而得到數(shù)組下標(biāo),而是直接通過** hashcode()值與 0x7FFFFFFF 按位與,在對(duì)數(shù)組長度取余,從而得到數(shù)組下標(biāo)**。
我們分析一下index = (hash & 0x7FFFFFFF) % tab.length
0x7FFFFFFF為16進(jìn)制表示法,換成2進(jìn)制為:0111 1111 1111 1111 1111 1111 1111 1111共32位,其中最高位為0,而我們知道,在二進(jìn)制表示中,最高位為0表示正數(shù),1表示負(fù)數(shù),因此(hash & 0x7FFFFFFF)表示將key的hashcode()值的最高位設(shè)置為0,解決了key的hashcode()值為負(fù)數(shù)的情況,再對(duì)數(shù)組長度取余,便可得到數(shù)組下標(biāo)。
九、常用方法
size()
獲取哈希表中鍵值對(duì)節(jié)點(diǎn)的數(shù)量。該方法被synchronized修飾,表示線程安全
public synchronized int size() { return count; }
isEmpty()
判斷哈希表中是否不存在鍵值對(duì),該方法被synchronized修飾,表示線程安全
public synchronized boolean isEmpty() { return count == 0; }
contains() 與 containsValue()
判斷哈希表中是否存在指定的value,遍歷方式與HashMap相同,該方法被synchronized修飾,表示線程安全
從方法首行的判空語句可知,HashTable中不允許value值為空
public synchronized boolean contains(Object value) { if (value == null) { throw new NullPointerException(); } Entry<?,?> tab[] = table; for (int i = tab.length ; i-- > 0 ;) { for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) { if (e.value.equals(value)) { return true; } } } return false; } public boolean containsValue(Object value) { return contains(value); }
containsKey()
判斷哈希表中是否存在指定的key,遍歷方式與HashMap相同,該方法被synchronized修飾,表示線程安全
從方法第二行int hash = key.hashCode();來看,HashTable中不允許key值為空,否則會(huì)拋出空指針異常。
邏輯與HashMap相同,都是先通過計(jì)算哈希值確認(rèn)哈希表中數(shù)組的下標(biāo),再通過遍歷鏈表的形式,查找是否存在鍵值對(duì)的key與指定的key相同。
public synchronized boolean containsKey(Object key) { Entry<?,?> tab[] = table; 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 true; } } return false; }
get()
根據(jù)指定的key,從哈希表中獲取該key對(duì)應(yīng)的value,遍歷方式與HashMap相同,該方法被synchronized修飾,表示線程安全
public synchronized V get(Object key) { Entry<?,?> tab[] = table; // 計(jì)算哈希值確認(rèn)哈希表中數(shù)組的下標(biāo) int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; // 遍歷鏈表,查找到相同的key的鍵值對(duì),并返回該鍵值對(duì)的value for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; }
put()
將指定的 <key, value> 鍵值對(duì)保存,該方法被synchronized修飾,表示線程安全
從方法首行的判空語句可知,HashTable中不允許value值為空。
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; // 根據(jù)參數(shù)key計(jì)算出對(duì)應(yīng)的數(shù)組下標(biāo) int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; // 對(duì)該數(shù)組下標(biāo)上的鏈表進(jìn)行遍歷,如果存在key相等的鍵值對(duì),則對(duì)其對(duì)應(yīng)的value值進(jìn)行覆蓋 for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; // 覆蓋value entry.value = value; return old; } } // 如果不存在key相等的鍵值對(duì),則調(diào)用addEntry()方法通過頭插法保存鍵值對(duì) addEntry(hash, key, value, index); return null; }
remove()
public synchronized V remove(Object key) { Entry<?,?> tab[] = table; // 計(jì)算數(shù)組下標(biāo) int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") // 遍歷該數(shù)組下標(biāo)上的鏈表 Entry<K,V> e = (Entry<K,V>)tab[index]; 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; }
十、總結(jié)
- 底層結(jié)構(gòu)為數(shù)組+單鏈表實(shí)現(xiàn)的哈希表
- 當(dāng)出現(xiàn)哈希沖突或擴(kuò)容,且當(dāng)前位置為鏈表時(shí),采用尾插法將節(jié)點(diǎn)插入到鏈表
- 插入的數(shù)據(jù)是無序的
- 線程安全
- 保存鍵值對(duì)節(jié)點(diǎn)的類為Entry,而HashMap中保存鍵值對(duì)節(jié)點(diǎn)的類為Node
- 哈希表數(shù)組默認(rèn)初始容量為11,默認(rèn)加載因子為0.75
- 哈希表數(shù)組的容量可任意指定,擴(kuò)容后的容量 為 擴(kuò)容前容量*2 + 1
- 哈希表和擴(kuò)容閾值在構(gòu)造方法中完成初始化,是即時(shí)加載而不是延時(shí)初始化
- key和value都不允許為空,在HashMap中為key和value都允許為空。
- HashTable的擴(kuò)容方法為rehash(),HashMap的擴(kuò)容方法為resize()
- 先擴(kuò)容,后插入。在HashMap中為先插入后擴(kuò)容。
到此這篇關(guān)于Java的HashTable源碼解讀的文章就介紹到這了,更多相關(guān)HashTable源碼解讀內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot產(chǎn)生環(huán)形注入的解決方案
這篇文章主要介紹了Spring Boot產(chǎn)生環(huán)形注入的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09詳解IDEA 啟動(dòng)tomcat 端口占用原因以及解決方法( 使用debug模式)
這篇文章主要介紹了詳解IDEA 啟動(dòng)tomcat 端口占用原因以及解決方法( 使用debug模式) ,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08MyBatis加載映射文件和動(dòng)態(tài)代理的實(shí)現(xiàn)
本文主要介紹了MyBatis加載映射文件和動(dòng)態(tài)代理的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05Java?Servlet響應(yīng)httpServletResponse過程詳解
HttpServletResponse是處理http響應(yīng)的對(duì)象,調(diào)用該對(duì)象的方法,設(shè)置到對(duì)象屬性的內(nèi)容,tomcat最終會(huì)組織為http響應(yīng)報(bào)文2022-02-02Java?Stream如何將List分組成Map或LinkedHashMap
這篇文章主要給大家介紹了關(guān)于Java?Stream如何將List分組成Map或LinkedHashMap的相關(guān)資料,stream流是Java8的新特性,極大簡化了集合的處理操作,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12java密鑰交換算法DH定義與應(yīng)用實(shí)例分析
這篇文章主要介紹了java密鑰交換算法DH定義與應(yīng)用,結(jié)合實(shí)例形式分析了Java密鑰交換算法DH的原理、定義、使用方法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2019-09-09Java?JWT實(shí)現(xiàn)跨域身份驗(yàn)證方法詳解
JWT(JSON?Web?Token)是目前流行的跨域認(rèn)證解決方案,是一個(gè)開放標(biāo)準(zhǔn)(RFC?7519),它定義了一種緊湊的、自包含的方式,用于作為JSON對(duì)象在各方之間安全地傳輸信息。本文將介紹JWT如何實(shí)現(xiàn)跨域身份驗(yàn)證,感興趣的可以學(xué)習(xí)一下2022-01-01