鍍金池/ 教程/ Java/ Hashtable 的實現(xiàn)原理
LinkedHashSet 的實現(xiàn)原理
LinkedHashMap 與 LRUcache
HashSet 和 HashMap 的比較
Hashtable 的實現(xiàn)原理
ArrayList 的實現(xiàn)原理
HashSet 的實現(xiàn)原理
HashMap 的實現(xiàn)原理
LinkedList 的實現(xiàn)原理
ConcurrentHashMap 的實現(xiàn)原理
LinkedHashMap 的實現(xiàn)原理

Hashtable 的實現(xiàn)原理

概述

和 HashMap 一樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對。

Hashtable 在 Java 中的定義為:

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

從源碼中,我們可以看出,Hashtable 繼承于 Dictionary 類,實現(xiàn)了 Map, Cloneable, java.io.Serializable接口。其中Dictionary類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類,每個鍵和值都是對象(源碼注釋為:The Dictionary class is the abstract parent of any class, such as Hashtable, which maps keys to values. Every key and every value is an object.)。但在這一點我開始有點懷疑,因為我查看了HashMap以及TreeMap的源碼,都沒有繼承于這個類。不過當我看到注釋中的解釋也就明白了,其 Dictionary 源碼注釋是這樣的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. 該話指出 Dictionary 這個類過時了,新的實現(xiàn)類應該實現(xiàn)Map接口。

Hashtable 源碼解讀

成員變量

Hashtable是通過"拉鏈法"實現(xiàn)的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。

  • table是一個 Entry[] 數(shù)組類型,而 Entry(在 HashMap 中有講解過)實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數(shù)組中的。
  • count 是 Hashtable 的大小,它是 Hashtable 保存的鍵值對的數(shù)量。
  • threshold 是 Hashtable 的閾值,用于判斷是否需要調整 Hashtable 的容量。threshold 的值="容量*加載因子"。
  • loadFactor 就是加載因子。
  • modCount 是用來實現(xiàn) fail-fast 機制的。

關于變量的解釋在源碼注釋中都有,最好還是應該看英文注釋。

/**
     * The hash table data.
     */
    private transient Entry<K,V>[] table;

    /**
     * The total number of entries in the hash table.
     */
    private transient int count;

    /**
     * The table is rehashed when its size exceeds this threshold.  (The
     * value of this field is (int)(capacity * loadFactor).)
     *
     * @serial
     */
    private int threshold;

    /**
     * The load factor for the hashtable.
     *
     * @serial
     */
    private float loadFactor;

    /**
     * The number of times this Hashtable has been structurally modified
     * Structural modifications are those that change the number of entries in
     * the Hashtable or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the Hashtable fail-fast.  (See ConcurrentModificationException).
     */
    private transient int modCount = 0;

構造方法

Hashtable 一共提供了 4 個構造方法:

  • public Hashtable(int initialCapacity, float loadFactor): 用指定初始容量和指定加載因子構造一個新的空哈希表。useAltHashing 為 boolean,其如果為真,則執(zhí)行另一散列的字符串鍵,以減少由于弱哈希計算導致的哈希沖突的發(fā)生。
  • public Hashtable(int initialCapacity):用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。
  • public Hashtable():默認構造函數(shù),容量為 11,加載因子為 0.75。
  • public Hashtable(Map<? extends K, ? extends V> t):構造一個與給定的 Map 具有相同映射關系的新哈希表。
/**
     * Constructs a new, empty hashtable with the specified initial
     * capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hashtable.
     * @param      loadFactor        the load factor of the hashtable.
     * @exception  IllegalArgumentException  if the initial capacity is less
     *             than zero, or if the load factor is nonpositive.
     */
    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);
        useAltHashing = sun.misc.VM.isBooted() &&
                (initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    }

    /**
     * Constructs a new, empty hashtable with the specified initial capacity
     * and default load factor (0.75).
     *
     * @param     initialCapacity   the initial capacity of the hashtable.
     * @exception IllegalArgumentException if the initial capacity is less
     *              than zero.
     */
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
        this(11, 0.75f);
    }

    /**
     * Constructs a new hashtable with the same mappings as the given
     * Map.  The hashtable is created with an initial capacity sufficient to
     * hold the mappings in the given Map and a default load factor (0.75).
     *
     * @param t the map whose mappings are to be placed in this map.
     * @throws NullPointerException if the specified map is null.
     * @since   1.2
     */
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

put 方法

put 方法的整個流程為:

  1. 判斷 value 是否為空,為空則拋出異常;
  2. 計算 key 的 hash 值,并根據(jù) hash 值獲得 key 在 table 數(shù)組中的位置 index,如果 table[index] 元素不為空,則進行迭代,如果遇到相同的 key,則直接替換,并返回舊 value;
  3. 否則,我們可以將其插入到 table[index] 位置。

我在下面的代碼中也進行了一些注釋:

public synchronized V put(K key, V value) {
        // Make sure the value is not null確保value不為null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        //確保key不在hashtable中
        //首先,通過hash方法計算key的哈希值,并計算得出index值,確定其在table[]中的位置
        //其次,迭代index索引位置的鏈表,如果該位置處的鏈表存在相同的key,則替換value,返回舊的value
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            //如果超過閥值,就進行rehash操作
            rehash();

            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        //將值插入,返回的為null
        Entry<K,V> e = tab[index];
        // 創(chuàng)建新的Entry節(jié)點,并將新的Entry插入Hashtable的index位置,并設置e為新的Entry的下一個元素
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        return null;
    }

通過一個實際的例子來演示一下這個過程:

假設我們現(xiàn)在Hashtable的容量為5,已經存在了(5,5),(13,13),(16,16),(17,17),(21,21)這 5 個鍵值對,目前他們在Hashtable中的位置如下:

http://wiki.jikexueyuan.com/project/java-collection/images/hashtable1.png" alt="圖1" />

現(xiàn)在,我們插入一個新的鍵值對,put(16,22),假設key=16的索引為1.但現(xiàn)在索引1的位置有兩個Entry了,所以程序會對鏈表進行迭代。迭代的過程中,發(fā)現(xiàn)其中有一個Entry的key和我們要插入的鍵值對的key相同,所以現(xiàn)在會做的工作就是將newValue=22替換oldValue=16,然后返回oldValue=16.

http://wiki.jikexueyuan.com/project/java-collection/images/hashtable2.png" alt="圖2" />

然后我們現(xiàn)在再插入一個,put(33,33),key=33的索引為3,并且在鏈表中也不存在key=33的Entry,所以將該節(jié)點插入鏈表的第一個位置。

http://wiki.jikexueyuan.com/project/java-collection/images/hashtable3.png" alt="圖3" />

get 方法

相比較于 put 方法,get 方法則簡單很多。其過程就是首先通過 hash()方法求得 key 的哈希值,然后根據(jù) hash 值得到 index 索引(上述兩步所用的算法與 put 方法都相同)。然后迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。

public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
    }

Hashtable 遍歷方式

Hashtable 有多種遍歷方式:

//1、使用keys()
Enumeration<String> en1 = table.keys();
    while(en1.hasMoreElements()) {
    en1.nextElement();
}

//2、使用elements()
Enumeration<String> en2 = table.elements();
    while(en2.hasMoreElements()) {
    en2.nextElement();
}

//3、使用keySet()
Iterator<String> it1 = table.keySet().iterator();
    while(it1.hasNext()) {
    it1.next();
}

//4、使用entrySet()
Iterator<Entry<String, String>> it2 = table.entrySet().iterator();
    while(it2.hasNext()) {
    it2.next();
}

Hashtable 與 HashMap 的簡單比較

  1. HashTable 基于 Dictionary 類,而 HashMap 是基于 AbstractMap。Dictionary 是任何可將鍵映射到相應值的類的抽象父類,而 AbstractMap 是基于 Map 接口的實現(xiàn),它以最大限度地減少實現(xiàn)此接口所需的工作。
  2. HashMap 的 key 和 value 都允許為 null,而 Hashtable 的 key 和 value 都不允許為 null。HashMap 遇到 key 為 null 的時候,調用 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
  3. Hashtable 方法是同步,而HashMap則不是。我們可以看一下源碼,Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內部通過 synchronized 代碼塊來實現(xiàn)。所以有人一般都建議如果是涉及到多線程同步時采用 HashTable,沒有涉及就采用 HashMap,但是在 Collections 類中存在一個靜態(tài)方法:synchronizedMap(),該方法創(chuàng)建了一個線程安全的 Map 對象,并把它作為一個封裝的對象來返回。