我的jdk源碼(十四):Hashtable類

一、概述

    Hashtable類繼承於Dictionary抽象類,jdk註釋中說明了Dictionary類已經過時,新的實現類應該去實現Map接口,而不是繼承Dictionary類。但是面試的時候還是常常會問到Hashtable與HashMap的區別,所以我們還是來看一下Hashtable類的源碼,以及現在的實際應用場景中用什麼類來代替它。

二、源碼分析

    (1) 類的聲明

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

    與HashMap類相比,實現的接口完全一致,只是集成的父類不同:Hashtable繼承了Dictionary類;HashMap則是繼承自AbstractMap類。

    (2) 成員變量

    //存放Entry元素的數組
    private transient Entry<?,?>[] table;
    //實際元素的數量
    private transient int count;
    //擴容的臨界容量,threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    private int threshold;
    //負載因子
    private float loadFactor;
    //修改標記,用於fail-fast機制
    private transient int modCount = 0;
    //指定的序列化標識ID
    private static final long serialVersionUID = 1421746759512286392L;
    //數組最大容量,留8個字節存儲對象頭,具體可在jvm的學習中瞭解
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    transient修飾符修飾的成員變量不會被序列化。 

  (3) 構造方法

    //默認無參構造函數,設置默認的容量爲11,負載因子爲0.75f
    public Hashtable() {
        this(11, 0.75f);
    }

    //指定容量的構造函數
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    //指定容量和負載因子的構造函數
    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);
        //如果指定容量爲0,那麼返回的容量爲1
        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }
    //傳入指定的Map返回爲Hashtable
    public Hashtable(Map<? extends K, ? extends V> t) {
        //設置容量Math.max(2*t.size(), 11),取2倍t的元素數量和11比較,取大的值
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

    不同於HashMap的是,Hashtable在成員變量中並沒有設置默認容量,而是在構造函數中設置的,並且默認容量爲11;HashMap是成員變量中就設置了初始容量爲16。

    (4) Entry<K,V>源碼如下:

    private static class Entry<K,V> implements Map.Entry<K,V> {
        final 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;
        }
        //這裏與HashMap的Node不同,HashMap的Node沒有clone()方法
        @SuppressWarnings("unchecked")
        protected Object clone() {
            return new Entry<>(hash, key, value,
                                  (next==null ? null : (Entry<K,V>) next.clone()));
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }
        //設置value時,如果value爲null,則直接拋出異常
        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;
            //必須是key和value都相等才返回true
            return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
               (value==null ? e.getValue()==null : value.equals(e.getValue()));
        }
        //hashCode()與HashMap也不同,HashMap是將key和value的hash值進行異或運算。
        public int hashCode() {
            return hash ^ Objects.hashCode(value);
        }

        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }

    (5) addEntry()方法

    //添加一個Entry元素到指定位置index
    private void addEntry(int hash, K key, V value, int index) {
        //修改標記+1
        modCount++;
        
        Entry<?,?> tab[] = table;
        //如果元素數量超過了限制的數量,就調用rehash()方法進行擴容
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            //用k的hash值與Integer.MAX_VALUE-1進行&與運算後的結果對table的容量取模獲得新下標
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        //創建新元素,並獲取舊元素e的引用
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        //將index位置設置爲新元素,且新元素的下一個元素指定爲e,也就是說每次添加元素都是添加在鏈表頭
        tab[index] = new Entry<>(hash, key, value, e);
        //元素總數+1
        count++;
    }

    (6) rehash()方法

    //擴容方法
    protected void rehash() {
        //記錄原始的容量爲oldCapacity
        int oldCapacity = table.length;
        //記錄下原始的容器爲oldMap
        Entry<?,?>[] oldMap = table;

        //計算新容量newCapacity的值爲2倍oldCapacity的值+1,也就是newCapacity = 2oldCapacity + 1 
        int newCapacity = (oldCapacity << 1) + 1;
        //如果新容量大於數組最大運行容量MAX_ARRAY_SIZE,也就是Integer.MAX_VALUE - 8
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            //判斷原始容量oldCapacity是否已經等於了MAX_ARRAY_SIZE,如果是則直接return
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            //如果原始容量oldCapacity還未達到MAX_ARRAY_SIZE,則將此次新容量newCapacity設置爲MAX_ARRAY_SIZE
            newCapacity = MAX_ARRAY_SIZE;
        }
        //用新容量初始化一個Entry數組
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
        //修改標記+1
        modCount++;
        //計算新的擴容臨界值threshold,取新容量newCapacity和負載因子loadFactor的乘積與MAX_ARRAY_SIZE + 1中較小的值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        //將新Map設置爲容器
        table = newMap;
        //循環將舊容器oldMap中的元素添加到新容器中
        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;
                //重新計算hash值
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                //把newMap原來index下的元素設置爲e的下一個元素,並將元素e放在index位置上
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

    HashTable的擴容機制如下:例如默認初始容量是11,加載因子爲0.75,那麼擴容閥值就是8,當數組長度達到8的時候,HashTable就會進行一第次擴容,擴容後的容量就是 8 * 2 + 1 = 17 ( int newCapacity = (oldCapacity << 1) + 1) ,此時的擴容閥值就是 17 * 0.75 = 13 ,當下次達到13的時候,就會在重複擴容一次。其實,這個擴容消耗還是蠻大的,因爲擴容後需要原來HashTable中的元素一一複製到新的HashTable中。

    (7) put()方法

  //添加一個元素  
  public synchronized V put(K key, V value) {
        // 如果value爲null,直接拋出異常
        if (value == null) {
            throw new NullPointerException();
        }
        //獲取現有的容器tab[]
        Entry<?,?> tab[] = table;
        //計算桶位,如果key爲null,此處會拋異常
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        //循環遍歷是否有相同key的元素存在,如果有就替換舊元素的value值,並且返回舊元素的alue值
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        //如果遍歷完,不存在相同的key,則調用addEntry()方法添加元素
        addEntry(hash, key, value, index);
        return null;
    }

    我們可以看到Hashtable類的put()方法加入了synchronized關鍵字修飾,以確保此方法線程安全。

    (8) get()方法

    //獲取指定key的value值,線程安全
    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
   	    //計算hash
        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;
            }
        }
   		//沒有返回 null
        return null;
    }

    (9) clear()方法

    //將元素全部置爲null,也是線程安全的
    public synchronized void clear() {
        Entry<?,?> tab[] = table;
        modCount++;
        for (int index = tab.length; --index >= 0; )
            tab[index] = null;
        count = 0;
    }

    (10) containsKey()方法

    //判斷是否包含key,線程安全
    public synchronized boolean containsKey(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        //找到“key對應的Entry(鏈表)”,然後在鏈表中找出“哈希值”和“鍵值”與key都相等的元素
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return true;
            }
        }
        return false;
    }

    沒啥好說的,就是拿key直接轉化爲座標index,從index往後找,查找是否存在此key的元素,判斷的依據是hash值和key值都要相同。

    (11) containsValue()方法

 public boolean containsValue(Object value) {
        return contains(value);
    }

public synchronized boolean contains(Object value) {
        //如果value爲null直接拋異常
        if (value == null) {
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;
        //循環遍歷容器
        for (int i = tab.length ; i-- > 0 ;) {
            for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }
    

三、總結

    Hashtable類還是要與HashMap來比較着分析,主要有一下幾點不同:

    * 繼承的父類不同:Hashtable類繼承自Dictionary這一過時的類;HashMap類繼承自AbstractMap類。

    * 數據結構不同:Hashtable始終是"數組+鏈表"的形式;HashMap在jdk1.8後是有"數組+鏈表"和"數組+紅黑樹"的形式的。

    * 無參初始容量不同:Hashtable無參初始容量爲11;HashMap的無參初始容量爲16

    * hash值計算方式不同:HashTable計算哈希的方式是直接取key本身的hash;而HashMap計算hash的方式爲"(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) ",即自身哈希和哈希無符號右移16位做與運算。

    * 獲得索引key的方式不同:Hashtable是“index = (hash & 0x7FFFFFFF) % tab.length”,採用的是取模運算;而HashMap在jdk1.8已經採用“(length - 1) & hash”,把hash值和容量進行“與”操作,這得益於HashMap的容量始終爲2的次冪,這樣計算效率大大提升。

    * 擴容機制不同:一般情況下,Hashtable每次擴容是從n到2n+1;HashMap每次擴容從n變爲2n,並且由於HashMap在指定容量進行初始化以及每次擴容時都會調用 inflateTable()方法來保證自己的容器容量始終是2的次冪。

    * key和value限制不同:Hashtable不允許key爲null,也不允許value爲null,源碼中我們得知,每次都會判斷value是否爲null,如果是就直接拋出異常,而key則是在調用key.hashCode()時,如果key爲null也會拋出異常;HashMap中的源碼在判斷key爲null後,會設置key的hash值爲0,也就是放在桶的第一個位置,代碼中也不會value是否爲null做限制,那麼結論就是HashMap允許一個key爲null的元素(再有就覆蓋原來的value),允許多個value爲null的元素。

    * 線程安全問題:Hashtable中涉及容器變化以及訪問的方法,都採用了synchronized關鍵字修飾,以保證線程安全,相對的效率低一些;HashMap無synchronized修飾,線程不安全,相比Hashtable效率高一些。

    值得注意的是Hashtable類似乎也要被淘汰了,jdk1.8的Hashtable類的註釋中有寫: 如果你不需要線程同步,建議使用HashMap來代替HashTable,如果你的你是需要線程同步的話使用ConcurrentHashMap來替代Hashtable 。

    更多精彩內容,敬請掃描下方二維碼,關注我的微信公衆號【Java覺淺】,獲取第一時間更新哦!

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