硬核HashMap源碼分析,HashMap文章中的聖經

一 前言

本篇是繼硬核ArrayList源碼分析,答應我每天看一遍好麼之後進行的第二篇源碼分析;學完本篇你將對hashMap的結構和方法有個全面的瞭解;面試自己有多強,超人都不知道;比如HashMap的擴容機制,最大容量是多少,HashMap鏈表是如何轉到紅黑樹,HashMap爲什麼線程不安全,HashMap的key,value是否能null值等等;

知識追尋者(Inheriting the spirit of open source, Spreading technology knowledge;)

二 源碼分析

2.1 官方說明實踐

  • 官方說**HashMap實現了Map接口,擁有所有的Map操作;並且允許key,value都爲null;**實踐一下是不是這樣子;
    public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>();
        map.put(null,null);
        System.out.println(map);//{null=null}
    }

果然如此沒有報錯;再加一個null的key和value看看;不貼代碼啊了,最終輸出還是一個{null=null};說明HashMap的key是唯一的;?怎麼保證key唯一後面繼續分析;

  • 繼續看官方說明,HashMap與HashTable不同之處是,HashMap不同步,HashTable不允許key,value爲null;實踐一下
    public static void main(String[] args) {
        Hashtable hashtable = new Hashtable();
        hashtable.put(null,null);
    }

控制檯紅了啊!!!輸出內容如下,震驚,HashTable的key,value真的不能爲null;

{null=null}Exception in thread "main" 
java.lang.NullPointerException
	at java.util.Hashtable.put(Hashtable.java:459)
	at test.HashMapTest.main(HashMapTest.java:17)

Process finished with exit code 1

點擊源碼看看,HashTable的put方法特大號的Make開頭確保key,value不能爲null,synchronized修飾;人生值得懷疑一下,因爲知識追尋者以前都沒用過Hashtable耍代碼;

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
  • 繼續看官方說明,HashMap無法保證Map中內容的順序;再次動手實踐一下,如果如此,到底啥原有讓map順序不一致呢?後續分析
    public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("清晨","娃哭了");
        map.put("中午","娃餓了");
        map.put("晚上","娃不想睡覺了");
        System.out.println(map);//{中午=娃餓了, 晚上=娃不想睡覺了, 清晨=娃哭了}
    }

後續的官方說明就不是單單能幾句話可以描述的懂了,開胃菜完畢,還是看看具體的源碼吧,複習的時候記得看下小技巧Tip;

Tip:

HashMap允許key和value都爲null;key具有唯一性;

HashMap與HashTable不同之處是,HashMap不同步,HashTable不允許key,value爲null;

HashMap無法保證Map中內容的順序

2.2 空參構造方法分析

測試代碼

   public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>();
    }

構造方法源碼如下,發行HashMap 的加載因子loadFactor 的值是默認值0.75;

  public HashMap() {//默認因子DEFAULT_LOAD_FACTOR=0.75;初始化容量爲16
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

走了一遍父類如下

HashMap --> AbstractMap --> Object

看下HashMap的父類圖譜,結構倒是很簡單對吧;

看到這邊,知識追尋者有2個疑問

問題一 : 負載因子0.75是什麼?

問題二:初始化容量16在哪裏?

先回答一下初始化容量16問題,知識追尋者找到成員變量,默認初始化容量字段值爲16;而且默認初始化值必須是2的倍數;

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16默認的初始化容量

回答負載因子0.75是指HashMap的一個擴容機制;比如 擴容因子是0.7,假設容量爲10;當插入的數據達到第10*0.7 = 7 時就會發生擴容;那麼知識追尋者再計算一下HashMap的擴容算法,0.75 * 16 = 12;那麼也就是插入12 個數之後HashMap會發生擴容;

Tip: HashMap默認因子0.75;初始化容量16;如果容量是12,插入12個數之後再插入數據會發生擴容;

2.3 HashMap實現原理分析

官方源碼的說明如下:HashMap的實現是由桶和哈希表實現,當桶的容量過於龐大時會轉爲樹節點(與TreeMap類似,由於TreeMap底層是紅黑樹實現,故說桶轉爲紅黑樹也是對的);

拋出一個問題什麼是桶?

回答桶之前我們先了解一下什麼是哈希表(HashTable)

哈希表本質是一個鏈表數組;經過Hash算法計算後得出的int值找到數組上對應的位置;丟一張圖給讀者們看看;

知識追尋者真是爲讀者操碎了心,再丟一個計算hash值的方法給你們;記得溫習一下java基礎String類的hashCode() 方法;若2個對象相同,hash值相同;若2個字符串內容相同(equals方法),hash值相同;

    public static void main(String[] args) {
        int hash_morning = Objects.hashCode("清晨");
        int hash_night = Objects.hashCode("晚上");
        System.out.println("清晨的hash值:"+hash_morning);//清晨的hash值:899331
        System.out.println("晚上的hash值:"+hash_night);//晚上的hash值:832240

    }

繼續說我們的桶,一個桶就是一個鏈表中的每個節點,你也可以理解爲一個下圖中除了tab上的的entry ;看圖

當桶的容量達到一定數量後就會轉爲紅黑樹,看圖

看到這邊的讀者肯定收穫良多了,這還是開胃菜而已,進入我們今天的正題,源碼分析篇;

2.4 put方法源碼分析

測試代碼

    public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("清晨","娃哭了");
        System.out.println(map);
    }

首先進入的是put方法裏面包含了putVal方法

  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);//
    }

先對key進行了一次hash再作爲putVal裏面的hash

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

看看putVal方法內容,因爲只插入一個數據,索引爲空的情況不會走else裏面內容,知識追尋者這裏省略了;

很清晰的可以看見創建了一個節點(Node<K,V>會在下面給出)放入了tab中,tab也很好理解,就是一個存放Node的列表;其索引 i = (容量 - 1) & hash值 , 本質上就是hash值拿去與的結果,所以是個哈希表;

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {//evict =true
        Node<K,V>[] tab; Node<K,V> p; int n, i;// 聲明 樹數組, 樹節點,n,i
        if ((tab = table) == null || (n = tab.length) == 0)//table 爲空
            n = (tab = resize()).length; // resize()是 Node<K,V>[] resize() n 就是樹的長度 根據初始容量就是 n=16
        if ((p = tab[i = (n - 1) & hash]) == null) // i = (16-1) & hash ---> 15 & 899342 = 14 ;
            tab[i] = newNode(hash, key, value, null);// 創建新節點,並插入數據
        else {
            .......
        }
        ++modCount;//集合結構修改次數+1
        if (++size > threshold)// size=1 ; threshold=12
            resize();
        afterNodeInsertion(evict);
        return null;
    }

這邊做了個圖記錄一下

Node代碼如下

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//哈希值 key value異或的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 int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        } 
...........

知識追尋者又加了一個晚上進去,跑過代碼後轉爲圖如下

這麼知識追尋者提出個想法,15的二進制是 1111 ,任何一個key ,value 的哈希值進行異或後得到Node的哈希值再與原來的1111相與 也就是取Node哈希值的二進制的後四位爲準;tab的容量爲16;存進去一個Node後 size 加1,最多也就是隻能存儲12個值就會發生擴容;這種情況下只要hash值相同就會發生碰撞,然後就會進入else方法,問題產生了,怎麼構造2個一樣的Node哈希值是個問題; 這邊給出了示例如下:

參考鏈接:https://blog.csdn.net/hl_java/article/details/71511815

     public static void main(String[] args) {
         System.out.println(HashMapTest.hash("Aa"));//2112
         System.out.println(HashMapTest.hash("BB"));//2112
     }
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

繼續試驗,發現 key的hash值相同後,會進入else方法,會在原來 AA 這個Node節點屬性next添加一個節點存放Bb;哇塞這不就是實現了一個entry鏈表了麼

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {//evict =true
        Node<K,V>[] tab; Node<K,V> p; int n, i;// 聲明 樹數組, 樹節點,n,i
        if ((tab = table) == null || (n = tab.length) == 0)//table 爲空
            n = (tab = resize()).length; // resize()是 Node<K,V>[] resize() n 就是node的長度 根據初始容量就是 n=16
        if ((p = tab[i = (n - 1) & hash]) == null) // i = (16-1) & hash ---> 15 & 哈希值 
            tab[i] = newNode(hash, key, value, null);// 創建新節點,並插入數據
        else {
            Node<K,V> e; K k; // 聲明 node k
            if (p.hash == hash && // p這個node 存放 Aa-->a ; hash 2112
                ((k = p.key) == key || (key != null && key.equals(k))))//這邊key內容不相同所以沒進入,故不是相同節點
                e = p;
            else if (p instanceof TreeNode)// 這邊 p不是 TreeNode 故沒轉到紅黑樹加Node
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) { // 默認 桶的數量爲0
                    if ((e = p.next) == null) { // 將p這個Node的下一個節點賦值給e爲null;鏈表的開端喲
                        p.next = newNode(hash, key, value, null);// P創建節點存放BB--> b 
                        // 其中 TREEIFY_THRESHOLD = 8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//樹化
                        break;
                    }
                    if (e.hash == hash && //這邊還是判定p和 e 是否是同一個Node
                        ((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;//key相同會新值會取代舊值
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//集合結構修改次數+1
        if (++size > threshold)// size=1 ; threshold=12
            resize();
        afterNodeInsertion(evict);
        return null;
    }

知識追尋者記錄一下,原來key經過hash後如果hash值相同就會在形成一個鏈表,看代碼也知道除了Tab上的第一個Entry,後面每一個Entry的Node都是一個桶;可以看見一個for循環是個無限循環,如果key的hash值相同次數達到8以後就會調用treeifyBin(tab, hash); 知識追尋者又理解了,只要桶的數量達到8後就會將鏈表節點樹化這就是 數組 + 鏈表(每個列表的節點就是一個桶) —> 數組 + 紅黑樹;爲什麼鏈表要轉爲紅黑樹呢,這就考慮到算法的效率問題,鏈表時間複雜度o(n),而紅黑樹的時間複雜度o(logn),樹的查找效率更高;

·

Tip : hashMap 本質是一串哈希表;哈希表首先由數組 + 鏈表組成;當hash碰撞後會形成鏈表,每個鏈表的節點又是一個桶;當桶的數量達到8之後就會進行樹化,將鏈表轉爲紅黑樹,此時HashMap結構就是 數組 + 紅黑樹 ;如果插入相同的key,新的value會取代舊的value;

2.5 擴容源碼分析

測試代碼

   public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>(12);
    }

首先進入HashMap初始化代碼

 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);// 容量12 ,負載因子0.75
    }

進入構造器看看;很簡單 就是HashMap的最大容量值是2的30次方;其次由於輸入的數字是12不是2的次冪會進行擴容,擴容的結果也很簡單就是比輸出的值大的2的次冪;比12大的2 的次冪就是16;

    public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=12,loadFactor=0.75
        if (initialCapacity < 0)//容量小於0,拋出非法參數異常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)// MAXIMUM_CAPACITY = 1<<30 也就是 2 的30次方 ;記hashMap最大
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) // loadFactor=0.75>0 ,否則拋出非法參數異常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;//負載因子賦值
        this.threshold = tableSizeFor(initialCapacity);// 返回一個2次方長度的容量;此時 threshold = 16
    }

點進入看看tableSizeFor(initialCapacity)是什麼;多個無符號右位或操作;其目的就是構造2次冪個1;很簡單的計算,讀者可以自行計算;

    static final int tableSizeFor(int cap) {//cap=12
        int n = cap - 1;// n =11
        n |= n >>> 1;// 11無符號右移 1位的結果與11相或; n=15
        n |= n >>> 2;//n=15
        n |= n >>> 4;//n=15
        n |= n >>> 8;//n=15
        n |= n >>> 16;//n=15
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//15+1=16
    }

Tip : HashMap 的擴容機制就是比輸入容量值大的第一個二次冪

2.6 get源碼分析

測試代碼

   public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>(12);
        map.put("a","b");
        map.get("a");
    }

進入get方法

  public V get(Object key) {
        Node<K,V> e;//聲明Node 節點e
        return (e = getNode(hash(key), key)) == null ? null : e.value;// 根據key的哈希值獲取Node節點返回Value
    }

進入 getNode(hash(key), key)方法,可以看見進行了優先選取tab上的entry節點;如果不是纔會進入鏈表或者紅黑樹進行遍歷比對hash值獲取節點;看過put源碼之後get源碼都沒什麼奇點了;

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 聲明 節點數組tab , 節點first, e ; n ,k
        if ((tab = table) != null && (n = tab.length) > 0 &&// tab.length =16 即容量
            (first = tab[(n - 1) & hash]) != null) { // 與get的計算hash 值一樣; hash值= (容量-1)& hash 得到tab索引;然後獲取索引值賦值給first
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))//直接檢查是否是tab上的enrty,是直接就返回Node
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)//如果是紅黑樹就遍歷獲取節點
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);//否則循環遍歷鏈表獲取節點
            }
        }
        return null;
    }

2.7 hashCode源碼分析

測試代碼

    public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>(12);
        map.put("a","b");
        map.put("b","c");
        int hashCode = map.hashCode();
        System.out.println(hashCode);//4
    }

進入hashCode,就是遍歷每個entrySet的哈希值進行相加作爲HashMap的hash值

   public int hashCode() {
        int h = 0;
        Iterator<Entry<K,V>> i = entrySet().iterator();//迭代器
        while (i.hasNext())
            h += i.next().hashCode();//迭代每個entrySet的哈希值相加
        return h;
    }

做個驗證

    public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>(12);
        map.put("a","b");
        map.put("b","c");
        Set<Map.Entry<String, Object>> entries = map.entrySet();
        entries.stream().forEach(entry->{
            System.out.println(entry.hashCode());
        });
    }

輸出內容是1,3;

Tip : HashMap 的hashCode 就是 entry 的hash值之和,其中Node的哈希值由key和value的哈希值之和;

三總結

  • HashMap允許key和value都爲null;key具有唯一性;
  • HashMap與HashTable不同之處是,HashMap不同步,HashTable不允許key,value爲null;
  • HashMap無法保證Map中內容的順序
  • HashMap默認因子0.75;初始化容量16;初始化容量爲12,插入12個數之後再插入數據會發生擴容;
  • hashMap 本質是一串哈希表;哈希表首先由數組 + 鏈表組成;當hash碰撞後會形成鏈表,每個鏈表的節點又是一個桶;當桶的數量達到8之後就會進行樹化,將鏈表轉爲紅黑樹,此時HashMap結構就是 數組 + 紅黑樹 ;如果插入相同的key,新的value會取代舊的value;
  • HashMap 的擴容機制就是比輸入容量值大的第一個二次冪;比如12,擴容後就是16;17擴容後就是32;
  • HashMap 的hashCode 就是 entry 的hash值之和,其中Node的哈希值由key和value的哈希值之和;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章