五分鐘看懂Hashtable原始碼以及與HashMap的區別

上一篇我們認識了什麼是 Map
、 Hash
,瞭解了 Hash
處理雜湊衝突的幾種常用方法(拉鍊法、開放定址法),以及分析了JDK1.8版本的 HashMap
原始碼,對Java集合框架有了初步的認識,我們本篇繼續分析JDK1.8版本的 Hashtable
原始碼,最後比較 HashMap
和 Hashtable
的區別。
Hashtable
注意是Hashtable不是HashTable(t為小寫),這不是違背了駝峰定理了嘛?這還得從Hashtable的出生說起,Hashtable是在 Java1.0
的時候建立的,而集合的統一規範命名是在後來的 Java2
開始約定的,而當時又釋出了新的集合代替它,所以這個命名也一直使用到現在,所以Hashtable是一個 過時 的集合了,不推崇大家使用這個類,雖說Hashtable是過時的了,我們還是有必要分析一下它,以便對Java集合框架有一個整體的認知。
首先 Hashtable
採用 拉鍊法 處理雜湊衝突,是 執行緒安全 的,鍵值不允許為 null
,然後Hashtable繼承自Dictionary,實現Map介面,Hashtable有幾個重要的成員變數 table
、 count
、 threshold
、 loadFactor
- table:是一個
Entry[]
資料型別,而Entry
實際是一個單鏈表 - count:Hashtable的大小,即Hashtable中儲存的鍵值對數量
- threshold:Hashtable的閾值,用於判斷是否需要調整Hashtable的容量,threshold = 容量 負載因子 ,threshold=11*0.75 取整即8
- loadFactor:用來實現快速失敗機制的
建構函式
Hashtable
有4個建構函式
//無參建構函式 預設Hashtable容量是11,預設負載因子是0.75 public Hashtable() { this(11, 0.75f); } //指定Hashtable容量,預設負載因子是0.75 public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); } //指定Hashtable的容量和負載因子 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; //new一個指定容量的Hashtable table = new Entry<?,?>[initialCapacity]; //閾值threshold=容量*負載因子 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); } //包含指定Map的建構函式 public Hashtable(Map<? extends K, ? extends V> t) { this(Math.max(2*t.size(), 11), 0.75f); putAll(t); } 複製程式碼
這裡的Hashtable容量和HashMap的容量就有區別,Hashtable並不要求容量是2的冪次方,而HashMap要求容量是2的冪次方。負載因子則預設都是0.75。
put方法
put
方法是 同步 的,即執行緒安全的,這點和 HashMap
不一樣,還有具體的 put
操作和 HashMap
也存在很大的差別,Hashtable插入的時候是插入到 連結串列頭部 ,而HashMap是插入到 連結串列尾部 。
//synchronized同步鎖,所以Hashtable是執行緒安全的 public synchronized V put(K key, V value) { // Make sure the value is not null //如果值value為空,則丟擲異常 至於為什麼官方不允許為空,下面給出分析 if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; //直接取key的hashCode()作為雜湊地址,這與HashMap的取hashCode()之後再進行hash()的結果作為雜湊地址 不一樣 int hash = key.hashCode(); //陣列下標=(雜湊地址 & 0x7FFFFFFF) % Hashtable容量,這與HashMap的陣列下標=雜湊地址 & (HashMap容量-1)計算陣列下標方式不一樣,前者是取模運算,後者是位於運算,這也就是為什麼HashMap的容量要是2的冪次方的原因,效率上後者的效率更高。 int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; //遍歷Entry連結串列,如果連結串列中存在key、雜湊地址相同的節點,則將值更新,返回舊值 for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } //如果為新的節點,則呼叫addEntry()方法新增新的節點 addEntry(hash, key, value, index); //插入成功返回null return null; } private void addEntry(int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; //如果當前鍵值對數量>=閾值,則執行rehash()方法擴容Hashtable的容量 if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; //獲取key的hashCode(); hash = key.hashCode(); //重新計算下標,因為Hashtable已經擴容了。 index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. @SuppressWarnings("unchecked") //獲取當前Entry連結串列的引用 復賦值給e Entry<K,V> e = (Entry<K,V>) tab[index]; //建立新的Entry連結串列的 將新的節點插入到Entry連結串列的頭部,再指向之前的Entry,即在連結串列頭部插入節點,這個和HashMap在尾部插入不一樣。 tab[index] = new Entry<>(hash, key, value, e); count++; } 複製程式碼
hashCode()為什麼要& 0x7FFFFFFF呢?因為某些物件的hashCode()可能是負值,& 0x7FFFFFFF保證了進行%運算時候得到的下標是個正數
get方法
get
方法也是同步的,和 HashMap
不一樣,即執行緒安全,具體的 get
操作和 HashMap
也有區別。
//同步 public synchronized V get(Object key) { Entry<?,?> tab[] = table; //和put方法一樣 都是直接獲取key的hashCode()作為雜湊地址 int hash = key.hashCode(); //和put方法一樣 通過(雜湊地址 & 0x7FFFFFFF)與Hashtable容量做%運算 計算出下標 int index = (hash & 0x7FFFFFFF) % tab.length; //遍歷Entry連結串列,如果連結串列中存在key、雜湊地址一樣的節點,則找到 返回該節點的值,否者返回null for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; } 複製程式碼
remove方法
//同步 public synchronized V remove(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; //遍歷Entry連結串列,e為當前節點,prev為上一個節點 for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) { //找到key、雜湊地址一樣的節點 if ((e.hash == hash) && e.key.equals(key)) { modCount++; //如果上一個節點不為空(即不是當前節點頭結點),將上一個節點的next指向當前節點的next,即將當前節點移除連結串列 if (prev != null) { prev.next = e.next; } else { //如果上一個節點為空,即當前節點為頭結點,將table陣列儲存的連結串列頭結點地址改成當前節點的下一個節點 tab[index] = e.next; } //Hashtable的鍵值對數量-1 count--; //獲取被刪除節點的值 並且返回 V oldValue = e.value; e.value = null; return oldValue; } } return null; } 複製程式碼
rehash方法
Hashtable的 rehash
方法和HashMap的 resize
方法一樣,是用來擴容雜湊表的,但是擴容的實現又有區別。
protected void rehash() { //獲取舊的Hashtable的容量 int oldCapacity = table.length; //獲取舊的Hashtable引用,為舊雜湊表 Entry<?,?>[] oldMap = table; // overflow-conscious code //新的Hashtable容量=舊的Hashtable容量 * 2 + 1,這裡和HashMap的擴容不一樣,HashMap是新的Hashtable容量=舊的Hashtable容量 * 2。 int newCapacity = (oldCapacity << 1) + 1; //如果新的Hashtable容量大於允許的最大容量值(Integer的最大值 - 8) if (newCapacity - MAX_ARRAY_SIZE > 0) { //如果舊的容量等於允許的最大容量值則返回 if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; //新的容量等於允許的最大容量值 newCapacity = MAX_ARRAY_SIZE; } //new一個新的Hashtable 容量為新的容量 Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; //計算新的閾值 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; //擴容後遷移Hashtable的Entry連結串列到正確的下標上 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; } } } 複製程式碼
接下來我們執行以下程式碼,驗證以下資料遷移過程
Hashtable hashtable = new Hashtable(); for (int i = 1; i <= 24; i ++) { hashtable.put(String.valueOf(i), i); } for (int i = 25; i <= 80; i ++) { hashtable.put(String.valueOf(i), i); } 複製程式碼
new
一個Hashtable,預設容量是 11
,負載因子是 0.75
執行第一個 for
迴圈後, 20
儲存在下標為 0
的 Entry
中,即 (hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 11 = 0

執行第二個 for
迴圈後,變成了 20
儲存在下標為 70
的 Entry
中,因為Hashtable擴容了4次,分別是從容量為預設的11->23->47->95->191,然後此時容量是191,所以 (hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 191 = 70

HashMap和Hashtable區別
到這裡我們分析了HashMap和Hashtable的原理,現在比較以下他們的區別。
不同點
- 繼承的類不一樣 :HashMap繼承的
AbstractMap
抽象類,Hashtable繼承的Dictionay
抽象類 - 應對多執行緒處理方式不一樣 :HashMap是非執行緒安全的,Hashtable是執行緒安全的,所以Hashtable效率比較低
- 定位演算法不一樣 :HashMap通過
key
的hashCode()進行hash()得到雜湊地址,陣列下標=雜湊地址 & (容量 - 1),採用的是與運算,所以 容量需要是2的冪次方結果才和取模運算結果一樣 。而Hashtable則是:陣列下標=(key的hashCode() & 0x7FFFFFFF ) % 容量,採用的取模運算,所以容量沒要求 - 鍵值對規則不一樣 :HashMap允許鍵值為
null
,而Hashtable不允許鍵值為null
- 雜湊表擴容演算法不一樣 :HashMap的容量擴容按照原來的容量*2,而Hashtable的容量擴容按照原來的容量*2+1
- 容量(capacity)預設值不一樣 :HashMap的容量預設值為16,而Hashtable的預設值是11
- put方法實現不一樣 :HashMap是將節點插入到連結串列的尾部,而Hashtable是將節點插入到連結串列的頭部
相同點
- 實現相同的介面 :HashMap和Hashtable均實現了
Map
介面 - 負載因子(loadFactor)預設值一樣 :HashMap和Hashtable的負載因子預設都是0.75
- 採用相同的方法處理雜湊衝突 :都是採用鏈地址法即拉鍊法處理雜湊衝突
- 相同雜湊地址可能分配到不同的連結串列,同一個連結串列內節點的雜湊地址不一定相同 :因為HashMap和Hashtable都會擴容,擴容後容量變化了,相同的雜湊地址取到的陣列下標也就不一樣。