国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

HashMap的工作原理 - JDK1.8

2019-11-08 03:20:47
字體:
來源:轉載
供稿:網友
文章中我會盡量用通俗易懂的文字來描述。我們在java編程中經常會用到HashMap,因為它查找方便、可以插入null的鍵和值、效率高等特性,那么,HashMap是的工作原理是怎么樣的呢?一、HashMap概述HashMap是由數組 + 鏈表 的數據結構,它是數組和鏈表的結合體,請看下圖。

二、HashMap數據結構打開HashMap.java文件,找到以下代碼塊,這個就是HashMap存放數據的對象,可以看出它就是一個“鏈表”的數組

/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some Operations to allow * bootstrapping mechanics that are currently not needed.) */transient Node<K,V>[] table;再來看看Node<K,V>(鏈表)的定義,我將注釋直接寫在代碼上。
/** * Basic hash bin node, used for most entries.  (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */static class Node<K,V> implements Map.Entry<K,V> {        final int hash;//hash(key)返回的值        final K key;//鍵        V value;//值        Node<K,V> next;//鏈表的關鍵,naxt中裝載著下一個鏈表        //類構造方法,初始化操作        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; }        //返回hashCode,使用異或算法        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;        }}作者:韋慶明鏈接:https://zhuanlan.zhihu.com/p/25266385來源:知乎著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。很多人可能不是很理解“鏈表”這個概念,我們可以想象一下一根鏈子的特性:

1、一個環套著另外1個圈,串成一條鏈。

2、一個環只能關聯到左右2個環,頭、尾部只能關聯1個。

而上面的代碼中,Node<K,V> next 字段中裝載著下一個鏈表。next 中的 next 又可以裝載著下下個鏈表,一環套一環,這就是鏈表的實現原理。

三、HashMap的存儲和讀取

1) 數組元素存儲規則HashMap是按什么規則存放到數組中的呢?

在JDK1.8之前貌似是按照 hash(key)%len(除余) 來獲取數組下標,并存放到此位置。JDK1.8之后,按照 (len-1) & hash(“與”運算) 來獲取數組下標,并存放到此位置,HashMap作者這樣做的思路沒有表述出來,是因為這個算法的能減少沖突?當然,不管是使用 hash(key)%len還是(len-1) & hash 來計算元素存放位置下標,都會存在一個問題,就是如果數組中這個位置存在數據了,怎么辦?這就是HashMap選擇使用數組+鏈表來實現的好處,如數組下標3的位置存在了數據,那么新插入的數據會存放在這個數組位置的鏈表的末端。注解:len=數組長度圖解,數組下標計算公式使用 hash(key)%len(除余) (講解時候用,比較好找規律)

到此我們大致明白HashMap的數據結構與存放規則了。接下來我們從代碼上一步步解讀它的具體實現。2) put 代碼解讀put() 方法代碼,注釋寫在代碼中。
/** * Associates the specified value with the specified key in this map. * If the map PReviously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or *         <tt>null</tt> if there was no mapping for <tt>key</tt>. *         (A <tt>null</tt> return can also indicate that the map *         previously associated <tt>null</tt> with <tt>key</tt>.) */public V put(K key, V value) {    //執行putVal方法,并返回相應的結果    //執行方法前,先執行hash()方法獲取hash    return putVal(hash(key), key, value, false, true);}hash() 方法代碼,注釋寫在代碼中。
/** * Computes key.hashCode() and spreads (XORs) higher bits of hash * to lower.  Because the table uses power-of-two masking, sets of * hashes that vary only in bits above the current mask will * always collide. (Among known examples are sets of Float keys * holding consecutive whole numbers in small tables.)  So we * apply a transform that spreads the impact of higher bits * downward. There is a tradeoff between speed, utility, and * quality of bit-spreading. Because many common sets of hashes * are already reasonably distributed (so don't benefit from * spreading), and because we use trees to handle large sets of * collisions in bins, we just XOR some shifted bits in the * cheapest possible way to reduce systematic lossage, as well as * to incorporate impact of the highest bits that would otherwise * never be used in index calculations because of table bounds. */static final int hash(Object key) {    int h;    //使用三目運算,判斷key=null則等于0,這就表示key=null值的都存放[0]位置。    //否則等于key.hashCode() ^(異或) (key.hashCode() >>>(右移) 16)后的值。    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}putVal() 方法代碼,put的邏輯都寫在此方法中,注釋寫在代碼中。吐槽一下JDK1.8 HashMap的作者,把參數賦值和 if 判斷放到一起,看著真累。
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {    //參數字段注釋:    //tab = 主數據集數組對象,p = 主數據集數組中某元素    //n = 主數據集數組長度,i = 新數據插入位置    Node<K,V>[] tab; Node<K,V> p; int n, i;    //判斷table等于null 或 table數組長度等于 0     if ((tab = table) == null || (n = tab.length) == 0)        //執行resize()方法,初始化table,給tab參數賦值,并返回長度給 n 參數        n = (tab = resize()).length;    //給 i 賦值為 n-1 &(與運算) hash 后的值,并將 i 位置的元素賦值給 p 參數    //最后判斷 p 是否等于null    if ((p = tab[i = (n - 1) & hash]) == null)                // p = null 表示數組中這個位置還沒有數據,那么直接插入到這個位置        tab[i] = newNode(hash, key, value, null);    // p != null,那么就說明數組的 i 位置已經有數據了,執行以下操作    else {        //參數字段注釋:e = 主數據集數組中某元素,k = 某元素中的key        Node<K,V> e; K k;        //條件標識名稱:JudgeExistence(后面可以引用)        //判斷規則:必須條件1:p.hash 等于 hash(傳入添加)         //條件2:p.key 等于 key(傳入添加)        //條件3:key(傳入添加) != null 與 key(傳入添加)值 等于 p.key值        //只要符合1+2 或 1+3條件,則判定這個key的數據已經存在        //判斷期間 k 賦值為 p.key        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            //將 e 賦值為 p            e = p;        //判斷 p 是否為 TreeNode 類型        else if (p instanceof TreeNode)            //調用putTreeVal()方法,將返回值賦值 e             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);        else {            //執行無條件的 for 循環方法            for (int binCount = 0; ; ++binCount) {                //判斷 p.next == null,表示可以在鏈表中這個位置插入新數據                //并將 e 賦值 為 p.next                if ((e = p.next) == null) {                    //將新數據插入到鏈表的末端                    p.next = newNode(hash, key, value, null);                    //判斷循環次數 >= TREEIFY_THRESHOLD(8)-1                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                        //執行 treeifyBin() 方法                        treeifyBin(tab, hash);                    //將新數據插入到鏈表的末端后,停止循環                    break;                }                //判斷條件直接引用條件標識名稱:JudgeExistence                //判斷期間 k 賦值為 e.key                if (e.hash == hash &&                    ((k = e.key) == key || (key != null && key.equals(k))))                    //找到相同key的數據,停止循環                    break;                //將 p 賦值為 e ,用于下一次循環判斷                p = e;            }        }        //判斷e 是否成功獲取到數據        if (e != null) { // existing mapping for key            V oldValue = e.value;            //判斷可修改value 標識參數 onlyIfAbsent 是否等于 false            //或 e.value 是否等于 null,只要符合一項條件就可以修改            if (!onlyIfAbsent || oldValue == null)                //覆蓋原value                e.value = value;            afterNodeaccess(e);            return oldValue;        }    }    //HashMap結構被修改的次數,此次數不包括相同key被覆蓋修改的次數    ++modCount;    //判斷 鍵值映射的數目 > (容量*負載因子)上次設置的最大容量    if (++size > threshold)        //重新設置容量(在原有基礎上*2)        //resize()有兩個主要功能,一是初始化table,二是將table容量*2        resize();    afterNodeInsertion(evict);    return null;}

總結 put 的工作流程:

檢查兩個key是否一致的條件:

A = (已經插入的Entity),B = (即將插入的Entity)必須條件1:A.hash 等于 B.hash條件2:A.key 等于 B.key條件3:B.key 不等于 null 與 B.key值.equals(A.key值)只要符合1+2 或 1+3條件,則判定這個key的數據已經存在

1、table數組等于 null 或 table數組長度等于 0 時,會為其執行初始化操作。

2、使用 (len-1) & hash(“與”運算)計算出數組下標后,檢查數組此下標位置是否存在數據,不存在則直接插入數據到該位置。

3、如果第 2 步操作檢查到數據已經存在,檢查即將插入的 key 在 HashMap 中已經存在后,會覆蓋此位置原數據的 value,key不會被覆蓋 。

4、如果第 3 步判斷不成立,那么判斷 此下標位置 是否為 TreeNode 類型,是則獲取putVal版本樹中的數據,并覆蓋此位置原數據的 value,key不會被覆蓋 。

5、以上判斷都不成立,那么循環數組中此下標位置的鏈表。

5-1、循環鏈表,檢查到 next 值等于 null ,則將數據插入到此位置。最先加入的數據在鏈頭,新加入的數據在鏈尾。

5-2、循環鏈表,檢查到即將插入的 key 在鏈表中已經存在后,會覆蓋此位置原數據的 value,key不會被覆蓋 。

6、判斷 鍵值映射的數目 > (容量*負載因子)上次設置的最大容量值的時候,重新設置容量(在原有基礎上*2)。(注:這一步只在數據插入操作時候執行,覆蓋操作時候不執行

3) get 代碼解讀get() 方法代碼,注釋寫在代碼中。
/** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * * <p>More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}.  (There can be at most one such mapping.) * * <p>A return value of {@code null} does not <i>necessarily</i> * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */public V get(Object key) {    Node<K,V> e;    //e = getNode(hash(key), key)    return (e = getNode(hash(key), key)) == null ? null : e.value;}getNode() 方法代碼,get邏輯代碼寫在此方法內,注釋寫在代碼中。
/** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */final Node<K,V> getNode(int hash, Object key) {    //參數字段注釋:    //tab = 主數據集數組對象,first,e = 主數據集數組中某元素    //n = 主數據集數組長度,k = tab[x].key    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;    //判斷table不等于null 與 tab.length大于 0 與     //tab[(len-1) & hash] 此下標位置的數據不等于null    //判斷期間,tab賦值為table,n 賦值為tab.length,first 賦值為tab[x]    if ((tab = table) != null && (n = tab.length) > 0 &&        (first = tab[(n - 1) & hash]) != null) {        //條件標識名稱:JudgeExistence(后面可以引用)        //判斷必要條件1:first.hash(上一步獲取到的) 等于 hash(傳入的)        //條件2:first.key 等于 key(傳入的)        //條件3:key(傳入的) 不等于 null 與 key值(傳入的)等于 first.key值        //只要符合1+2 或 1+3條件,則判定這個key的數據存在        //判斷期間 k 賦值為 first.key        if (first.hash == hash && // always check first node            ((k = first.key) == key || (key != null && key.equals(k))))            //返回Node<K,V>數據            return first;        //上個判斷不成立,則從鏈表中找這個數據        //判斷下一個鏈表是否存在        //判斷期間 e 賦值為 first.next        if ((e = first.next) != null) {            //判斷first是否等于TreeNode類型            if (first instanceof TreeNode)                //是則從根節點種獲取到這條數據,并返回                return ((TreeNode<K,V>)first).getTreeNode(hash, key);            //使用do... while循環查找鏈表,直到找到數據或循環到鏈表末端            do {                //判斷條件直接引用條件標識名稱:JudgeExistence                //判斷期間 k 賦值為 e.key                if (e.hash == hash &&                    ((k = e.key) == key || (key != null && key.equals(k))))                    //返回Node<K,V>數據                    return e;            } while ((e = e.next) != null);        }    }    return null;}

總結 get() 的工作流程:

相對于put ,get就要簡單很多。

檢查兩個key是否一致的條件:

A = (已經插入的Entity),B = (即將插入的Entity)必須條件1:A.hash 等于 B.hash條件2:A.key 等于 B.key條件3:B.key 不等于 null 與 B.key值.equals(A.key值)只要符合1+2 或 1+3條件,則判定這個key的數據已經存在

它首先使用 (len-1) & hash 算法獲取數組的下標,并使用 key 對比第一個鏈表的數據,如果匹配,返回該鏈表的數據。否則循環該數組下標內的所有鏈表,直至找到匹配的數據。

如果找不到匹配的數據,返回null 。

HashMap工作原理總結:

1、HashMap是鏈表+數組的結合體,它的核心對象就是一個鏈表的數組。

2、HashMap可以存儲 null 的鍵值,鍵=null的時候會默認存放在 數組[0] 位置。

3、HashMap在JDK1.8后使用的數組下標算法為: (len-1) & hash(與運算)。

4、HashMap判斷是否存在重復 key 的核心條件代碼是:

if (e.hash == hash &&      ((k = e.key) == key || (key != null && key.equals(k))))

5、HashMap在出現重復 hash 值但 key 不同的時候,會將新數據插入到該數組下標中鏈表的末端。最先加入的數據在鏈頭,新加入的數據在鏈尾。

6、HashMap在插入數據的時候,如果新數據的 key 已經存在,那么新數據將會覆蓋原數據的 value 值,key不會被覆蓋。

HashMap就講到這里了,有時間再研究一下 resize() 方法吧。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 营口市| 阳春市| 凤冈县| 金华市| 毕节市| 石嘴山市| 宿州市| 道孚县| 汤阴县| 渭源县| 松潘县| 昭觉县| 无棣县| 海宁市| 合水县| 渑池县| 南充市| 商水县| 石门县| 潜江市| 华容县| 沁阳市| 临漳县| 武义县| 宁武县| 伽师县| 安达市| 南通市| 渭南市| 盖州市| 宁城县| 洛隆县| 山阴县| 商洛市| 怀远县| 大兴区| 洛宁县| 云浮市| 宝清县| 乐清市| 锡林浩特市|