學習HashMap的筆記

對於HashMap只是學習了下put,remove方法,hashMap是數組+鏈表+紅黑樹組成

因此下面貼出我本身給代碼的註釋,看不懂的見諒哈,畢竟我也是剛瞭解,若是有錯誤的地方請指出,很是感謝

  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;
    if ((tab = table) == null || (n = tab.length) == 0)//判斷table是否爲null或者table的長度是否爲0
        n = (tab = resize()).length;//調整table的長度
    if ((p = tab[i = (n - 1) & hash]) == null)//根據hash去獲取table中的位置,若是爲null直接插入
        tab[i] = newNode(hash, key, value, null);
    else {//若是當前數組位置已經存在,表示衝突了
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))//若是原數組中的元素和當前元素(須要新增的)hash同樣而且{key相等或者(key不等於null而且key的equals相等)}
            e = p;//把當前數組中的元素賦值給e
        else if (p instanceof TreeNode)//若是當前數組中衝突的節點爲紅黑樹
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//插入紅黑樹
        else {//這裏表示在衝突的hash桶中去查找爲null的位置而後插入(有可能會碰見到達鏈表定義長度,這時須要轉換成紅黑樹了)
            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))))//這裏的條件和上面的判斷同樣,結果是爲了若是找到hash想的而且key相等的表示存在
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)//若是onlyIfAbsent爲true表示不覆蓋原有的值(默認爲false)
                e.value = value;//覆蓋值
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)//若是當前map的大小大於閾值了,進行擴容
        resize();
    afterNodeInsertion(evict);
    return null;
}
View Code
  remove方法:

  

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {//若是table不等於null而且table的長度大於0 而且p(根據須要刪除的元素的hash值去查詢)是否在table中
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))//若是當前刪除元素的hash值和查找到的元素hash值同樣而且key相等或者(key不等於null而且key的equals相等)表示這個p元素就是須要刪除的元素
            node = p;//把p賦值給node
        else if ((e = p.next) != null) {//不然的話表示都在一個hash桶中,在桶(鏈表)中查找
            if (p instanceof TreeNode)//若是是紅黑樹
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//根據key和hash獲取須要刪除的元素
            else {//若是不是紅黑樹,則表示是鏈表--進行遍歷
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;//若是找到則賦值給node
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {//若是node不等於null而且(若是matchValue爲false,則不用比較值是否相等)--這裏表示找到了須要刪除的元素
            if (node instanceof TreeNode)//若是找到的刪除元素是紅黑樹
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);//紅黑樹刪除
            else if (node == p)//若是刪除的元素和p等價
                tab[index] = node.next;//把table下標的元素值設置成node(須要刪除的元素下一個節點)
            else
                p.next = node.next;//把node(須要刪除的節點)下一個節點給node的父節點的下一個節點
            ++modCount;//HashMap結構被修改的次數++
            --size;//容量--
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
View Code

get方法相對來講比較簡單,請讀者本身看吧~嘿嘿

關於Hash方法原理(轉載:https://www.zhihu.com/question/20733617)我也是不太明白~:

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

Java 7中是這樣的node

複製代碼
static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
複製代碼

上面這段代碼其實叫作"擾動函數"數組

下面摘自https://www.zhihu.com/question/20733617app

你們都知道上面代碼裏的key.hashCode()函數調用的是key鍵值類型自帶的哈希函數,返回int型散列值。

理論上散列值是一個int型,若是直接拿散列值做爲下標訪問HashMap主數組的話,考慮到2進制32位帶符號的int表值範圍從-21474836482147483648。先後加起來大概40億的映射空間。只要哈希函數映射得比較均勻鬆散,通常應用是很難出現碰撞的。

但問題是一個40億長度的數組,內存是放不下的。你想,HashMap擴容以前的數組初始大小才16。因此這個散列值是不能直接拿來用的。用以前還要先作對數組的長度取模運算,獲得的餘數才能用來訪問數組下標。源碼中模運算是在這個indexFor( )函數裏完成的。ide

bucketIndex = indexFor(hash, table.length);

indexFor的代碼也很簡單,就是把散列值和數組長度作一個"與"操做,函數

static int indexFor(int h, int length) {
        return h & (length-1);
}

順便說一下,這也正好解釋了爲何HashMap的數組長度要取2的整次冪。由於這樣(數組長度-1)正好至關於一個「低位掩碼」。「與」操做的結果就是散列值的高位所有歸零,只保留低位值,用來作數組下標訪問。以初始長度16爲例,16-1=15。2進製表示是00000000 00000000 00001111。和某散列值作「與」操做以下,結果就是截取了最低的四位值。

學習

10100101 11000100 00100101
&	00000000 00000000 00001111
----------------------------------
	00000000 00000000 00000101    //高位所有歸零,只保留末四位


但這時候問題就來了,這樣就算個人散列值分佈再鬆散,要是隻取最後幾位的話,碰撞也會很嚴重。更要命的是若是散列自己作得很差,分佈上成等差數列的漏洞,剛好使最後幾個低位呈現規律性重複,就無比蛋疼。

這時候「擾動函數」的價值就體現出來了,說到這裏你們應該猜出來了。看下面這個圖,
this

右位移16位,正好是32bit的一半,本身的高半區和低半區作異或,就是爲了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。並且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相保留下來。

最後咱們來看一下Peter Lawley的一篇專欄文章《An introduction to optimising a hashing strategy》裏的的一個實驗:他隨機選取了352個字符串,在他們散列值徹底沒有衝突的前提下,對它們作低位掩碼,取數組下標。

結果顯示,當HashMap數組長度爲512的時候,也就是用掩碼取低9位的時候,在沒有擾動函數的狀況下,發生了103次碰撞,接近30%。而在使用了擾動函數以後只有92次碰撞。碰撞減小了將近10%。看來擾動函數確實仍是有功效的。

但明顯Java 8以爲擾動作一次就夠了,作4次的話,多了可能邊際效用也不大,所謂爲了效率考慮就改爲一次了。spa

感受大概就是爲了進行稀釋碰撞(衝突)的次數--純屬我的理解code

相關文章
相關標籤/搜索