Java容器之Hashtable源碼分析(關於Hashtable的這些細節你可能還不知道)

  在上一篇博客 Java容器之HashMap源碼分析(媽媽再也不用擔心我不懂HashMap了) 從源碼層次分析了HashMap容器的底層實現,在本篇博客將繼續從源碼層次分析Hashtable的底層實現。
  註明:以下源碼分析都是基於jdk 1.8.0_221版本
在這裏插入圖片描述

一、Hashtable容器概述(一圖以蔽之

  Java中的Hashtable容器也是一個用來存放key-value(鍵值對)的容器,主要由table數組和若干鏈表組成。
  Hashtable容器類的聲明如下:

public class Hashtable<K,V> extends Dictionary<K,V>implements Map<K,V>, Cloneable, java.io.Serializable

在這裏插入圖片描述
  註明:既然Java已經有了HashMap容器,爲啥還有引入Hashtable?應該有部分小夥伴對此有疑問。通過對比HashMapHashtable容器的功能,可以發現,兩者都是key-value容器,不過HashMap是線程不安全的容器,即不能多個線程同時訪問一個HashMap容器;而Hashtable容器是線程安全的,即Hashtable容器支持多個線程同時訪問(通過synchronized關鍵字,this鎖實現)。

二、Hashtable類的屬性

  Hashtable類中的主要屬性如下:

/**
 * table數組(hash桶數組),用於存放key-value數據
 */
private transient Entry<?,?>[] table;

/**
 * 容器中已經存放的key-value個數
 */
private transient int count;

/**
 * 容器可存放key-value的閾值 = capacity * loadFactor
 * capacity是容器的容量(table數組的長度,初始化時默認是11)
 * loadFactor是負載因子,默認是0.75
 */
private int threshold;

/**
 * 負載因子,默認是0.75
 */
private float loadFactor;

/**
 * 用於記錄容器結構性調整(增刪Entry、rehash等)的次數
 */
private transient int modCount = 0;

/**
 * 序列化版本ID
 */
private static final long serialVersionUID = 1421746759512286392L;

/**
 * keySet用於存放容器中的所有key
 * values用於存放容器中的所有value
 * entrySet以set容器形式存放Entry,一般遍歷Hashtable會用到此屬性
 */
private transient volatile Set<K> keySet;
private transient volatile Set<Map.Entry<K,V>> entrySet;
private transient volatile Collection<V> values;

\color{red}注意:capacity是容器的容量(table數組的長度,初始化時默認是11),count記錄容器中已經存放的key-value個數,threshold記錄容器可存放key-value的閾值 = 容器容量 * 負載因子

三、Hashtable類的構造器

  Hashtable類有四個構造器,分別如下:

/**
 * @param      initialCapacity   容器容量(table數組的長度)初始化參數
 * @param      loadFactor        負載因子
 * @exception  IllegalArgumentException  initialCapacity、loadFactor參數非法異常
 */
public Hashtable(int initialCapacity, float loadFactor) {
	// 檢查initialCapacity、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];
    // 計算容器可存放的key-value數量閾值,容量 * 負載因子
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

/**
 * 默認負載因子爲0.75的構造器
 *
 * @param     initialCapacity   容器容量(table數組的長度)初始化參數
 * @exception IllegalArgumentException    initialCapacity參數非法異常
 */
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}

/**
 * 容器容量默認爲11,負載因子默認爲0.75
 */
public Hashtable() {
    this(11, 0.75f);
}

/**
 * 複製構造函數,用其它map容器初始化當前new的容器對象
 */
public Hashtable(Map<? extends K, ? extends V> t) {
	// 先調用構造函數public Hashtable(int initialCapacity, float loadFactor)
	// 一般Hashtable默認容量大小爲11,所以不能比它還小
    this(Math.max(2*t.size(), 11), 0.75f);
    // 然後將容器t中的所有entry複製到新建的容器對象中
    putAll(t);
}

四、增加key-value相關方法

1、addEntry方法

  addEntry方法的作用是往容器中放入一個entry,注意此方法沒有使用synchronized關鍵字修飾,但是用的private關鍵字修飾,也就是說不對外開發。只要是在本類調用addEntry的其它方法是線程同步,那麼也不會產生線程不安全的狀況。

/**
 * @parm hash 通過調用hashCode()得到的hash值
 * @parm key 鍵值對中的key
 * @parm value 鍵值對中的value
 * @parm index hash對應的table數組的預期下標
 */
private void addEntry(int hash, K key, V value, int index) {
	// 插入節點,屬於結構性調整
    modCount++;

    Entry<?,?> tab[] = table;
    if (count >= threshold) {
        // 如果容器當前存放的key-value數量達到了threshold閾值(容器容量 * 負載因子)
        // 則需要進行rehash擴容操作
        rehash();
		// 由於進行rehash操作,則key對應的hash需要重新計算,hash對應的下標也要重新計算
        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // 根據key-value,新建一個entry,放入tab[index]中
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    // 注意Entry的構造函數的第四個參數是next,也就是把tab[index] = new Entry
    // 然後讓new Entry.next = e(未更新前tab[index]指向的內容)
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

2、put方法

  put方法用了synchronizedpublic關鍵字修飾,對外開放,並且是線程安全。(synchronized關鍵字修飾非靜態方法,用的是this鎖

/**
 * @param      key     the hashtable key
 * @param      value   the value
 * @return     如果容器已經有key,更新value並且返回舊value,否則插入並且返回空
 * @exception  NullPointerException  if the key or value is
 *               <code>null</code>
 */
public synchronized V put(K key, V value) {
    // 確保value不能爲空
    if (value == null) {
        throw new NullPointerException();
    }

    // 計算key對應的hash以及hash對應在table數組中的下標
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    // entry直線table[index],即hash對應的鏈表頭部
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    // 遍歷這個鏈表
    for(; entry != null ; entry = entry.next) {
    	// 如果key已經存在,修改新的value,並且返回之前的value
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
	// 否則調用addEntry方法插入,當前方法已經加了synchronized關鍵字,所以addEntry不會產生線程不安全的狀況
    addEntry(hash, key, value, index);
    return null;
}

3、putAll方法

  putAll方法的作用是將其他map容器中的元素複製到當前容器中,使用了synchronized關鍵字修飾,所以不會引發線程安全的狀況。

/**
 * 作用是將其他map容器中的元素複製到當前容器中
 */
public synchronized void putAll(Map<? extends K, ? extends V> t) {
	// 遍歷容器t將key-value一一複製到當前容器中
    for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
        put(e.getKey(), e.getValue());
}

五、刪除key-value相關方法

1、remove(key)方法

  remove(key)方法的作用是通過key刪除key-value

/**
 * 通過key移除key-value
 * @param   key   the key that needs to be removed
 * @return  移除成功返回key對應的value,否則返空
 * @throws  NullPointerException  if the key is <code>null</code>
 */
public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    // 計算key對應的hash值,從而找到key在table數組中的下標
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    // 獲取hash值對應的鏈表頭部
    Entry<K,V> e = (Entry<K,V>)tab[index];
    // 遍歷此鏈表,pre指向的e前面的節點
    for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
        	// key匹配成功
        	// 移除節點也是結構性調整
            modCount++;
            if (prev != null) {
            	// 如果e前面(pre)不爲空,則直接把e移除鏈表即可
                prev.next = e.next;
            } else {
            	// 否則e就是鏈表的表頭tab[index],則需要更新tab[index]
                tab[index] = e.next;
            }
            count--;
            // 返回移除前key-value中的value
            V oldValue = e.value;
            e.value = null;
            return oldValue;
        }
    }
    return null;
}

2、remove(key,value)方法

  remove(key,value)方法是通過key-value刪除,只有兩個都匹配成功才進行刪除操作。此方法同樣使用了synchronized關鍵字修飾,所以不會引發線程不安全的問題。

public synchronized boolean remove(Object key, Object value) {
    Objects.requireNonNull(value);

    Entry<?,?> tab[] = table;
    // 計算key對應的hash值,從而找到key在table數組中的下標
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    // 獲取hash值對應的鏈表頭部
    Entry<K,V> e = (Entry<K,V>)tab[index];
    // 遍歷此鏈表,pre指向的e前面的節點
    for (Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
        if ((e.hash == hash) && e.key.equals(key) && e.value.equals(value)) {
        	// key匹配成功
        	// 移除節點也是結構性調整
            modCount++;
            if (prev != null) {
            	// 如果e前面(pre)不爲空,則直接把e移除鏈表即可
                prev.next = e.next;
            } else {
            	// 否則e就是鏈表的表頭tab[index],則需要更新tab[index]
                tab[index] = e.next;
            }
            count--;
            e.value = null;
            // 刪除成功則返回true
            return true;
        }
    }
    return false;
}

六、查找key-value相關方法

1、get(key)方法

  get(key)方法的作用是通過key查找value,該方法同樣使用了synchronized關鍵字修飾。

/**
 * 通過`key`查找`value`
 */
@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    // 計算key對應的hash值,從而找到key在table數組中的下標
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    // 遍歷tab[index]這個鏈表,查找key對應的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;
}

2、containsKey(key)方法

  containsKey(key)方法的作用是判斷容器中是否存在key,該方法也使用了synchronized關鍵字修飾。

/**
 * 判斷容器中是否竄在key
 */
public synchronized boolean containsKey(Object key) {
    Entry<?,?> tab[] = table;
    // 計算key對應的hash值,從而找到key在table數組中的下標
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    // 遍歷tab[index]鏈表,查找key
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return true;
        }
    }
    return false;
}

3、containsValue(value)方法

  containsValue(value)方法判斷容器中是否存在value,該方法也使用了synchronized關鍵字修飾。

/**
 * 判斷容器中是否存在`value`
 */
public synchronized boolean contains(Object value) {
    if (value == null) {
    	// 由於Hashtable的key、value都不能爲空,所以爲null的value不需要查找
        throw new NullPointerException();
    }
	// 此時需要遍歷整個table數組
    Entry<?,?> tab[] = table;
    for (int i = tab.length ; i-- > 0 ;) {
    	// 遍歷tab[i] 每個鏈表
        for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
            if (e.value.equals(value)) {
                return true;
            }
        }
    }
    return false;
}

七、其它方法

1、hashCode方法

  hashCode方法的效果類似於重寫Object類的hashCode方法。

/**
 * 返回容器對應的hash值
 */
public synchronized int hashCode() {
    /*
     * This code detects the recursion caused by computing the hash code
     * of a self-referential hash table and prevents the stack overflow
     * that would otherwise result.  This allows certain 1.1-era
     * applets with self-referential hash tables to work.  This code
     * abuses the loadFactor field to do double-duty as a hashCode
     * in progress flag, so as not to worsen the space performance.
     * A negative load factor indicates that hash code computation is
     * in progress.
     */
    int h = 0;
    if (count == 0 || loadFactor < 0)
        return h;  // Returns zero
	//
    loadFactor = -loadFactor;  // Mark hashCode computation in progress
    Entry<?,?>[] tab = table;
    // 將所有entry對應的hashCode求和
    for (Entry<?,?> entry : tab) {
        while (entry != null) {
            h += entry.hashCode();
            entry = entry.next;
        }
    }

    loadFactor = -loadFactor;  // Mark hashCode computation complete

    return h;
}

2、rehash方法

  rehash方法的作用是進行擴容,並且重新計算key對應的index(調整key-value的位置),此方法雖沒有使用synchronized關鍵字修飾,但是使用了protected關鍵字修飾,只對類中方法、子類方法可見,只要調用此方法的方法使用synchronized關鍵字修飾,那麼也不會引發線程不安全的狀況。

/**
 * 容器擴容,並且調整各個key-value的位置
 */
@SuppressWarnings("unchecked")
protected void rehash() {
	// 記錄之前table數組的狀態
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;

    // 擴大爲原來的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++;
    // 計算擴容後key-value的閾值,newCapacity * 負載因子
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
	// 將之前的table數組中的key-value進行調整
    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;
        }
    }
}

八、HashtableHashMap容器區別

1、HashMap允許keyvalue爲空,而Hashtable不允許。
2、Hashtable是線程安全的(通過synchronized關鍵字,this鎖實現),HashMap不是線程安全。
3、Hashtable繼承自DictionaryHashMap繼承自AbstractMap
4、迭代器不同,Hashtableenumerator迭代器,HashMapIterator迭代器。
5、Hashtable中的hash桶由鏈表構成,HashMap中的hash桶可由紅黑樹鏈表構成。
6、Hashtable中的hash值由Object.hashCode()& 0x7FFFFFFF方法計算而得(與上0x7FFFFFFF是防止hashCode出現負數,求餘時也會是負數),HashMap中的hash值由Object.hashCode()的高16位、低16位異或計算而得。
7、Hashtable初始化默認大小是11,擴容爲原來的2倍+1,HashMap初始化默認大小是16,每次擴容後的大小都是2的次冪。

九、總結

  Hashtable線程安全,相對效率低,結構也較爲簡單、HashMap線程不安全,但是高效,不過結構也更復雜些。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章