Map---Hashtable源碼解析

 

Map--HashTable

上一篇文章中,我們分析了HashMap的源碼,這一篇文章我們學習Map接口的另一個實現類---HashTable,在學習之前,不熟悉hashMap的可以先看我的上一篇文章Map--HashMap,我們需要先了解下它和HashMap有哪些異同點。

不同點 HashMap HashTable
繼承的父類 Dictionary類 AbstractMap
線程安全性 線程不安全 線程安全
key和value 允許爲null 不允許爲null
遍歷方式 Iterator Iterator和Enumeration
hash值計算 重新計算key的hash值 直接使用key的hashCode()
初始化 默認容量爲16 默認容量爲11
擴容方式 原容量*2 原容量*2+1
數據結構 數組+鏈表+紅黑樹 數組+鏈表
Iterator遍歷數組的順序 索引從小到大 索引從大到小
確認key在數組中的索引 i=(n-1)&hash index=(hash&0x7FFFFFFF)%tab.length
底層數組容量爲2的整數冪 一定要爲2的整數冪 不要求

NOTE:HashMap和HashTable最大的不同體現在線程安全、key和value是否爲null,HashTable是個過時的集合類,如果使用場景不需要線程安全,可以直接使用hashMap來代替;如果需要在線程安全的場景中使用,可以使用ConcurrentHashMap替換,看起來HashTable好像沒什麼用,但是面試經常問啊,所以我們還是需要了解下。

因爲HashMap和HashTable在存儲結構實現方式上很相似,所以這篇文章主要講解HashTable與HashMap不同的知識點。

我們先看一下HashTable的底層數據結構:

HashTable的數據結構

1、成員屬性

transient Entry[] table:Entry[ ]數組類型,每個Entry代表一個鍵值對

transient int count:HashTable內鍵值對的數量,不是容器的大小

int threshold:調整hashTable容量的閾值

float loadFactor:加載因子

transient int modCount:標記HashTable修改的次數

//Entry[ ]數組類型,每個Entry代表一個鍵值對
private transient Entry<?,?>[] table;
//HashTable內鍵值對的數量,不是容器的大小
private transient int count;
//調整hashTable容量的閾值
private int threshold;
//加載因子
private float loadFactor;
//標記HashTable修改的次數
private transient int modCount = 0;

2、構造函數

HashTable有四個構造函數,我們來逐一分析

  • HashTable(int initialCapacity, float loadFactor)

public Hashtable(int initialCapacity, float loadFactor) {
    //初始容量<0,拋出異常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
    //負載因子爲非負整數,否則拋出異常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//初始化hashTable中的參數、loadFactor、table和threshold
        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
    //選用initialCapacity*loadFactor和Max_ARRAY_Size+1最小的作爲閾值
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

MAX_ARRAY_SIZE表示爲給數組(Table)分配的最大容量,

 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

有同學可能會對MAX_ARRAY_SIZE取值有疑問,爲什麼是Integer.MAX_VALUE-8,這是因爲數組作爲一個對象,需要一塊內存來存儲對象頭信息,對象頭信息的最大佔用內存不能超過8個字節,所以需要減去這個頭信息纔是分配給Table的最大容量。

  • HashTable (int initialCapacity)

以給定的初始容量和麼默認加載因子(0.75f)構造hashtable

public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }
  • HashTable()

以默認的初始容量(11)和加載因子(0.75f)構造HashTable

 public Hashtable() {
        this(11, 0.75f);
    }
  • HashTable(Map<? extends K, ? extends V> t)

使用給定的鍵值對集合t來構造HashTable

   public Hashtable(Map<? extends K, ? extends V> t) {
       //初始化HashTable
        this(Math.max(2*t.size(), 11), 0.75f);
       //將t中的鍵值對插入到HashTable中
        putAll(t);
    }

我們看一下putAll方法,它內部使用了增強for循環來遍歷,內部調用了put方法,我們在覈心方法講解put方法

public synchronized void putAll(Map<? extends K, ? extends V> t) {
    //使用增強for循環來進行遍歷
        for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
            //調用put方法
            put(e.getKey(), e.getValue());
    }

3、核心方法

HashTable內部提供了很多方法,我們在這篇文章主要講解HashTable中比較重要的方法,put、get和remove

3.1、put方法

put方法時是將指定的鍵值添加到hashTable中,其添加步驟可以概括爲①判斷value不爲null,爲null時,則拋出異常②計算key的hash值並找到key的索引,獲取key所在位置的entry③遍歷entry,判斷key是否存在④如果key存在,則將用新的值替換舊值⑤如果指定的位置key不存在,直接添加,並返回null

 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;
     //直接使用hashCode作爲hash值
        int hash = key.hashCode();
     //找到key的索引位置
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
     //獲取指定下標的entry
        Entry<K,V> entry = (Entry<K,V>)tab[index];
     //遍歷鏈表
        for(; entry != null ; entry = entry.next) {
            //判斷鏈表中是否有與entry的hash和key相等的對象,如果有,則讓新值覆蓋舊值,並返回舊值
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
//如果在Table沒有對應key值,則新添加一個
        addEntry(hash, key, value, index);
        return null;
    }

在這個程序中,有的同學可能會有疑問,比如key的索引位置是怎麼計算的,添加的流程是什麼,

  • index的計算

hash&0x7FFFFFFF操作是爲了使得hashCode的值爲正數,(計算後的值)%tab.length表示對數組長度取模,從概率上講,採用取模計算可以保證結點在數組上的分配比價均勻,這只是減少哈希衝突一種策略。

如果在Table中對沒有對應的key值,則需要新添加一個,我們具體看一下添加流程

  • addEntry(hash, key, value, index)

addEntry方法的作用是將指定的值的放入到指定座標下,其步驟如下①判斷entry中的個數是否大於閾值②大於閾值時需要擴容並重新計算值的位置③不大於閾值時需要將指定的值放入到index位置下,原有的index位置的元素向後移,可以看出數據的插入是前插

  private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
      //判斷entry的個數是否大於閾值(閾值默認是11*0.75f)
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            //如果超過閾值時,擴容並改變原來元素的位置
            rehash();

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

        // Creates the new entry.
        @SuppressWarnings("unchecked")
      //將原下標爲index的元素賦值給e
        Entry<K,V> e = (Entry<K,V>) tab[index];
      //將新的結點放在座標爲index的位置,並將新結點的next設置爲e
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

我們對上面程序中擴容方法rehash還沒有講解,我們來講解下rehash的源碼

  • rehash()

rehash方法是對擴容後的數組重新計算下標值並放入的數組中,其步驟如下①將新的數組的容量擴展爲(原容量)*2+1,②判斷新數組的容量是否超出了最大容量的限制③超出了容量限制,就將最大容量賦值給新的數組容量④遍歷原有數組中的元素並重新計算新的索引,將值存入到新的數組中

  protected void rehash() {
        int oldCapacity = table.length;
      //將原來的數組賦值給oldMap
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
      //新的容量擴容爲:(原有容量)*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++;
      //根據新容量重新計算閾值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
  //遍歷原來數組
        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;
            }
        }
    }

3.2、get方法

返回指定key的value,如果不存在,則返回null

 public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
     //計算索引值
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
     //根據索引找到數組的位置,根據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;
    }

3.3、remove方法

刪除指定key的鍵值對,其流程如下①根據key找到數組中的索引,獲取key所在的entry②遍歷entry,判斷key是否存在③如果key存在,刪除指定的鍵值對,並返回Value值④如果不存在,返回null

    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        //計算key在hashtabke中的索引
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        //根據索引得到頭結點鍵值對
        Entry<K,V> e = (Entry<K,V>)tab[index];
        //遍歷entry,找到key的鍵值對並刪除,返回value值
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            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;
            }
        }
        //如果不存在,則返回null
        return null;
    }

參考文獻

[1]https://blog.csdn.net/panweiwei1994/article/details/77427010

[2]https://blog.csdn.net/panweiwei1994/article/details/77428710

[3]jdk開發文檔

[4]馬克.艾倫.維斯.數據結構與算法分析

在公衆號回覆success領取獨家整理的學習資源

看了這篇文章,你是否「博學」了

「掃碼關注我」,每天博學一點點。

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