1. 程式人生 > >【原始碼】Hashtable原始碼剖析

【原始碼】Hashtable原始碼剖析

注:以下原始碼基於jdk1.7.0_11
上一篇分析了HashMap的原始碼,相信大家對HashMap都有了更深入的理解。本文將介紹Map集合的另一個常用類,Hashtable。 Hashtable出來的比HashMap早,HashMap 1.2才有,而Hashtable在1.0就已經出現了。HashMap和Hashtable實現原理基本一樣,都是通過雜湊表實現。而且兩者處理衝突的方式也一樣,都是通過連結串列法。下面我們就詳細介紹下這個類。 首先看類宣告:
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

Hashtable並沒有去繼承AbstractMap,而是選擇繼承了Dictionary類,Dictionary是個被廢棄的抽象類,文件已經說得很清楚了:
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);
}

沒啥好說的,下面直接看Hashtable原始碼,首先依然是成員變數:

 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).)
     *容量的閾值,超過此容量將會導致擴容。值為容量*負載因子
     */
    private int threshold;
    /**
     * The load factor for the hashtable.
     *負載因子
     */
    private float loadFactor;
    /**
     * hashtable被改變的次數,用於快速失敗機制
     */
    private transient int modCount = 0;
成員變數跟HashMap基本類似,但是HashMap更加規範,HashMap內部還定義了一些常量,比如預設的負載因子,預設的容量,最大容量等等。 接下來是構造器:
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;//初始容量最小值為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).
     */
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);//預設負載因子為0.75
    }
    public Hashtable() {
        this(11, 0.75f);//預設容量為11,負載因子為0.75
    }
    /**
     * 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).
     */
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }
需注意的點: 1.Hashtable的預設容量為11,預設負載因子為0.75.(HashMap預設容量為16,預設負載因子也是0.75) 2.Hashtable的容量可以為任意整數,最小值為1,而HashMap的容量始終為2的n次方。 3.為避免擴容帶來的效能問題,建議指定合理容量。 另外我們看到,Hashtable的編碼相比較HashMap不是很規範,構造器中出現了硬編碼,而HashMap中定義了常量。 跟HashMap一樣,Hashtable內部也有一個靜態類叫Entry,其實是個鍵值對物件,儲存了鍵和值的引用。也可以理解為一個單鏈表的結點,因為其持有下一個Entry物件的引用:
private static class Entry<K,V> implements Map.Entry<K,V> {//鍵值對物件
        int hash;//雜湊值
        final K key;//鍵
        V value;//值
        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;
        }
        protected Object clone() {//直接通過new的方式克隆
            return new Entry<>(hash, key, value,
                                  (next==null ? null : (Entry<K,V>) next.clone()));
        }
        // Map.Entry Ops
        public K getKey() {
            return key;
        }
        public V getValue() {
            return value;
        }
        public V setValue(V value) {//可設定值
            if (value == null)
                throw new NullPointerException();
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry)o;
            return key.equals(e.getKey()) && value.equals(e.getValue());
        }
        public int hashCode() {
            return hash ^ value.hashCode();
        }
        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }
再次強調:HashMap和Hashtable儲存的是鍵值對物件,而不是單獨的鍵或值。 明確了儲存方式後,再看put和get方法:
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;
        int hash = hash(key);//根據鍵生成hash值---->若key為null,此方法會拋異常
        int index = (hash & 0x7FFFFFFF) % tab.length;//通過hash值找到其儲存位置
        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();//重新構建桶陣列,並對陣列中所有鍵值對重雜湊,耗時!
            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;//這裡是取摸運算
        }
        // Creates the new entry.
        Entry<K,V> e = tab[index];
        //將新結點插到連結串列首部
        tab[index] = new Entry<>(hash, key, value, e);//生成一個新結點
        count++;
        return null;
    }
需注意的點: 1.Hasbtable並不允許值和鍵為空(null),若為空,會拋空指標.大家可能奇怪,put方法在開始處僅對value進行判斷,並未對key判斷,這裡我認為是設計者的疏忽。當然,這並不影響使用,因為當呼叫hash方法時,若key為空,依然會丟擲空指標異常:
private int hash(Object k) {
        if (useAltHashing) {
            if (k.getClass() == String.class) {
                return sun.misc.Hashing.stringHash32((String) k);
            } else {
                int h = hashSeed ^ k.hashCode();
                h ^= (h >>> 20) ^ (h >>> 12);
                return h ^ (h >>> 7) ^ (h >>> 4);
             }
        } else  {
            return k.hashCode();//此處可能拋空指標異常
        }
    }
2.HashMap計算索引的方式是h&(length-1),而Hashtable用的是模運算,效率上是低於HashMap的。 3.另外Hashtable計算索引時將hash值先與上0x7FFFFFFF,這是為了保證hash值始終為正數。 4.特別需要注意的是這個方法包括下面要講的若干方法都加了synchronized關鍵字,也就意味著這個Hashtable是個執行緒安全的類,這也是它和HashMap最大的不同點. 下面我們看下擴容方法rehash:
protected void rehash() {
        int oldCapacity = table.length;//記錄舊容量
        Entry<K,V>[] oldMap = table;//記錄舊的桶陣列
        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;//新容量為老容量的2倍加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<K,V>[] newMap = new Entry[newCapacity];//建立新的陣列
        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        boolean currentAltHashing = useAltHashing;
        useAltHashing = sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = currentAltHashing ^ useAltHashing;
        table = newMap;
        for (int i = oldCapacity ; i-- > 0 ;) {//轉移鍵值對到新陣列
            for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;
                if (rehash) {
                    e.hash = hash(e.key);
                }
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = newMap[index];
                newMap[index] = e;
            }
        }
    }
Hashtable每次擴容,容量都為原來的2倍加2,而HashMap為原來的2倍。 接下來分析get方法:
  public synchronized V get(Object key) {//根據鍵取出對應索引
        Entry tab[] = table;
        int hash = hash(key);//先根據key計算hash值
        int index = (hash & 0x7FFFFFFF) % tab.length;//再根據hash值找到索引
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍歷entry鏈
            if ((e.hash == hash) && e.key.equals(key)) {//若找到該鍵
                return e.value;//返回對應的值
            }
        }
        return null;//否則返回null
    }
當然,如果你傳的引數為null,是會拋空指標的。 至此,最重要的部分已經講完,下面再看一些常用的方法:
 public synchronized V remove(Object key) {//刪除指定鍵值對
        Entry tab[] = table;
        int hash = hash(key);//計算hash值
        int index = (hash & 0x7FFFFFFF) % tab.length;//計算索引
        for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {//遍歷entry鏈
            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;
    }

 public synchronized void clear() {//清空桶陣列
        Entry tab[] = table;
        modCount++;
        for (int index = tab.length; --index >= 0; )
            tab[index] = null;//直接置空
        count = 0;
    }

下面是獲取其鍵集(keySet)和鍵值集(entrySet)的方法:
public Set<K> keySet() {
        if (keySet == null)//通過Collections的包裝,返回的是執行緒安全的鍵集
            keySet = Collections.synchronizedSet(new KeySet(), this);
        return keySet;
    }
 public Set<Map.Entry<K,V>> entrySet() {
        if (entrySet==null)//通過Collections的包裝,返回的是執行緒安全的鍵值集
            entrySet = Collections.synchronizedSet(new EntrySet(), this);
        return entrySet;
    }

這個KeySet和EntrySet是Hashtable的兩個內部類:
 private class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return getIterator(KEYS);
        }
        public int size() {
            return count;
        }
        public boolean contains(Object o) {
            return containsKey(o);
        }
        public boolean remove(Object o) {
            return Hashtable.this.remove(o) != null;
        }
        public void clear() {
            Hashtable.this.clear();
        }
    }


總結: 1.Hashtable是個執行緒安全的類(HashMap執行緒安全); 2.Hasbtable並不允許值和鍵為空(null),若為空,會拋空指標(HashMap可以); 3.Hashtable不允許鍵重複,若鍵重複,則新插入的值會覆蓋舊值(同HashMap); 4.Hashtable同樣是通過連結串列法解決衝突; 5.Hashtable根據hashcode計算索引時將hashcode值先與上0x7FFFFFFF,這是為了保證hash值始終為正數; 6.Hashtable的容量為任意正數(最小為1),而HashMap的容量始終為2的n次方。Hashtable預設容量為        11,HashMap預設容量為      16; 7.Hashtable每次擴容,新容量為舊容量的2倍加2,而HashMap為舊容量的2倍; 8.Hashtable和HashMap預設負載因子都為0.75;