HashMap的實現

最近準備開始系統的看一下jdk實現的源碼和了解一些設計模式,先從最經典的容器類入手,看了以後不得不說數學基礎是真的重要,若是再給我一次機會我必定更認真地學數學。本篇文章會分析HashMap。首先,在本篇文章開始以前咱們要知道什麼是哈希表,鏈表,紅黑樹,能夠參考我之前的博文進行了解。java

源碼分析

首先HashMap是經過哈希表實現的,哈希函數爲取餘,哈希衝突解決方法爲鏈地址法,也就是說HashMap的底層是一個數組+鏈表(紅黑樹)的結構(數組中的每一個元素可能造成一個鏈表,鏈表長度大於8時鏈表化爲紅黑樹),如圖就是一個hashmapnode

image

下面來具體看一下hashmap時如何實現的。算法

HashMap的屬性

首先看hashmap的屬性設計模式

//哈希表數組
    transient Node<K,V>[] table;
    //鍵值對的個數
    transient int size;
    //哈希表數組默認初始大小
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
    //哈希表數組的最大元素個數
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默認負載因子,擴容時有用,也能夠本身構造時指定,默認0.75f
    final float loadFactor;
    //當前HashMap所能容納鍵值對數量的最大值,超過這個值,則需擴容,threshold = capacity * loadFactor
    int threshold;
    //遍歷的entrySet
    transient Set<Map.Entry<K,V>> entrySet;
    //用於判斷是否在遍歷的時候進行改變,如果,拋出ConcurrentModificationException
    transient int modCount;
    //鏈表的節點
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }
    //Entry
    final class EntrySet extends AbstractSet<Map.Entry<K,V>>{...}
複製代碼
  • table就是hashMap中的哈希表,當把一個key,value存進hashmap中時,經過hash算法定位到table的下標,將key,value封裝爲Node存進數組中,當出現哈希衝突時,採用鏈的方式進行衝突解決。
  • threshold,當前HashMap所能容納鍵值對數量的最大值,超過這個值,則需擴容,threshold = capacity * loadFactor
  • loadFactor,負載因子,一樣的table,負載因子大時存放的節點更多,查找的效率變慢;負載因子小時存放的節點少,查找更快。因此負載因子用來維持時間和空間的關係。
  • size,鍵值對的個數
  • 這裏並無capacity容量這個成員變量,由於咱們沒有必要去保存它的容量,咱們看完增刪的邏輯就知道爲何沒有容量這個變量

構造函數

hashmap提供了四種構造函數數組

//無參構造函數,全部的屬性都爲默認屬性
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    //傳入初始容量,負載因子爲默認0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //傳入初始容量和負載因子
    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);
    }
    //經過map構造一個新map
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
複製代碼

用戶傳入初始化大小時是如何計算threshold呢?安全

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的冪,如cap爲31,返回32;cap爲7,返回8;cap爲9,返回16bash

查找

查找的邏輯爲先計算到數組的下標,而後對鏈表或紅黑樹進行遍歷查找多線程

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //定位桶的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                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;
    }
複製代碼

這裏是怎麼定位到下標的呢?

first = tab[(n - 1) & hash]
複製代碼

即直接經過(n-1)&hash就找到table中元素的下標,n=table.length,hash爲算出來的key的哈希值。而table的大小始終是2的n次方,(n - 1) & hash 等價於對 length 取餘。能夠嘗試當n=16時,hash=55,這時對hash%n操做獲得的結果爲7,(n-1)&hash結果也是7。而至於爲何不採用取餘操做而使用這種方式,衆所周知位運算的速度比取餘操做速度快。app

這裏順帶有個問題就是爲何hashmap中對key計算哈希值時要從新規定哈希函數呢?

這個問題的意思就是爲何hashmap中要本身實現hash(K key)函數。函數

  • 計算餘數時,若是n比較小,hash只有低4位參與了計算,高位的計算能夠認爲是無效的。這樣致使了計算結果只與低位信息有關,高位數據沒發揮做用。爲了處理這個缺陷,咱們能夠上圖中的hash高4位數據與低4位數據進行異或運算,即 hash ^ (hash >>> 4)。經過這種方式,讓高位數據與低位數據進行異或,以此加大低位信息的隨機性,變相的讓高位數據參與到計算中。
  • 能夠減小哈希衝突

遍歷

遍歷hashmap有大體三種方法

//1.採用foreach迭代entries
    Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    for(Map.Entry<Integer, Integer> entry : map.entrySet()){
    	System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue())
	}
	
	//2.使用foreach迭代keys和values
	Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    //迭代key
    for (Integer key : map.keySet()) {
    	System.out.println("Key = " + key);
    }
    //迭代value
    for (Integer value : map.values()) {
    	System.out.println("Value = " + value);
    }
    
    //3.使用Iterator迭代
    Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
    while (entries.hasNext()) {
    	Map.Entry<Integer, Integer> entry = entries.next();
    	System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
    }
複製代碼

而foreach咱們知道它只是一種語法糖,事實上在進行反編譯後發現foreach實際上就是等價於使用iterator進行迭代的。因此下面直接看迭代器時如何迭代的。

下面來看一下有關迭代器遍歷的代碼:

public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
    }
    
    /**
     * 迭代器
     */
    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
    
    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
    
        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry 
                // 尋找第一個包含鏈表節點引用的桶
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }
    
        public final boolean hasNext() {
            return next != null;
        }
    
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                // 尋找下一個包含鏈表節點引用的桶
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
        //省略部分代碼
    }
複製代碼

遍歷全部的鍵值對時,首先要獲取鍵集合EntrySet對象(對key和value進行遍歷類似),而後再經過 EntrySet 的迭代器EntryIterator進行遍歷。EntryIterator 類繼承自HashIterator類,核心邏輯也封裝在 HashIterator 類中。HashIterator 的邏輯並不複雜,在初始化時,HashIterator 先從桶數組中找到包含鏈表節點引用的桶。而後對這個桶指向的鏈表進行遍歷。遍歷完成後,再繼續尋找下一個包含鏈表節點引用的桶,找到繼續遍歷。找不到,則結束遍歷。如圖:

image

打印順序爲:54,29,16,43,31,46,60,74,88,77,90

若是鏈表轉爲紅黑樹的話採用的是哪一種遍歷方式?

能夠看到,在遍歷的代碼中並無對紅黑樹單獨進行遍歷,這時由於這裏咱們的紅黑樹中有一個成員變量指向原鏈表中的下一個節點,因此,轉爲紅黑樹的遍歷順序就是next的順序。

增長

//新增方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    //hash函數
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    /**
     * 增長操做的具體實現
     *
     * @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) { Node<K,V>[] tab; Node<K,V> p; int n, i; //若是是第一次添加會初始化table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //該下標沒有元素,直接插入(沒有產生哈希衝突) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 若是鍵的值以及節點hash等於鏈表中的第一個鍵值對節點時,則將e指向該鍵值對 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //p是樹節點類型,則用紅黑樹的方式插入 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); //若是鏈表長度大於或等於樹化閾值,則進行樹化操做 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //加入的節點和原來的節點是同一個節點(這裏哈希值和equals方法都相同才斷定爲同一個節點) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //判斷要插入的鍵值對是否存在 HashMap 中 if (e != null) { // existing mapping for key V oldValue = e.value; //onlyIfAbsent 表示是否僅在 oldValue 爲 null 的狀況下更新鍵值對的值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //當鍵值對數量超過擴容閾值時擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 複製代碼

總的來講,大概流程以下:

  • 是否須要對table初始化
  • 查看這個節點的key是否已經存在,若存在而且或原來value爲null,替換
  • 這個節點不存在就插入,插入的三種狀況
    • 沒衝突,直接插入
    • 衝突,鏈表插入,考慮樹化
    • 衝突,紅黑樹節點插入
  • 判斷鍵值對數量是否大於閾值,大於的話則進行擴容操做

擴容

再增長的時候會進行擴容,擴容的條件爲++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;
        //table已經完成初始化
        if (oldCap > 0) {
            //達到最大,不能繼續擴容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //變爲原來容量的兩倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //table未完成初始化但閾值已經設置完成,即便用HashMap(int)或HashMap(int,float)構造時會發生這種狀況
        else if (oldThr > 0) // initial capacity was placed in threshold
            //設置閾值爲新容量
            newCap = oldThr;
        //table未初始化,閾值未初始化,即便用HashMap()構造時發生這種狀況
        else {
            //設置初始值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //計算出新閾值
        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) {
            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;
                    //若是這個節點是紅黑樹須要對紅黑樹進行拆分
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //若是這個節點是鏈表的形態
                    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;
                            //(e.hash & oldCap) == 0爲true判斷的是擴容後在原下標,false爲要放進新的下標。
                            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);
                        //將鏈表放進新table中
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
複製代碼

好的,擴容的邏輯就是上文這樣,總結一下,擴容主要作了三件事

  • 計算新哈希表table的容量newCap和新閾值newThr
  • 根據新newCap獲得新的哈希表table
  • 將鍵值對節點從新映射到新的哈希表裏。若是節點是TreeNode類型,則須要拆分成黑樹。若是是普通節點,則節點按原順序進行分組。

鏈表和紅黑樹的轉化

前文說到當鏈表長度大於8時要將鏈表轉爲紅黑樹,紅黑樹長度小於6時要將紅黑樹轉爲鏈表。具體的就是轉化分爲三個操做:

  • 鏈表的樹化
  • 樹的鏈表化
  • 拆分
鏈表樹化
static final int TREEIFY_THRESHOLD = 8;
    
    /**
     * 當table數組容量小於該值時,優先進行擴容,而不是樹化
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
    }
    
    /**
     * 將普通節點鏈表轉換成樹形節點鏈表
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //table數組容量小於 MIN_TREEIFY_CAPACITY,優先進行擴容而不是樹化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            //hd爲頭節點,tl爲尾節點
            TreeNode<K,V> hd = null, tl = null;
            do {
                //將普通節點替換成樹形節點
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);  //將普通鏈表轉成由樹形節點鏈表
            if ((tab[index] = hd) != null)
                //將樹形鏈表轉換成紅黑樹
                hd.treeify(tab);
        }
    }
    
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }
複製代碼

看了鏈表變爲紅黑樹的源碼,咱們能夠得出如下結論:

鏈表變爲紅黑樹的條件:

  • 鏈表長度大於8:官方文檔說是由於按照泊松分佈的計算公式計算出了鏈表中元素個數爲8時的機率已經很是小,根據機率統計而選擇的8爲分界點
  • 哈希表table的容量大於64:當table容量較小時應該優先考慮擴容,由於若是容量小數據多會產生大量哈希碰撞,性能降低

鏈表如何變爲紅黑樹:

由於一開始hashmap設計時並無考慮到對節點進行大小比較,而紅黑樹是一種有序樹,因此要求每一個節點要可比較(實現comparable接口或提供比較器)。因此在樹化的過程會經歷如下過程:

  • 首先比較鍵與鍵之間hash的大小,若是hash相同,繼續往下比較
  • 檢測鍵類是否實現了Comparable接口,若是實現調用compareTo方法進行比較
  • 若是仍未比較出大小,經過tieBreakOrder方法比較

經過以上方法比較完以後就能進行排序變爲紅黑樹了

拆分

拆分就是擴容的時候,紅黑樹節點要映射到新的哈希表中的對應位置,這個過程叫樹的拆分。固然,若是將所有紅黑樹中的節點從新計算下標插入到新哈希表,由鏈->樹這樣也能夠,但這樣效率很低,因此hashmap的設計多了一個拆分。

// 紅黑樹轉鏈表閾值
    static final int UNTREEIFY_THRESHOLD = 6;
    
    final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
        TreeNode<K,V> b = this;
        // Relink into lo and hi lists, preserving order
        TreeNode<K,V> loHead = null, loTail = null;
        TreeNode<K,V> hiHead = null, hiTail = null;
        int lc = 0, hc = 0;
        /* 
         * 紅黑樹節點仍然保留了next引用,故仍能夠按鏈表方式遍歷紅黑樹。
         * 下面的循環是對紅黑樹節點進行分組,與上面相似
         */
        for (TreeNode<K,V> e = b, next; e != null; e = next) {
            next = (TreeNode<K,V>)e.next;
            e.next = null;
            if ((e.hash & bit) == 0) {
                if ((e.prev = loTail) == null)
                    loHead = e;
                else
                    loTail.next = e;
                loTail = e;
                ++lc;
            }
            else {
                if ((e.prev = hiTail) == null)
                    hiHead = e;
                else
                    hiTail.next = e;
                hiTail = e;
                ++hc;
            }
        }
    
        if (loHead != null) {
            //若是loHead不爲空,且鏈表長度小於等於6,則將紅黑樹轉成鏈表
            if (lc <= UNTREEIFY_THRESHOLD)
                tab[index] = loHead.untreeify(map);
            else {
                tab[index] = loHead;
                /* 
                 * hiHead == null 時,代表擴容後,
                 * 全部節點仍在原位置,樹結構不變,無需從新樹化
                 */
                if (hiHead != null) 
                    loHead.treeify(tab);
            }
        }
        //與上面相似
        if (hiHead != null) {
            if (hc <= UNTREEIFY_THRESHOLD)
                tab[index + bit] = hiHead.untreeify(map);
            else {
                tab[index + bit] = hiHead;
                if (loHead != null)
                    hiHead.treeify(tab);
            }
        }
    }
複製代碼

紅黑樹的拆分和擴容時對節點的重映射類似

紅黑樹的鏈表化

當刪除節點時可能形成紅黑樹的個數小於6個,這時要將紅黑樹再轉化爲鏈表。

final Node<K,V> untreeify(HashMap<K,V> map) {
        Node<K,V> hd = null, tl = null;
        //遍歷TreeNode鏈表,並用Node替換
        for (Node<K,V> q = this; q != null; q = q.next) {
            //替換節點類型
            Node<K,V> p = map.replacementNode(q, null);
            if (tl == null)
                hd = p;
            else
                tl.next = p;
            tl = p;
        }
        return hd;
    }
    
    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        return new Node<>(p.hash, p.key, p.value, next);
    }
複製代碼

由於紅黑樹中的節點有prev節點,這個節點指向的原鏈表形態時當前節點的下一個節點,因此在鏈化的時候就很方便了。

爲何鏈化爲6,樹化爲8?

如下爲我我的觀點

  • 首先,紅黑樹的查詢O(logn)比鏈表O(n)快;但紅黑樹添加刪除節點時會維持紅黑樹的特性而進行相應旋轉,變色操做,鏈表在添加刪除時只須要改變當前節點的next指針便可。
  • 當鏈表和紅黑樹的節點個數小於等於6時,查詢速度的差別小於刪除添加的差別,因此選擇添加刪除更方便的鏈表
  • 當鏈表和紅黑樹的節點個數大於8時,刪除添加的速度差別小於查詢速度的差別,因此選擇查詢更快的紅黑樹

刪除

刪除大概分爲三部

  • 肯定哈希表的下標
  • 遍歷下標的鏈表到相應的節點
  • 刪除

固然,和前面同樣,刪除鏈表節點可能會變爲刪除紅黑樹節點,有可能刪除紅黑樹節點後會進行紅黑樹的鏈表化。下面來看一下實現代碼:

//暴露給使用者的刪除方法
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
    /**
     * 刪除的具體實現
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    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) {
            Node<K,V> node = null, e; K k; V v;
            //若是鍵的值與鏈表第一個節點相等,則將node指向該節點
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {  
                //若是是紅黑樹類型,調用紅黑樹的查找邏輯定位待刪除節點
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //遍歷鏈表,找到待刪除節點
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            
            //以上步驟都是找到待刪除節點node,接下來刪除node,並修復鏈表或紅黑樹
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
複製代碼

其中,關於紅黑樹的維護這裏就不具體講解了,由於我實在以爲紅黑樹的維護思想不難,可是很麻煩,解讀的話也差很少和日常紅黑樹維護的思路相同,就是左旋轉,右旋轉,變色。紅黑樹鏈表化前文也分析過了。

其餘關於hashMap

modCount屬性

咱們在使用迭代器進行迭代時,當咱們改變hashmap的狀態時,會拋出ConcurrentModificationException異常。

拋出這個異常的緣由

拋出這個異常的緣由就是在遍歷hashmap的時候,hashmap的數據改變了。咱們在分析迭代器遍歷的時候有這麼一段代碼:

abstract class HashIterator {
        //其餘代碼
        final Node<K,V> nextNode() {
            //其餘代碼
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //其餘代碼
        }
    }
複製代碼

能夠看到,這個異常拋出的條件是modeCount!=expectedModCount的時候,而modCount以前說過期hashmap的一個屬性,每當進行put,remove操做的時候都會對modCount進行++操做,而exceptedModeCount是一開始遍歷時候的modCount。因此說,這個異常拋出的時候表明遍歷的時候表發生了改變。這也就是java中的fail-fast機制。這個機制主要是針對多線程環境下的線程安全問題,但事實上hashmap仍是線程不安全的。

如何解決這個問題?

咱們想作的無非就是在遍歷的時候刪除元素

  • 可使用iterator提供的remove方法進行刪除
  • 直接改用ConcurrentHashMap

序列化

咱們看到哈希表table使用了transient修飾,這個關鍵詞表明這個屬性不會被序列化。

爲何table不能被序列化?

咱們知道肯定hashmap使用的哈希函數會用到key的hashCode,而key的哈希code方法可能沒有被重寫,這樣這個方法就會調用Object的hashCode方法,而Object的hashCode方法是本地native方法,那麼會形成的問題就是同一個hashmap可能在不一樣虛擬機下反序列話時形成每一個鍵的hash值不同,就會形成鍵值對對應的位置出錯。

如何解決hashmap序列化問題?

可能我須要將hashmap傳送到其餘計算機上,那麼hashmap本身實現了writeObject(java.io.ObjectOutputStream s)和readObject(java.io.ObjectInputStream s)方法。他們做用是該類在序列化時會自動調用這兩個方法,沒有這兩個方法就調用defaultWriteObject()和defaultReadObject()。而hashmap實現了,因此看一下他們是如何進行序列化的,下面只分析寫,讀就對是相應數據的讀就好了。

private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
        int buckets = capacity();
        // Write out the threshold, loadfactor, and any hidden stuff
        s.defaultWriteObject();
        s.writeInt(buckets);
        s.writeInt(size);
        internalWriteEntries(s);
    }
    
    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        Node<K,V>[] tab;
        if (size > 0 && (tab = table) != null) {
            for (Node<K, V> e : tab) {
                for (; e != null; e = e.next) {
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                }
            }
        }
    }
複製代碼

能夠看到,hashmap實現序列化的方式就是將hashmap拆解爲屬性和節點,直接把屬性和節點序列化便可

總結

那麼hashmap的分析就到此結束,原本是想把有關Map的經常使用實現類都寫在一篇的,但真正查看hashmap源碼的時候才發現一些實現真的要考慮不少問題和細節,寫在一篇裏太多太雜。經過此次完全的源碼分析我也是感覺到了代碼的設計,編寫藝術,在一開始查看源碼的時候我都以爲這些編寫人員是否是有一點設計過分,但後來又對每一個細節進行思考,包括查了不少篇網上的博客,我才真的以爲設計人員的厲害之處,雖然他們的代碼量不少很長,註釋也很是多,但每一行代碼都有本身的很關鍵的做用,每一行代碼都必不可少,考慮的也很是全面,包括對每個細節的考慮。總的來講,此次源碼的分析不只讓我知道了hashmap的實現細節;還教會了我怎樣去設計一個完美,通用的代碼;還有就是數學的重要性。

下一篇會經過分析ConccurrentHashMap,而後再簡單的看其餘容器時如何實現的,最後祝我本身期末不掛科。

相關文章
相關標籤/搜索