深入理解Map之HashMap

map 主要有四個實現類:

      HashMap、Hashtable、LinkedHashMap、TreeMap

LinkedHashMap:

有序,按照順序插入數據,根據Iterator遍歷時,先插的先得到。

TreeMap:

是SortedMap接口的實現類,默認按照鍵值的升序保存數據,也可以指定排序的比較器,key必須實現Comparable接口或者構造map時傳入自定義的Comparable,否則會拋ClassCastException異常

HashMap:

線程不安全,可用synchronizedMap或者ConcurrentHashMap代替便線程安全了

根據鍵的HashCode值來存儲數據

最多允許一條記錄的鍵爲null,允許多個值爲null

HashMap 詳解:

   底層結構:數組 + 鏈表 + 紅黑樹

以上是hashmap的結構,每一個黑點表示一個Node,其中的Node是什麼呢,來看一下源碼:

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;
        }
   ......此處省略部分方法,詳細請看jdk1.8源碼
}

Node是HashMap 的內部類,實現了Map.Entry接口,

hash用來定位數組索引的位置, next表示鏈表的下一個Node。 而Node[] table 表示哈系桶數組,初始化長度默認16

HashMap是使用哈希表來存儲的,哈希表爲了解決哈希衝突,用開放地址法鏈地址法,HashMap採用鏈地址法,即數組和鏈表結合,每個數組元素上都有個鏈表結構,當數組被hash後,得到數組下標,然後把數組放在對應下標的鏈表裏。

下面先介紹下HashMap的構造函數,瞭解一下內部構造:

HashMap有四個構造函數,默認無參構造和參數是Map的構造函數是Java規範推薦實現的,還有兩個是HashMap專門提供的。

    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(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

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

   
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

threshold: 所能容納的key-value對的極限,即HashMap擴容的閾值。等於容量(數組長度)乘以負載因子(length * loadFactor)

loadFactor:負載因子,默認0.75   (用於衡量散列表的空間使用程度)

modCount:字段主要用來記錄HashMap內部結構發生變化的次數

size:這個字段其實很好理解,就是HashMap中實際存在的鍵值對數量

length:數組(table)長度

負載因子默認值0.75是對空間和時間效率的一個平衡選擇。

//默認初始容量爲16(2的4次方),該數必須爲2的冪次
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量 30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//當put一個元素時,其鏈表長度達到8時將鏈表轉換爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;

//鏈表長度小於6時,解散紅黑樹
static final int UNTREEIFY_THRESHOLD = 6;

//默認的最小的擴容量64,爲避免重新擴容衝突,至少爲4 * TREEIFY_THRESHOLD=32,即默認初始容量的2倍
static final int MIN_TREEIFY_CAPACITY = 64;

在HashMap中,哈系桶數組table的長度length設置成2的n次方,這是非常規設置(一般設置爲素數),這樣做主要是爲了取模和擴容時的優化,減少衝突。

儘管這樣,依然避免不了會出現拉鍊過長的情況,一旦過長,就會嚴重影響性能,jdk1.8中 進行了優化,加入了紅黑樹的結果,當鏈表長度過長(默認爲8)時,鏈表就轉爲紅黑樹結構。

 

下面講一下HashMap 的get,put方法的實現原理:

一  如何確定哈希桶數組索引的位置:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

Hash算法包括:取key的hashCode值,高位運算,取模運算

二  HashMap的put方法:

下面看一下put方法的源碼以及大概的分析:

    public V put(K key, V value) {
        //對key的hashCode做hash
        return putVal(hash(key), key, value, false, true);
    }
//這裏onlyIfAbsent表示只有在該key對應原來的value爲null的時候才插入,也就是說如果value之前存在了,就不會被新put的元素覆蓋。
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //若tab爲空,則創建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //計算index(即根據key計算hash值得到數組的索引),並對null做處理。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //若節點key的hash值相同,則覆蓋value
            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 {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //鏈的長度大於8,則轉爲紅黑樹處理
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //key 已經存在,直接覆蓋value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            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;
    }

下圖爲對HashMap的put方法的分析過程圖示例:

簡單來講,HashMap的put方法是基於hashing原理,在put方法傳遞鍵值對時,先對鍵調用hashCode方法,其返回的hashCode的值來確定桶的位置,即數組的索引來存儲鍵,來作爲Map.Entry

三  HashMap的hash衝突問題:

產生原因:    在put方法在hashCode獲取hash值時,當put的元素越來越多時,難免產生不同的key產生相同的hash值問題,此時,便造成了hash衝突問題。

解決方法:    鏈表結構解決衝突問題

       當存儲時,若hash值相同,則會找到相同的bucket的位置,此時發生碰撞,但由於是鏈表結構,每個Map.Entry都有一個next指針,

       獲取時,調用equals方法。並且java8中的紅黑樹結構,更加大大減少了查詢時的複雜度。

減少碰撞方法:使用final修飾 或 不可變對象作爲鍵(例如:Integer,String),因爲這些已經重寫了equals方法和hashcode方法

另外:存入相同的key時,獲取的是後put的數據。

四  HashMap的擴容機制:

       擴容就是當前容量不夠存儲數據時,進行擴容,resize方法。下面看一下簡單的擴容過程示意圖:

前半部分講到,默認的容量爲16,且可修改,但必須爲2的冪次,擴容就是將原來的容量擴大2倍,jdk1.8做了些優化,

因爲是擴大2倍,所以,元素要麼在遠位置,要麼在原位置移動2次冪的位置。

JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置。

而1.8做了些優化,並不會重新計算hash值,當擴容後,n變爲2倍,n-1的範圍多出了一個bit字節,通過這個bit是0還是1判斷,0則是索引沒變,1則是索引變化,變爲“原索引+oldCap”  resize()這塊的源碼暫時還未仔細看,有興趣的可以看看,一起溝通研究下。

五.  線程安全問題:

        文章開頭說到,HashMap是線程不安全的,在併發環境中,會造成死循環,爲什麼呢?

以jdk1.7 爲例,重新調整map大小會出現競爭問題,在多線程中,map調整大小的過程中,即擴容重哈希時,存儲在鏈表的元素的次序會倒過來,因爲移動到新的bucket位置時,HashMap將元素放在頭部而不是尾部(避免尾部遍歷),這樣導致Entry鍊形成環,若競爭發生,這樣會發生死循環。導致線程不安全。多線程建議使用ConcurrentHashMap(採用了分段加鎖技術)

 

 

 

 

 

 

 

 

 

 

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