做者:炸雞可樂 原文出處:www.pzblog.cnhtml
在集合系列的第一章,我們瞭解到,Map 的實現類有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties 等等。java
本文主要從數據結構和算法層面,探討 Hashtable 的實現,若是有理解不當之處,歡迎指正。程序員
Hashtable 一個元老級的集合類,早在 JDK 1.0 就誕生了,而 HashMap 誕生於 JDK 1.2,在實現上,HashMap 吸取了不少 Hashtable 的思想,雖然兩者的底層數據結構都是 數組 + 鏈表 結構,具備查詢、插入、刪除快的特色,可是兩者又有不少的不一樣。算法
打開 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 類是一個已經被廢棄的類,翻譯過來的意思是這個類已通過時,新的實現應該實現 Map 接口而不是擴展此類,這一點咱們能夠從它代碼的註釋中能夠看到:數據結構
/** * <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 的底層是以數組來存儲,同時,在存儲數據經過key
計算數組下標的時候,是以哈希算法爲主,所以可能會產生哈希衝突的可能性。數據結構和算法
通俗的說呢,就是不一樣的key
,在計算的時候,可能會產生相同的數組下標,這個時候,如何將兩個對象放入一個數組中呢?this
而解決哈希衝突的辦法,有兩種,一種開放地址方式(當發生 hash 衝突時,就繼續以此繼續尋找,直到找到沒有衝突的hash值),另外一種是拉鍊方式(將衝突的元素放入鏈表)。spa
Java Hashtable 採用的就是第二種方式,拉鍊法!
因而,當發生不一樣的key
經過一系列的哈希算法計算獲取到相同的數組下標的時候,會將對象放入一個數組容器中,而後將對象以單向鏈表
的形式存儲在同一個數組下標容器中,就像鏈子同樣,掛在某個節點上,以下圖:
與 HashMap 相似,Hashtable 也包括五個成員變量:
/**由Entry對象組成的數組*/ private transient Entry[] table; /**Hashtable中Entry對象的個數*/ private transient int count; /**Hashtable進行擴容的閾值*/ private int threshold; /**負載因子,默認0.75*/ private float loadFactor; /**記錄修改的次數*/ private transient int modCount = 0;
具體各個變量含義以下:
key-value
鍵值對都是存儲在 Entry 數組中的;容量 * 加載因子
;接着來看看Entry
這個內部類,Entry
用於存儲鏈表數據,實現了Map.Entry
接口,本質是就是一個映射(鍵值對),源碼以下:
private static class Entry<K,V> implements Map.Entry<K,V> { /**hash值*/ final int hash; /**key表示鍵*/ final K key; /**value表示值*/ V value; /**節點下一個元素*/ Entry<K,V> next; ...... }
咱們再接着來看看 Hashtable 初始化過程,核心源碼以下:
public Hashtable() { this(11, 0.75f); }
this 調用了本身的構造方法,核心源碼以下:
public Hashtable(int initialCapacity, float loadFactor) { ..... //默認的初始大小爲 11 //而且計算擴容的閾值 this.loadFactor = loadFactor; table = new Entry<?,?>[initialCapacity]; threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); }
能夠看到 HashTable 默認的初始大小爲 11,若是在初始化給定容量大小,那麼 HashTable 會直接使用你給定的大小;
擴容的閾值threshold
等於initialCapacity * loadFactor
,咱們在來看看 HashTable 擴容,方法以下:
protected void rehash() { int oldCapacity = table.length; //將舊數組長度進行位運算,而後 +1 //等同於每次擴容爲原來的 2n+1 int newCapacity = (oldCapacity << 1) + 1; //省略部分代碼...... Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; }
能夠看到,HashTable 每次擴充爲原來的 2n+1。
咱們再來看看 HashMap,若是是執行默認構造方法,會在擴容那一步,進行初始化大小,核心源碼以下:
final Node<K,V>[] resize() { int newCap = 0; //部分代碼省略...... newCap = DEFAULT_INITIAL_CAPACITY;//默認容量爲 16 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; }
能夠看出 HashMap 的默認初始化大小爲 16,咱們再來看看,HashMap 擴容方法,核心源碼以下:
final Node<K,V>[] resize() { //獲取舊數組的長度 Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int newCap = 0; //部分代碼省略...... //當進行擴容的時候,容量爲 2 的倍數 newCap = oldCap << 1; Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; }
能夠看出 HashMap 的擴容後的數組數量爲原來的 2 倍;
也就是說 HashTable 會盡可能使用素數、奇數來作數組的容量,而 HashMap 則老是使用 2 的冪做爲數組的容量。
咱們知道當哈希表的大小爲素數時,簡單的取模哈希的結果會更加均勻,因此單從這一點上看,HashTable 的哈希表大小選擇,彷佛更高明些。
Hashtable 的 hash 算法,核心代碼以下:
//直接計算key.hashCode() int hash = key.hashCode(); //經過除法取餘計算數組存放下標 // 0x7FFFFFFF 是最大的 int 型數的二進制表示 int index = (hash & 0x7FFFFFFF) % tab.length;
從源碼部分能夠看出,HashTable 的 key 不能爲空,不然報空指針錯誤!
但另外一方面咱們又知道,在取模計算時,若是模數是 2 的冪,那麼咱們能夠直接使用位運算來獲得結果,效率要大大高於作除法。因此在 hash 計算數組下標的效率上,HashMap 卻更勝一籌,可是這也會引入了哈希分佈不均勻的問題, HashMap 爲解決這問題,又對 hash 算法作了一些改動,具體咱們來看看。
HashMap 的 hash 算法,核心代碼以下:
/**獲取hash值方法*/ static final int hash(Object key) { int h; // h = key.hashCode() 爲第一步 取hashCode值(jdk1.7) // h ^ (h >>> 16) 爲第二步 高位參與運算(jdk1.7) return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//jdk1.8 } /**獲取數組下標方法*/ static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8沒有這個方法,可是實現原理同樣的 return h & (length-1); //第三步 取模運算 }
HashMap 因爲使用了2的冪次方,因此在取模運算時不須要作除法,只須要位的與運算就能夠了。可是因爲引入的 hash 衝突加重問題,HashMap 在調用了對象的 hashCode 方法以後,又作了一些高位運算,也就是第二步方法,來打散數據,讓哈希的結果更加均勻。
與此同時,在 jdk1.8 中 HashMap 還引進來紅黑樹實現,當衝突鏈表長度大於 8 的時候,會將鏈表結構改變成紅黑樹結構,讓查詢變得更快,具體實現能夠參見《集合系列》中的 HashMap 分析。
put 方法是將指定的 key, value 對添加到 map 裏。
put 流程圖以下:
打開 HashTable 的 put 方法,源碼以下:
public synchronized V put(K key, V value) { //當 value 值爲空的時候,拋異常! if (value == null) { throw new NullPointerException(); } Entry<?,?> tab[] = table; //經過key 計算存儲下標 int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; //循環遍歷數組鏈表 //若是有相同的key而且hash相同,進行覆蓋處理 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; } } //加入數組鏈表中 addEntry(hash, key, value, index); return null; }
put 方法中的 addEntry 方法,源碼以下:
private void addEntry(int hash, K key, V value, int index) { //新增修改次數 modCount++; Entry<?,?> tab[] = table; if (count >= threshold) { //數組容量大於擴容閥值,進行擴容 rehash(); tab = table; //從新計算對象存儲下標 hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } //將對象存儲在數組中 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; //每次擴容爲原來的 2n+1 int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) //大於最大閥值,再也不擴容 return; newCapacity = MAX_ARRAY_SIZE; } Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; //從新計算擴容閥值 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; //將舊數組中的數據複製到新數組中 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; } } }
總結流程以下:
有一個值得注意的地方是 put 方法加了synchronized
關鍵字,因此,在同步操做的時候,是線程安全的。
get 方法根據指定的 key 值返回對應的 value。
get 流程圖以下:
打開 HashTable 的 get 方法,源碼以下:
public synchronized V get(Object key) { Entry<?,?> tab[] = table; //經過key計算節點存儲下標 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; }
一樣,有一個值得注意的地方是 get 方法加了synchronized
關鍵字,因此,在同步操做的時候,是線程安全的。
remove 的做用是經過 key 刪除對應的元素。
remove 流程圖以下:
打開 HashTable 的 remove 方法,源碼以下:
public synchronized V remove(Object key) { Entry<?,?> tab[] = table; //經過key計算節點存儲下標 int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; Entry<K,V> e = (Entry<K,V>)tab[index]; //循環遍歷鏈表,經過hash和key判斷鍵是否存在 //若是存在,直接將改節點設置爲空,並從鏈表上移除 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; }
一樣,有一個值得注意的地方是 remove 方法加了synchronized
關鍵字,因此,在同步操做的時候,是線程安全的。
總結一下 Hashtable 與 HashMap 的聯繫與區別,內容以下:
儘管,Hashtable 雖然是線程安全的,可是咱們通常不推薦使用它,由於有比它更高效、更好的選擇 ConcurrentHashMap,在後面咱們也會講到它。
最後,引入來自 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.
簡單來講就是,若是你不須要線程安全,那麼使用 HashMap,若是須要線程安全,那麼使用 ConcurrentHashMap。
HashTable 已經被淘汰了,不要在新的代碼中再使用它。
一、JDK1.7&JDK1.8 源碼
二、博客園 - 程序員趙鑫 - HashMap和HashTable到底哪不一樣?
原文出處:https://www.cnblogs.com/dxflqm/p/11947384.html