HashMap、TreeMap詳解

java面試總結(三)------HashaMap、TreeMap

HashMap和TreeMap作爲最常用同時也是最容易被考察的點來說,掌握是至關重要的

  • HashMap
    基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。

    基於數組(Node[] table)和鏈表結合組成的複合結構,數組被分爲一個個桶(bucket),通過哈希值決 定了鍵值對在這個數組的尋址;哈希值相同的鍵值對,則以鏈表形式存儲,參考下面的示意圖。這裏 需要注意的是,如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),圖中的鏈表就會被改造爲樹形結構。
    在這裏插入圖片描述HashMap有四個構造函數,如下:

	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

	public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

從上至下分別是 無參(以默認負載因子構造HashMap)、帶初始容量參(以初始容量參和默認負載因子構造HashMap)、以初始容量和負載因子爲參以另一個Map爲參(此處不做重點)

我們着重看第三個構造函數,即以初始容量和負載因子爲參的構造函數,在源碼中,先經歷了一系列的非法性判斷,然後初始化負載因子,然後 tableSizeFor(initialCapacity) ,其中tableSizeFor(initialCapacity)源碼如下:

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

看不懂?沒關係 ,看這篇資料,這裏不做贅述,其實這個函數的意思是 返回大於輸入參數且最近的2的整數次冪的數,即輸入 10 返回 16,記住這個函數,這個會非常重要,那麼爲什麼要這麼做呢?請看文章最下面。

然後下面就講最基礎的幾個api

 	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
		Node<K,V>[] tab; Node<K,V> p; int , i;
 		if ((tab = table) == null || (n = tab.length) = 0)
 			n = (tab = resize()).legth;
 		if ((p = tab[i = (n - 1) & hash]) == ull)
 		tab[i] = newNode(hash, key, value, nll);
 		 else {
		 // ...
 		if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for frs 
		 treeifyBin(tab, hash);
 		// ... 
 		}
	}
	public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    public void clear() {
        Node<K,V>[] tab;
        modCount++;
        if ((tab = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }

在構造函數中,發現僅僅只是初始化了參數,並沒有進行其他操作,是按照lazy-load原則,在首次使用時被初始化(拷貝構造函數除外),然後看 put函數,裏面調用了putVal() 函數,
putVal()解析如下:

  • 如果表格是null,resize方法會負責初始化它,這從tab = resize()可以看出。

  • resize方法兼顧兩個職責,創建初始存儲表格,或者在容量不滿足需求的時候,進行擴容(resize)。

  • 在放置新的鍵值對的過程中,如果發生下面條件,就會發生擴容。

	if (++size > threshold)
 		resize();
  • 具體鍵值對在哈希表中的位置(數組index)取決於下面的位運算
	i = (n - 1) & hash

爲什麼要做以上的位運算呢,也請看文章最最下面的問題部分。

2. TreeMap

TreeMap稍後詳解

問題

  1. 擴容後,HashMap中原來的元素是怎麼儲存的
    參考資料
    即 :
    如果無鏈,原來的元素,要麼在原位置,要麼在原位置+原數組長度 那個位置上。
    如果有鏈,

    查閱權威資料再更

  2. 爲什麼每次擴容後大小必須是2的n次方&&爲什麼求下標是(n - 1) & hash?

    這兩個問題可以一起回答,在源碼中可以看到,每次擴容包括初始容量16必須是2的n次方,爲什麼呢?
    其實很容易回答,先回答另一個問題,在默認情況下(容量=16)怎麼保證一個32位的二進制串在0-15中分佈?大部分同學可能回答是取餘,是的,在大部分情況看來,取餘似乎是個不錯的選擇,但是取餘會進行除法,比較慢,所以java8中提供了這麼一種方法:

    對於默認情況,16=2的4次方,轉成二進制即10000,然後按照源碼公式,(10000-1)&hash,如圖

在這裏插入圖片描述

這也就很好的解釋了爲什麼容量必須是2的n次方,是爲了滿足按位與得出下標值的運算的條件,其原理是容量-1的二進制一定全是1,然後再與hash值做 按位與 運算,就能得到一個處於 0 - 容量 的大小的二進制串,也就得到它的下標,所以直接使用位運算速度快,且分佈儘量在均勻範圍內。

如果容量不是2的n次方,那麼容量-1的二進制一定不全是1,如果用此值進行按位與操作,那麼某一位是0的情況下會導致某個哈系桶將永遠得不到儲存,就違背了儘量均勻分佈的原則.

  1. 如果兩個鍵的hashcode相同,如何獲取值對象
    找到參考資料再更

  2. 爲什麼默認負載因子是0.75
    在理想情況下,使用隨機哈希碼,節點出現的頻率在hash桶中遵循泊松分佈,同時給出了桶中元素個數和概率的對照表。

    從上面的表中可以看到當桶中元素到達8個的時候,概率已經變得非常小,也就是說用0.75作爲加載因子,每個碰撞位置的鏈表長度超過8個是幾乎不可能的.
    參考

參考資料 :
HashMap

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