HashMap學習筆記

hashmap的 擴容機制

上一篇說了,hashmap的構造器裏指明瞭兩個對於理解HashMap比較重要的兩個參數 int initialCapacity, float loadFactor,這兩個參數會影響HashMap效率,HashMap底層採用的散列數組實現,利用initialCapacity這個參數我們可以設置這個數組的大小,也就是散列桶的數量,但是如果需要Map的數據過多,在不斷的add之後,這些桶可能都會被佔滿,這是有兩種策略,一種是不改變Capacity,因爲即使桶佔滿了,我們還是可以利用每個桶附帶的鏈表增加元素。但是這有個缺點,此時HaspMap就退化成爲了LinkedList,使get和put方法的時間開銷上升,這是就要採用另一種方法:增加Hash桶的數量,這樣get和put的時間開銷又回退到近於常數複雜度上。Hashmap就是採用的該方法。

關於擴容,看hashmap的擴容方法,resize方法,它的源碼如下:

//擴容方法
// 重新調整HashMap的大小,newCapacity是調整後的單位
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        // 新建一個HashMap,將“舊HashMap”的全部元素添加到“新HashMap”中,
        // 然後,將“新HashMap”賦值給“舊HashMap”。
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int) (newCapacity * loadFactor);
    }

很明顯,是從新建了一個HashMap的底層數組,長度爲原來的兩倍,而後調用transfer方法,將舊HashMap的全部元素添加到新的HashMap中(要重新計算元素在新的數組中的索引位置)。transfer方法的源碼如下:

// 將HashMap中的全部元素都添加到newTable中
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K, V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K, V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

很明顯,擴容是一個相當耗時的操作,因爲它需要重新計算這些元素在新的數組中的位置並進行復制處理。因此,我們在用HashMap時,最好能提前預估下HashMap中元素的個數,這樣有助於提高HashMap的性能。

hashmap什麼時候需要增加容量呢?

因爲效率問題,JDK採用預處理法,這時前面說的loadFactor就派上了用場,當size > initialCapacity * loadFactor,hashmap內部resize方法就被調用,使得重新擴充hash桶的數量,在目前的實現中,是增加一倍,這樣就保證當你真正想put新的元素時效率不會明顯下降。所以一般情況下HashMap並不存在鍵值放滿的情況。當然並不排除極端情況,比如設置的JVM內存用完了,或者這個HashMap的Capacity已經達到了MAXIMUM_CAPACITY(目前的實現是2^30)。

initialCapacity和loadFactor參數設什麼樣的值好呢?

initialCapacity的默認值是16,有些人可能會想如果內存足夠,是不是可以將initialCapacity設大一些,即使用不了這麼大,就可避免擴容導致的效率的下降,反正無論initialCapacity大小,我們使用的get和put方法都是常數複雜度的。這麼說沒什麼不對,但是可能會忽略一點,實際的程序可能不僅僅使用get和put方法,也有可能使用迭代器,如initialCapacity容量較大,那麼會使迭代器效率降低。所以理想的情況還是在使用HashMap前估計一下數據量。

加載因子默認值是0.75,是JDK權衡時間和空間效率之後得到的一個相對優良的數值。如果這個值過大,雖然空間利用率是高了,但是對於HashMap中的一些方法的效率就下降了,包括get和put方法,會導致每個hash桶所附加的鏈表增長,影響存取效率。如果比較小,除了導致空間利用率較低外沒有什麼壞處,只要有的是內存,畢竟現在大多數人把時間看的比空間重要。但是實際中還是很少有人會將這個值設置的低於0.5。

HashMap的key和value都能爲null麼?如果k能爲null,那麼它是怎麼樣查找值的?

如果key爲null,則直接從哈希表的第一個位置table[0]對應的鏈表上查找。記住,key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中。

HashMap中put值的時候如果發生了衝突,是怎麼處理的?

JDK使用了鏈地址法,hash表的每個元素又分別鏈接着一個單鏈表,元素爲頭結點,如果不同的key映射到了相同的下標,那麼就使用頭插法,插入到該元素對應的鏈表。

HashMap的key是如何散列到hash表的?相比較HashTable有什麼改進?

我們一般對哈希表的散列很自然地會想到用hash值對length取模(即除留餘數法),HashTable就是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,且hashtable直接使用了hashcode值,沒有重新計算。

HashMap中則通過 h&(length-1) 的方法來代替取模,其中h是key的hash值,同樣實現了均勻的散列,但效率要高很多,這也是HashMap對Hashtable的一個改進。

接下來,我們分析下爲什麼哈希表的容量一定要是2的整數次冪。

首先,length爲2的整數次冪的話,h&(length-1) 在數學上就相當於對length取模,這樣便保證了散列的均勻,同時也提升了效率;

其次,length爲2的整數次冪的話,則一定爲偶數,那麼 length-1 一定爲奇數,奇數的二進制的最後一位是1,這樣便保證了 h&(length-1) 的最後一位可能爲0,也可能爲1(這取決於h的值),即與後的結果可能爲偶數,也可能爲奇數,這樣便可以保證散列的均勻,而如果length爲奇數的話,很明顯 length-1 爲偶數,它的最後一位是0,這樣 h&(length-1) 的最後一位肯定爲0,即只能爲偶數,這樣導致了任何hash值都只會被散列到數組的偶數下標位置上,浪費了一半的空間,因此length取2的整數次冪,是爲了使不同hash值發生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。

作爲對比,再討論一下Hashtable

HashTable同樣是基於哈希表實現的,其實類似HashMap,只不過有些區別,HashTable同樣每個元素是一個key-value對,其內部也是通過單鏈表解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。

HashTable比較古老, 是JDK1.0就引入的類,而HashMap 是 1.2 引進的 Map 的一個實現。

HashTable 是線程安全的,能用於多線程環境中。Hashtable同樣也實現了Serializable接口,支持序列化,也實現了Cloneable接口,能被克隆。

這裏寫圖片描述

Hashtable繼承於Dictionary類,實現了Map接口。Dictionary是聲明瞭操作”鍵值對”函數接口的抽象類。 有一點注意,HashTable除了線程安全之外(其實是直接在方法上增加了synchronized關鍵字,比較古老,落後,低效的同步方式),還有就是它的key、value都不爲null。另外Hashtable 也有 初始容量加載因子

public Hashtable() {
    this(11, 0.75f);
}

默認加載因子也是 0.75,HashTable在不指定容量的情況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量一定要爲2的整數次冪,而HashMap則要求一定爲2的整數次冪。因爲HashTable是直接使用除留餘數法定位地址。且Hashtable計算hash值,直接用key的hashCode()。

還要注意:前面說了Hashtable中key和value都不允許爲null,而HashMap中key和value都允許爲null(key只能有一個爲null,而value則可以有多個爲null)。但如在Hashtable中有類似put(null,null)的操作,編譯同樣可以通過,因爲key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規範規定的。

最後針對擴容:Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍。

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