關於Hashmap的我的理解

剛剛看到QQ羣有人吹Hashmap,一想我啥都不懂,就趕快補了一波。下面分享一下我對Hashmap的理解,主要用於我的備忘。若是有不對,請批評。想要解鎖更多新姿式?請訪問http://blog.tengshe789.tech/node

總起算法

Hashmap是散列表,存儲結構是鍵值對形式。根據健的Hashcode值存儲數據,有較快的訪問速度。segmentfault

它的線程是不安全的,在兩個線程同時嘗試擴容HashMap時,可能將一個鏈表造成環形的鏈表,全部的next都不爲空,進入死循環;要想讓它安全,能夠用 Collections的synchronizedMap 方法使 HashMap具備線程安全的能力,或者使用ConcurrentHashMap 。數組

他的鍵值對均可覺得空,映射不是有序的。安全

Hashmap有兩個參數影響性能:初始容量,加載因子。數據結構

Hashmap存儲結構架構

JDK1.8中Hashmap是由鏈表、紅黑樹、數組實現的app

//用來實現數組、鏈表的數據結構
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//保存節點的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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

Hashmap構造方法函數

HashMap有4個構造方法。性能

代碼:

//方法1.制定初始容量和負載因子
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);
    }

//方法2.指定初始容量
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//方法三。無參構造。
     HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

//方法四。將另外一個 Map 中的映射拷貝一份到本身的存儲結構中來,這個方法不是很經常使用
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

Hashmap變量成員

//未指定容量的時候,數組的初始容量。初始容量是16
//爲何不直接寫16?由於速度快。計算機裏面要轉換二進制。
//必須2的n次冪
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//負載因子。當hashmap容量超過 容量*負載因子 時,進行擴容操做(resize())
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//肯定什麼時候將hash衝突的鏈表轉換成紅黑樹
static final int TREEIFY_THRESHOLD = 8;

//用來確什麼時候將紅黑樹轉換成鏈表
static final int UNTREEIFY_THRESHOLD = 6;

//當鏈表轉換成紅黑樹時,須要判斷數組容量。若數組容量過小致使hash衝突太多,則不進行紅黑樹操做,轉而利用reseize擴容
static final int MIN_TREEIFY_CAPACITY = 64;

初始容量、負載因子、閾值.

通常狀況下,使用無參構造方法建立 HashMap。但當咱們對時間和空間複雜度有要求的時候,使用默認值有時可能達不到咱們的要求,這個時候咱們就須要手動調參。

在 HashMap 構造方法中,可供咱們調整的參數有兩個,一個是初始容量initialCapacity,另外一個負載因子loadFactor。經過這兩個設定這兩個參數,能夠進一步影響閾值大小。但初始閾值 threshold 僅由initialCapacity 通過移位操做計算得出。

名稱 用途
initialCapacity HashMap 初始容量
loadFactor 負載因子
threshold 當前 HashMap 所能容納鍵值對數量的最大值,超過這個值,則需擴容

默認狀況下,HashMap 初始容量是16,負載因子爲 0.75。 註釋中有說明,閾值可由容量乘上負載因子計算而來 ,即threshold = capacity * loadFactor

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;
    }

這段代碼有點難,根據大神的說法,這個方法的意思是,找到大於或等於 cap 的最小2的冪。咱們先來看看 tableSizeFor 方法的圖解 :

圖中容量是229+1,計算後是230

引用一下啊大神說的:

對於 HashMap 來講,負載因子是一個很重要的參數,該參數反應了 HashMap 桶數組的使用狀況(假設鍵值對節點均勻分佈在桶數組中)。經過調節負載因子,可以使 HashMap 時間和空間複雜度上有不一樣的表現。當咱們調低負載因子時,HashMap 所能容納的鍵值對數量變少。擴容時,從新將鍵值對存儲新的桶數組裏,鍵的鍵之間產生的碰撞會降低,鏈表長度變短。此時,HashMap 的增刪改查等操做的效率將會變高,這裏是典型的拿空間換時間。相反,若是增長負載因子(負載因子能夠大於1),HashMap 所能容納的鍵值對數量變多,空間利用率高,但碰撞率也高。這意味着鏈表長度變長,效率也隨之下降,這種狀況是拿時間換空間。至於負載因子怎麼調節,這個看使用場景了。通常狀況下,咱們用默認值就能夠了。

插入PUT

過程:

  1. 對Key求hash值,而後計算下標
  2. 若是沒有碰撞,就放入桶中
  3. 若是碰撞了,就以鏈表形式放到後面
  4. 若是鏈表長度超過閾值,就把鏈表轉換成紅黑樹
  5. 若是鏈表存在則替換舊值
  6. 若是桶滿了(容量*負載因子),則從新resize

    public V put(K key, V value) {

    //調用核心方法
        return putVal(hash(key), key, value, false, true);
    }

putVal

核心算法在putVal()中。要想理解,先要明白桶排序(Bucket Sort)

它是迄今爲止最快的一種排序法,其時間複雜度僅爲Ο(n),也就是線性複雜度。

桶排序核心思想是:根據數據規模n劃分,m個相同大小的區間 (每一個區間爲一個桶,桶可理解爲容器) 。將n個元素按照規定範圍分佈到各個桶中去 ,再對每一個桶中的元素進行排序,排序方法可根據須要,選擇快速排序,或者歸併排序,或者插入排序 ,而後依次從每一個桶中取出元素,按順序放入到最初的輸出序列中(至關於把全部的桶中的元素合併到一塊兒) 。

下面是代碼:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //n是數組長度
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判斷桶數組是不是空
        if ((tab = table) == null || (n = tab.length) == 0)
            //是就用resize()初始化
            n = (tab = resize()).length;
        //根據 hash 值肯定節點在數組中的插入位置
        //若此位置沒有元素則進行插入,注意肯定插入位置所用的計算方法爲 (n - 1) & hash,因爲 n 必定是2的冪次,這個操做至關於hash % n
        if ((p = tab[i = (n - 1) & hash]) == null)
            //將新節點引入桶中
            tab[i] = newNode(hash, key, value, null);
        else {
            //臨時變量e進行記錄。若是有值,說明僅僅是值的覆蓋。
            Node<K,V> e; K k;
            // 若是鍵的值以及節點 hash 等於鏈表中的第一個鍵值對節點時,則將 e 指向該鍵值對
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)// 若是桶中的引用類型爲 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);
                        // 鏈表中不包含要插入的鍵值對節點時,則將該節點接在鏈表的最後
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //臨時變量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;
    }

HASH

hash算法,高十六位與低十六進行異或運算,這樣作的好處是使獲得結果會盡量不一樣。

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

resize

HashMap 的擴容機制與其餘變長集合的套路不太同樣,HashMap 按當前桶數組長度的2倍進行擴容,閾值也變爲原來的2倍(若是計算過程當中,閾值溢出歸零,則按閾值公式從新計算)。擴容以後,要從新計算鍵值對的位置,並把它們移動到合適的位置上去。

resize總共作了3件事,分別是:

  1. 計算新桶數組的容量 newCap 和新閾值 newThr
  2. 根據計算出的 newCap 建立新的桶數組,桶數組 table 也是在這裏進行初始化的
  3. 將鍵值對節點從新映射到新的桶數組裏。若是節點是 TreeNode 類型,則須要拆分成黑樹。若是是普通節點,則節點按原順序進行分組。

    //resize()函數在size > threshold時被調用
    final Node<K,V>[] resize() {

    Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //oldCap大於 0 表明原來的 table 非空
        if (oldCap > 0) {
            // 當 table 容量超過容量最大值,則再也不擴容
            if (oldCap >= MAXIMUM_CAPACITY) {
                //閾值設爲整形最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }// 按舊容量和閾值的2倍計算新容量和閾值的大小
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            /*
            *oldCap 小於等於 0 且 oldThr 大於0,表明用戶建立了一個 HashMap,可是使用的構造函數爲
       * HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)
       * 或 HashMap(Map<? extends K, ? extends V> m),致使 oldTab 爲 null,oldCap 爲0,
       * oldThr 爲用戶指定的 HashMap的初始容量
            */
            newCap = oldThr;
        else { //設置新容量和新閾值大小
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
            // newThr 爲 0 時,按閾值計算公式進行計算
        if (newThr == 0) {
            //計算新閾值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //準備初始化過程
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //遍歷。把 oldTab 中的節點 reHash 到 newTab 中去
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //判斷老數組是否爲空
                if ((e = oldTab[j]) != null) {
                    //不爲空,設成空
                    oldTab[j] = null;
                    //若節點是單個節點,直接從新分配定位
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //若節點是 TreeNode 節點,要進行 紅黑樹的 rehash 操做
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //如果鏈表,進行鏈表的 rehash 操做
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 遍歷鏈表,並將鏈表節點按原順序進行分組
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // rehash 後節點新的位置必定爲原來基礎上加上 oldCap
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
}

關於HashMap在何時時間複雜度是O(1),何時是O(n),何時又是O(logn)的問題

O(1):鏈表的長度儘量短,理想狀態下鏈表長度都爲1

O(n):當 Hash 衝突嚴重時,若是沒有紅黑樹,那麼在桶上造成的鏈表會變的愈來愈長,這樣在查詢時的效率就會愈來愈低;時間複雜度爲O(N)。

O(logn):採用紅黑樹以後能夠保證查詢效率O(logn)

手寫

/**
 * @author tengshe789
 */
public class 手寫HashMap {
    public static class Node<K,V>{
        K key;
        V value;
        Node<K,V> next;
        public Node(K key, V value, Node<K, V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public K getKey() {
            return this.key;
        }

        public V getValue() {
            return this.value;
        }

        public V setValue(V value) {
            this.value=value;
            return this.value;
        }
    }

    public static class HashMap<K, V>{
        /*數據存儲的結構==>數組+鏈表*/
        Node<K,V>[] array=null;

       /* 哈希桶的長度 */
        private static int defaultLength=16;

        /*加載因子/擴容因子*/
        private static double factor=0.75D;

        /*集合中的元素個數*/
        private int size;

        /*打印函數*/
        public void print() {
            System.out.println("===============================");
            if(array!=null) {
                Node<K, V> node=null;
                for (int i = 0; i < array.length; i++) {
                    node=array[i];
                    System.out.print("下標["+i+"]");
                    //遍歷鏈表
                    while(node!=null) {
                        System.out.print("["+node.getKey()+":"+node.getValue()+"]");
                        if(node.next!=null) {
                            node=node.next;
                        }else {
                            //到尾部元素
                            node=null;
                        }
                    }
                    System.out.println();
                }

            }
        }

        //put元素方法
        public V put(K k, V v) {

            //1.懶加載機制,使用的時候進行分配
            if(array==null) {
                array=new Node[defaultLength];
            }

            //2.經過hash算法,計算出具體插入的位置
            int index=position(k,defaultLength);


            //擴容。判斷是否須要擴容
            //擴容的準則,元素的個數   大於  桶的尺寸*加載因子
            if(size > defaultLength*factor) {
                resize();
            }

            //3.放入要插入的元素
            Node<K, V> node=array[index];
            if(node==null) {
                array[index]=new Node<K,V>(k,v,null);
                size++;
            }else {
                if(k.equals(node.getKey()) || k==node.getKey()) {
                    return node.setValue(v);
                }else {
                    array[index]=new Node<K,V>(k,v,node);
                    size++;
                }
            }

            return null;
        }

        //擴容,而且從新排列元素
        private void resize() {
            //翻倍擴容
            //1.建立新的array臨時變量,至關於defaultlength*2
            Node<K, V>[] temp=new Node[defaultLength << 1];

            //2.從新計算散列值,插入到新的array中去。 code=key % defaultLength ==> code=key % defaultLength*2
            Node<K, V> node=null;
            for (int i = 0; i < array.length; i++) {
                node=array[i];
                while(node!=null) {
                    //從新散列
                    int index=position(node.getKey(),temp.length);
                    //插入頭部
                    Node<K, V> next = node.next;
                    //3
                    node.next=temp[index];
                    //1
                    temp[index]=node;
                    //2
                    node=next;

                }
            }

            //3.替換掉老array
            array=temp;
            defaultLength=temp.length;
            temp=null;


        }

        private int position(K k,int length) {
            int code=k.hashCode();

            //取模算法
            return code % (length-1);

            //求與算法
            //return code & (defaultLength-1);
        }

        public V get(K k) {
            if(array!=null) {
                int index=position(k,defaultLength);
                Node<K, V> node=array[index];
                //遍歷鏈表
                while(node!=null) {
                    //若是key值相同返回value
                    if(node.getKey()==k) {
                        return node.getValue();
                    } else
                        //若是key值不一樣則調到下一個元素
                    {
                        node=node.next;
                    }
                }
            }


            return null;
        }


    }

        public static void main(String[] args) {
            HashMap<String, String> map=new HashMap<String, String>();
            map.put("001號", "001");
            map.put("002號", "002");
            map.put("003號", "003");
            map.put("004號", "004");
            map.put("005號", "005");
            map.put("006號", "006");
            map.put("007號", "007");
            map.put("008號", "008");
            map.put("009號", "009");
            map.put("010號", "010");
            map.put("011號", "011");
            map.print();

            System.out.println("========>"+map.get("009號"));
        }

    }

參考資料

coolblog

阿里架構師帶你分析HashMap源碼實現原理

感謝!

如下來自n天后的我:

補充一下看到一個很是好的:點擊連接,值得學習

想要了解更多精彩新姿式?請訪問個人我的博客 本篇爲原創內容,已在我的博客率先發表,隨後CSDN,segmentfault,juejin同步發出。若有雷同,那真是緣分~

相關文章
相關標籤/搜索