天天看點

java 集合之HashMap 源碼閱讀記錄

源碼版本:jdk1.8

接口繼承

java 集合之HashMap 源碼閱讀記錄

源碼注釋概要:

  1. 允許key和value的值為null
  2. 與Hashtable不同的地方,HashMap是unsynchronized,線程不安全,Hashtable不允許key和value為null,但是線程安全
  3. 疊代整個HashMap的時候,時間複雜度是和HashMap執行個體的容量呈正相關的,如果業務需要經常疊代整個HashMap,初始容量不要設定得過高(負載因子也不要設定得太低)
  4. 容量定義:HashMap的bucket數目
  5. 負載因子定義:在容量自動擴容之前,裝物品的一個路徑成本,比如現在有10個bucket,現在負載因子定義為0.6,那麼裝滿了6個bucket之後就不能再裝了,就需要擴容之後才能繼續裝。
  6. 重建規則:鍵值映射數量> 負載因子*容量,HashMap的結構将被重建,容量變為2倍,結構被重建,意味着不同時間疊代同一個HashMap順序可能會不同
  7. 負載因子預設值為0.75,平衡考慮了時間複雜度和空間複雜度。負載因子高,空間使用率高,但是會增加查詢消耗,負載因子低則會導緻空間浪費。
  8. Map m = Collections.synchronizedMap(new HashMap(…)) 這樣使用,可以保證HashMap的原子性

    10.疊代器的方法都采用了fail-fast(快速失敗機制),隻要在疊代的時候哈希表被修改,就會及時抛出ConcurrentModificationException異常,而避免以後出現不定期的,難以定位的錯誤。

底層使用數組、連結清單、紅黑樹實作。

HashMap采用鍊位址法來實作,将所有關鍵字為同義詞的結點連結在同一個單連結清單中,也就是同一個bucket中。

  1. 可以結合三者的優點,數組便于随機存儲,是以計算hash碼所在的bucket最為合适
  2. 連結清單正好維護處于一個bucket的所有Node節點。連結清單便于修改,删除,時間複雜度為O(1),但是查詢隻能按鍊查找,時間複雜度為O(N)。
  3. 但是連結清單查找的時候時間複雜度為O(N),如果連結清單過長,時間消耗太多,是以,對應bucket的Node節點如果過多,就轉為紅黑樹維護,這樣查詢,修改,删除都是O(logN)的時間複雜度

哈希存儲結構

定義了一個Node<K,V> 的内部類,實作了Map.Entry<K,V>的接口,維護<K,V>鍵值對,普通連結清單節點使用Node維護,紅黑樹節點使用TreeNode

java 集合之HashMap 源碼閱讀記錄

TreeNode 繼承了LinkedHashMap.Entry<K,V>

java 集合之HashMap 源碼閱讀記錄

get()

get(Object key)方法根據指定的key值傳回對應的value,該方法調用了getNode(hash(key), key)得到相應的Node,然後傳回對應Node.value。

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }      

算法思想是首先通過hash()函數得到對應bucket的下标,然後查找對應bucket的連結清單或者紅黑樹,通過key.equals(k)方法來判斷是否是要找的那個鍵值對Node。

hash()

hashCode 為native方法,這裡主要好奇的是為啥要這麼異或。hashCode()方法傳回的是int類型的資料,總共4位元組,一共32位。

因為哈希表bucket數組的容量為2的幂次方,是以是僅在目前數組mask上的比特上變化的哈希值将會總是發生碰撞。

hashCode的高16位異或低16位得到哈希值,這樣既減少了哈希碰撞,同時也不是很複雜,沒有增加多少系統的開銷。

為什麼(hashcode >>> 16)​​

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

getNode()

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

首先會檢查第一個Node,如果不是,就判斷這個Node上面是不是一棵樹,是樹就在樹上查找,不然就依次的按連結清單順序查找。

大緻示意圖如下所示。

java 集合之HashMap 源碼閱讀記錄

我們還可以看到,源碼中找對應的bucket是通過這個(n - 1) & hash實作的,這就是為什麼哈希表的容量必須為2的幂次方的原因,可以使用位運算快速求出要查詢的hash碼所在的bucket 。如上所示,容量為8的哈希表,n-1=0111 ,與hash碼相與,正好消掉高位,使得隻能映射到0-7區間内,也等價于hash%8。但是一般位運算會比較快

put()

調用putVal()方法,這裡第四個參數對應putVal()的onlyIfAbsent(),寫死了,為false。

為true時,隻有哈希表不存在這個key,才會put

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

putVal()

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)
            n = (tab = resize()).length;
            //取所在的bucket,如果為null,則建立一個
        if ((p = tab[i = (n - 1) & hash]) == 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))))
                e = 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);
                        //這裡設定的是一個門檻值,表明連結清單的長度如果剛好達到TREEIFY_THRESHOLD常量,就調用treeifyBin()方法,将連結清單轉為紅黑樹
                        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;
                }
            }
            //如果找到了這個key已經存在,覆寫掉以前的鍵值對映射
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //快速失敗機制的modCount
        ++modCount;
        //先插入,再擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }      

這裡有一個有意思的地方就是,連結清單轉為紅黑樹的部分,大家試想,為什麼不插傳入連結表的時候,連結清單維護的資料數超過TREEIFY_THRESHOLD常量就直接轉為紅黑樹呢?

這裡非常類似懶加載,懶惰标記之類的思想。因為并不是每一個連結清單的資料我們都需要将他轉化為紅黑樹,因為轉化是需要時間消耗的,這個工作我們不一定要做。隻有等别人查詢到他的時候,我們才轉化。就好像平時我們做工作,沒必要把所有事都按照最終标準來,說不定我們弄的那個東西别人根本不會來查,這樣我們的工作就沒有意義。是以不要用勤奮要掩蓋你戰術上的懶惰就是這個意思。

當連結清單長度大于TREEIFY_THRESHOLD常量時,并且此時哈希buckets數組容量必須大于MIN_TREEIFY_CAPACITY才能将連結清單轉為紅黑樹

remove(Object key)

其實感覺有時候源碼為啥要封裝成多個函數?

但是看的時候又會發現,你能夠很清晰的明白這個函數是幹嘛用的,是以把業務盡量拆成了單一職責的業務,是為了友善調試和理清業務邏輯吧。

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }      

removeNode()

參數

  1. hash: key的hash值
  2. key
  3. value
  4. matchValue 如果為真,隻在值相等時移除
  5. movable 如果為false,則在移除時不要移動其他節點
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;
        //找對對應的bucket
        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
            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;
    }      

擴容操作resize()

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {  //哈希表平常擴容進入的分支,而下面的else分支對應的HashMap幾個構造函數對應的情況
            if (oldCap >= MAXIMUM_CAPACITY) {
            //threshold設定到Integer的最大值,但是并不擴容,傳回
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //數組大小*2
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 門檻值也乘2
        }
        else if (oldThr > 0) //對應的HashMap(int initialCapacity, float loadFactor)構造出來的hashMap,第一次put的時候進入
            newCap = oldThr;
        else {               // 其他構造方法,第一次put的時候進入
            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) { //該bucket不為null
                    oldTab[j] = null; //原資料置為null,資料已經由e接手
                    if (e.next == null) //該bucket隻有一個元素
                        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;
                            //判斷這條元素連結清單是否需要更換bucket
                            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);
                        //不需要移動bucket,在新表的 原bucket位置 也就是j放入這個連結清單
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //需要移動bucket,在新表的 原bucket+原表容量 位置放入這個連結清單
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }      

這裡面連結清單複制那一段代碼維護了兩個連結清單,然後分别放在新表不同的bucket的裡面

  • 一類連結清單是需要移動bucket的元素
  • 一類連結清單維護的是不需要移動bucket的元素

    比如我們原來哈希表的bucket數目為8,那麼編号就是0-7

現在我們老表裡面的2号bucket下存放的有10,18,26等hash碼的元素,那麼分别移動到新表的哪些bucket裡面呢?

10&8=8 需要移動到2+8(原來的容量大小)的bucket裡面

18&8=0 不需要移動bucket

26&8=8 需要移動到2+8(原來的容量大小)的bucket裡面

存放在老表的2号bucket裡面的元素隻可能是哈希碼為8x+2(x為自然數),是以說具有16x+2(x為自然數)的哈希碼元素就不用移動到其他的bucket裡面。

我們隻需要将元素的hash碼與原來的容量大小相與即可知道是否該移動這個元素到新的bucket裡面去。

自定義對象哈希

在HashMap的put()函數運作的代碼中,我們會發現hashCode()和equals()方法是做判斷對比Key所要用到的函數。

hashCode()是Native()方法。

HashMap的equals()是使用Object的equals()方法,如下面代碼所示是直接用==比對的

public boolean equals(Object obj) {
        return (this == obj);
    }      

是以不适用于對象,但是我們使用的Integer,String等包裝類卻可以,進入他們的源碼可以看到都是重寫了這兩個方法的,是以我們自己定義的類也需要重寫這兩個方法。

public static int hashCode(int value) {
        return value;
    }
    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }      

如果我們不重寫這兩個方法

使用如下代碼測試

public class A{
    static class People {
        int age;
        int sex;
        String name;
        public People(int age,int sex,String name){
            this.age = age;
            this.sex = sex;
            this.name = name;
        }
        @Override
        public String toString() {
            return "People{" +
                    "age=" + age +
                    ", sex=" + sex +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    public static void main(String[] args){
        HashMap<People,Integer> hashMap = new HashMap<>();
        hashMap.put(new People(1,0,"tom"),1);
        hashMap.put(new People(1,0,"tom"),2);
        hashMap.put(new People(2,1,"jim"),3);

        for(People people:hashMap.keySet()){
            System.out.print(people);
            System.out.printf(":%d\n",hashMap.get(people));
        }

        System.out.println(new People(1,0,"tom").hashCode());
        System.out.println(new People(1,0,"tom").hashCode());
        System.out.println(new People(2,1,"jim").hashCode());
    }
}      

運作結果

java 集合之HashMap 源碼閱讀記錄

可以看見HashMap存入多個具有同樣值的Key,不然按照HashMap的規則應該将用新的Value覆寫老的Value,隻會保留一個<Key,Value>映射,但是這裡有兩個。

對于hashCode(),我們可以看見具有相同值的兩個People執行個體的哈希碼是不一樣的,這個hashCode()是Native方法。

對于equals(),調用Object.equals()方法

public boolean equals(Object obj) {
        return (this == obj);
    }      

因為java是函數調用是傳的執行個體的位址,同時因為我們new的多個對象執行個體位址不一樣,這樣就會出錯。

在People類裡面重寫Object類的這兩個方法即可

@Override
        public int hashCode() {
            final int prime = 13331;
            long code = 1;
            code = (prime * code + age)%Integer.MAX_VALUE;
            code = (prime * code + sex)%Integer.MAX_VALUE;
            code = (prime * code + name.hashCode())%Integer.MAX_VALUE;
            return (int)code;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null){
                return false;
            }
            if (this == obj){
                return true;
            }
            if (getClass() != obj.getClass()){
                return false;
            }
            People contrast = (People) obj;
            if (age != contrast.age){
                return false;
            }
            if(sex != contrast.sex){
                return false;
            }
            if(!name.equals(contrast.name)){
                return false;
            }
            return true;
        }}