HashMap如何添加元素詳解

Map接口結構

map接口是一個雙邊隊列,擁有key,value兩個屬性,其中key在存儲的集合中不允許重複,value可以重複。

MapHashMapLinkedHashMapHashtable實現map接口實現map接口繼承HashMap實現map接口MapHashMapLinkedHashMapHashtable

HashMap特點

  • 存儲結構在jdk1.7當中是數組加鏈表的結構,在jdk1.8當中改爲了數組加鏈表加紅黑樹的結構。
  • HashMap在多線程的環境下是不安全的,沒有進行加鎖措施,所以執行效率快。如果我麼需要有一個線程安全的HashMap,可以使用Collections.synchronizedMap(Map<K,V> m)方法獲得線程安全的HashMap,也可以使用ConcurrentHashMap類創建線程安全的map
  • 存儲的元素在jdk1.7當中是Entry作爲存儲的節點,在jdk1.8當中用Node作爲存儲的節點,Node實現了Map.Entry接口。在map中其實是將keyvalue節點組合起來成爲一個Node節點作爲存儲。
// jdk1.8Node節點
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

		// 下面省略代碼
  }
  • HashMapkey不允許重複,value可以允許重複,並且keyvalue都允許是null值,因爲他們是另外存儲的。
  • 當我們使用自定義類作爲HashMap的key時我們需要重寫object類的,hashCode()equals()方法。
  • HashMap中鏈表部分在jdk1.7中新的節點總是在鏈表的頭部,舊的節點在新節點的next域當中,而在jdk1.8中是新的節點總是在鏈表的尾部,他們的指向都是由舊節點指向新的節點,由此我們可以記成七上八下

HashMap當中常見的名詞

  • DEFAULT_INITIAL_CAPACITY 默認數組容量大小,16
  • MAXIMUM_CAPACITY 最大數組容量大小,2^30
  • DEFAULT_LOAD_FACTOR 默認的負載因子,0.75
  • TREEIFY_THRESHOLD 鏈表改成紅黑樹存儲的最小長度
  • MIN_TREEIFY_CAPACITY 鏈表改成紅黑樹存儲,map數組最小容量

HashMap存儲元素過程源碼解析(jdk1.8)


	// 存儲元素的數組,加上transient關鍵字代表不可以被序列化
	transient Node<K,V>[] table;
	
	// 這個方法是HashMap對外公開的添加元素方法
	public V put(K key, V value) {

		// 實際調用裏面的一個默認修飾符的方法
        return putVal(hash(key), key, value, false, true);
    }
    
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        
        // 在jdk1.7當中,創建一個HashMap實例是直接分配一個長度爲16的數組,
        // 在jdk1.8當中,創建HashMap時是分配了一個長度爲0的數組,然後調用put方法時,如果數組長
        // 度爲0時,則調用數組擴容方法,擴容數組到長度爲16。當長度不爲0時,擴容操作是擴大到原來
        // 長度的兩倍
        if ((tab = table) == null || (n = tab.length) == 0)
			
			// 調用擴容方法,並利用n變量,記錄數組的長度
            n = (tab = resize()).length;
        
        // 通過計算哈希散列,得出該key應該放在數組的哪一個位置上,用變量p存儲該位置上的元素
        // 判斷該位置上是否已經存在元素,如果不存在元素則直接將該元素放入數組中,存放
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

		// 如果數組的該位置已經存在了元素
        else {
            Node<K,V> e; K k;
            // 比較需要加入的元素,於原來的元素hash值進行比較,如果hash值和equals都爲true那麼
            // 判定兩個元素爲相同元素,用變量e來記錄原來的元素,方便下面進行替換
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                
            // 如果上述條件不滿足,但是是一個TreeNode類型,則當作樹節點插入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            // 當hash值不同,或者equals爲false,也不是TreeNode時將新元素插入數組對應位置的
            // 鏈表中,遍歷對應數組位置的鏈表
                for (int binCount = 0; ; ++binCount) {

					// 用變量e存儲鏈表的下一個節點
                    if ((e = p.next) == null) {
                    
                    	// 如果該鏈表只有一個元素,則直接將舊節點的next域指向新的節點
                        p.next = newNode(hash, key, value, null);

						// 當單條鏈表上元素數量大於最大數量時則按照紅黑樹存儲8,在進行紅黑樹時
						// 又會進行判斷數組容量是否到達紅黑樹最小容量64,兩個條件同時滿足,則該
						// 條鏈表改造成紅黑樹存儲
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 比較鏈表的下一個節點的hash值和equals
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
			
			// 如果e不等於null則證明,在鏈表中存在相同的元素,則進行替換
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

	// 將鏈表改造成紅黑樹
	final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
		
		// 判斷是否滿足條件,數組大小達到64,沒有達到則做擴容操作,達到則改成紅黑樹
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

HashMap常見的問題

爲什麼在使用自定義類型作爲key需要我們重寫hashCode()equals()方法

因爲map的存值的時候是先計算hash值,然後再判斷equals,通過這兩個值是否都爲true來判斷該元素是否再map中已經存在。如果不重寫這兩個方法,可能會存在我們認爲相同,但是他們的hash或者equals不同的元素也能存進map中,從而達不到key唯一的效果。

HashMap中爲什麼要存在DEFAULT_LOAD_FACTOR負載因子這個概念

因爲我們map的存儲數據結構是數組加鏈表加紅黑樹結構,如果沒有負載因子,map是不可能滿的,所以加上一個負載因子的概念來判斷數組是否擴容,減少鏈表的負擔。

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