詳解HashMap

詳解HashMap

    一、數據結構

    HashMap是由Hash表(散列表)維護的一個數據結構模型,什麼是Hash表呢?
    哈希表,是根據Key-value直接進行訪問的數據結構,也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表:記錄的存儲位置=f(關鍵字)。

    首先我們來看看HashMap源碼中的“靜態類”Entry:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
}

    源碼過多,我們只展示其數據結構,是一個典型的鏈式數據結構,完全可以推測出HashMap解決Hash衝突的方式可能爲鏈地址法。

    二、HashMap中的主要方法

    1.put()方法

    依舊根據源碼進行分析,對其數據結構進行更深入的驗證和分析:

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
//當table爲空時,傳入一個臨界值,構造一個新的table,table爲一個數組,存放多個Entry,該方法只允許最終數組大小爲2的冪
        }
        if (key == null)
            return putForNullKey(value);
//允許加入一個key爲空的value
        int hash = hash(key);
//獲取key的hash值,通過hash函數對key的HashCode進行處理得到的值
        int i = indexFor(hash, table.length);
//根據Key的HashCode找到其在數組中的index:return h & (length-1);也就是使用數組長度求餘。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//循環遍歷數組中指定index中的Entry鏈表
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//當key的HashCode值相同,且兩者equals爲true,開始覆蓋原來的key
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
//否則表示key爲空,插入新值
        modCount++;
        addEntry(hash, key, value, i);//加入一個新的Entry
        return null;
    }

    2.get()方法:

    源碼:
public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    get方法相對簡單,如果key爲null則調用getForNullKey方法,否則getEntry,獲取指定entry。

    3.總結

    由以上源碼我們可以得出,HashMap解決衝突的方式是鏈地址法。Hash函數,爲key的HashCode值,與Hash表表長求餘,餘數爲插入表的index,若有衝突,則在該表的Entry後面鏈式插入。同時,可以根據源碼細節,看出HashMap允許key爲null。

    三、addEntry()細節和HashMap的擴容

    addEntry()源碼:
    //在table指定位置新增Entry, 這個方法很重要      
    void addEntry(int hash, K key, V value, int bucketIndex) {  
        if ((size >= threshold) && (null != table[bucketIndex])) {  
        //table容量不夠, 該擴容了(兩倍table),重點來了,下面將會詳細分析  
            resize(2 * table.length);  
        //計算hash, null爲0  
            hash = (null != key) ? hash(key) : 0;  
        //找出指定hash在table中的位置  
            bucketIndex = indexFor(hash, table.length);  
        }  
  
        createEntry(hash, key, value, bucketIndex);  
    }  
      
    //擴容方法 (newCapacity * loadFactor)  
    void resize(int newCapacity) {  
        Entry[] oldTable = table;  
        int oldCapacity = oldTable.length;  
    //如果之前的HashMap已經擴充打最大了,那麼就將臨界值threshold設置爲最大的int值  
        if (oldCapacity == MAXIMUM_CAPACITY) {  
            threshold = Integer.MAX_VALUE;  
            return;  
        }  
      
    //根據新傳入的capacity創建新Entry數組,將table引用指向這個新創建的數組,此時即完成擴容  
        Entry[] newTable = new Entry[newCapacity];  
        transfer(newTable, initHashSeedAsNeeded(newCapacity));  
        table = newTable;  
    //擴容公式在這兒(newCapacity * loadFactor)  
    //通過這個公式也可看出,loadFactor設置得越小,遇到hash衝突的機率就越小  
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);  
    }  
  
    //擴容之後,重新計算hash,然後再重新根據hash分配位置,  
    //由此可見,爲了保證效率,如果能指定合適的HashMap的容量,會更合適  
    void transfer(Entry[] newTable, boolean rehash) {  
        int newCapacity = newTable.length;  
        for (Entry<K,V> e : table) {  
            while(null != e) {  
                Entry<K,V> next = e.next;  
                if (rehash) {  
                    e.hash = null == e.key ? 0 : hash(e.key);  
                }  
                int i = indexFor(e.hash, newCapacity);  
                e.next = newTable[i];  
                newTable[i] = e;  
                e = next;  
            }  
        }  
    } 

    在addEntry時,會判斷是否到了當前HashMap的容量臨界值,如果到了,則進行擴容:

    擴容方式是直接將HashMap的數組長度翻倍,默認數組的長度爲16,負載因子爲0.75f,臨界值爲負載因子乘以數組長度。擴容時機爲key-value鍵值對也就是Entry的數量大於臨界值時,進行擴容,並在擴容後將原來的元素重寫排版。



    
發佈了73 篇原創文章 · 獲贊 13 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章