集合框架知識系列05 HashMap的源碼分析和使用示例

1、HashMap簡介

HashMap是基於「拉鍊法」實現的散列表。通常用於單線程程序中,JDK 1.8對HashMap進行了比較大的優化,底層實現由以前的「數組+鏈表」改成「數組+鏈表+紅黑樹」。下面先介紹HashMap中一些關鍵的知識點。node

一、哈希表

哈希表是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。下面是百度百科中的一張哈希表示例:
clipboard.png
經常使用的散列方法有: 直接尋址法:取關鍵字或關鍵字的某個線性函數值爲散列地址、數字分析法:分析一組數據,好比一組員工的出生年月日,這時咱們發現出生年月日的前幾位數字大致相同、平方取中法:當沒法肯定關鍵字中哪幾位分佈較均勻時,能夠先求出關鍵字的平方值,而後按須要取平方值的中間幾位做爲哈希地址。數組

二、紅黑樹

紅黑樹是一顆自平衡的二叉查找樹,除了符合二叉查找樹的特定,還有一下一些特色:緩存

  • 節點是紅色或黑色。
  • 根節點是黑色。
  • 每一個葉子節點都是黑色的空節點(NIL節點)。
  • 每一個紅色節點的兩個子節點都是黑色。(從每一個葉子到根的全部路徑上不能有兩個連續的紅色節點)
  • 從任一節點到其每一個葉子的全部路徑都包含相同數目的黑色節點。

下面是一棵紅黑樹的示例圖:數據結構

clipboard.png

三、HashMap中的節點結構

HashMap中經過實現Map.Entry<K,V>接口做爲哈希表節點的,具體代碼以下:app

static class Node<K,V> implements Map.Entry<K,V> {
            
            //hash值
            final int hash;
           //map中的key
            final K key;
            //map中的值
            V value;
           //指向的下一個節點,用於hash表中的鏈表 
           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 K getKey()        { return key; }
            public final V getValue()      { return value; }
            public final String toString() { return key + "=" + value; }
    
            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;
            }
     }

另外,經過定義繼承LinkedHashMap.Entry<K,V> 來定義TreeNode<K,V>做爲紅黑樹的節點,具體代碼在下一節介紹。ide

2、源碼分析

HashMap繼承自AbstractMap,而且實現了Map、Cloneable和Serializable接口,具體的源碼分析以下:函數

public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable
    {
    
        /**
         * 默認初始化容量大小,必須是2的次冪
         */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
        /**
         * 最大的容量
         */
        static final int MAXIMUM_CAPACITY = 1 << 30;
    
        /**
         * 默認負載因子.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        
        /**
         * 鏈表節點轉紅黑樹節點的閾值,9個節點時轉
         */
        static final int TREEIFY_THRESHOLD = 8;
    
        /**
         * 紅黑樹節點轉爲鏈表的閾值,6個節點時轉
         */
        static final int UNTREEIFY_THRESHOLD = 6;
    
        /**
         * 鏈表節點轉紅黑樹節點時,哈希表達最小節點爲64
         */
        static final int MIN_TREEIFY_CAPACITY = 64;
        
        /**
         * 鏈表節點結構
         */
        static class Node<K,V> implements Map.Entry<K,V> {
            final int 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 K getKey()        { return key; }
            public final V getValue()      { return value; }
            public final String toString() { return key + "=" + value; }
    
            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;
            }
        }
    
        /* ---------------- 靜態公用方法 -------------- */
    
        /**
         * 對hashCode的hash計算如總結中圖所示:
         * 在設計hash函數時,由於目前的table長度n爲2的次冪,因此計算下標的時候,可以使用按位與&代替取模%:(n - 1) & hash。
         * 設計者想了一個顧全大局的方法(綜合考慮了速度、做用、質量),就是把高16bit和低16bit異或了一下。
         * 設計者還解釋到由於如今大多數的hashCode的分佈已經很不錯了,就算是發生了碰撞也用O(logn)的tree去作了。
         * 僅僅異或一下,既減小了系統的開銷,也不會形成由於高位沒有參與下標的計算(table長度比較小)時,引發的碰撞。
         */
         
        /**
         *計算hash值,根據key的hashCode計算
         */
        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
        /**
         * 若是對象實現了Comparable接口,則返回其Class對象
         */
        static Class<?> comparableClassFor(Object x) {
            if (x instanceof Comparable) {
                Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
                if ((c = x.getClass()) == String.class) // bypass checks
                    return c;
                if ((ts = c.getGenericInterfaces()) != null) {
                    for (int i = 0; i < ts.length; ++i) {
                        if (((t = ts[i]) instanceof ParameterizedType) &&
                            ((p = (ParameterizedType)t).getRawType() ==
                             Comparable.class) &&
                            (as = p.getActualTypeArguments()) != null &&
                            as.length == 1 && as[0] == c) // type arg is c
                            return c;
                    }
                }
            }
            return null;
        }
            
        /**
         * 若是x和kc匹配,返回k.compareTo(x),不然返回0。
         */
        @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
        static int compareComparables(Class<?> kc, Object k, Object x) {
            return (x == null || x.getClass() != kc ? 0 :
                    ((Comparable)k).compareTo(x));
        }
    
        /**
         * 根據給定的容量大小,返回一個2的次冪大小的值。好比,cap=7,返回8
         */
        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;
        }
    
        /* ---------------- 成員變量 -------------- */
    
        /**
         * 哈希表定義,在第一次使用時初始化
         */
        transient Node<K,V>[] table;
    
        /**
         * 節點緩存
         */
        transient Set<Map.Entry<K,V>> entrySet;
    
        /**
         * map中含有key-value的大小
         */
        transient int size;
    
        /**
         * 修改次數
         */
        transient int modCount;
    
        /**
         * 下次要調整容量的大小 (capacity * load factor).
         */
        int threshold;
    
        /**
         * 哈希表的負載因子
         */
        final float loadFactor;
    
        /* ---------------- 公共操做 -------------- */
    
        /**
         * 給定初始化容量和負載因子的構造方法
         */
        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);
        }
    
        /**
         * 給定初始化大小的構造函數
         */
        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
        /**
         * 無參構造方法
         */
        public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    
        /**
         * 經過給定的map構造一個hashmap,負載因子是0.75
         */
        public HashMap(Map<? extends K, ? extends V> m) {
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            putMapEntries(m, false);
        }
    
        /**
         * 此方法是先構造一個hashMap對象,調用putVal方法將m中的元素入新map
         *
         */
        final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
            //m的大小
            int s = m.size();
            if (s > 0) {
              //table沒有初始化,先計算threshold 
                if (table == null) { // pre-size
                     //獲取容量初始大小,+1能夠節省一次resize
                    float ft = ((float)s / loadFactor) + 1.0F;
                    int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                             (int)ft : MAXIMUM_CAPACITY);
                   //計算threshold 
                    if (t > threshold)
                        threshold = tableSizeFor(t);
                }
                //若是threshold小於s,調整大小
                else if (s > threshold)
                    resize();
                 //調用 putVal將m中的節點元素入此hashmap
                for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                    K key = e.getKey();
                    V value = e.getValue();
                    putVal(hash(key), key, value, false, evict);
                }
            }
        }
    
        /**
         * 返回map中key-value中的數量
         */
        public int size() {
            return size;
        }
    
        /**
         * 返回map中key-value中的數量是否爲0
         */
        public boolean isEmpty() {
            return size == 0;
        }
    
        /**
         * 經過key獲取一個節點元素,若是不存在,返回null
         *
         */
        public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    
        /**
         * Map.get的實現方法
         */
        final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            //若是table不爲空,且長度大於0、經過hash能找到第一個節點
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
                //檢查第一個節點,判斷key是否相等
                if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
               //若是有下一個節點
                if ((e = first.next) != null) {
                    //判斷是否爲紅黑樹節點,若是是,調用getTreeNode方法獲取節點元素
                    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;
        }
    
        /**
         * 返回map中是否包含key
         */
        public boolean containsKey(Object key) {
            return getNode(hash(key), key) != null;
        }
    
        /**
         * 調用putVal方法,添加一個節點
         */
        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
        /**
         * Map.put的實現方法
         *
         * @param hash hash for key
         * @param key the key
         * @param value the value to put
         * @param onlyIfAbsent 若是是true,不替換已存在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爲空,或者大小爲0,調用resize方法
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            //hash後,若是此位置的節點爲null,則新建節點,賦值到此位置
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                //檢查第一個節點,若是key一致,替換節點
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                //若是此節點時紅黑樹節點,調用紅黑樹putTreeVal方法添加節點
                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;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                //根據onlyIfAbsent 判斷是否須要替換value
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
             //size+1
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    
        /**
         * 數組初始化或者加倍
         * @return the table
         */
        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) {
                //若是原數組容量已大於等於最大容量,閾值賦值最大整數,返回原數組
                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
            }
            //若是原閾值大於0,則新閾值就是原閾值
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            //不然,新閾值和新容量都默認
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            //若是新閾值爲0,經過加載因子和新容量計算新閾值
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            //將新閾值賦值threshold 
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                //定義新的數組
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
                 //table指向新的數組
            table = newTab;
            //若是原數組爲空,直接返回新定義數組,第一次put時
            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)
                            //hash到新表
                            newTab[e.hash & (newCap - 1)] = e;
                        //判斷是否爲紅黑樹節點,若是是,調用split方法處理
                        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,則(e.hash & (oldCap - 1))
     * 和(e.hash & (newCap - 1))值是同樣的,說明節點的位置沒有發生變化。這樣作的緣由是oldCap和newCap都是
     * 2的次冪,而且newCap是oldCap的2倍,表示oldCap轉換爲二進制的惟一一個1向高位移位一次。下面舉例說明:
     * 好比,oldCap=16,則newCap=32。若是(e.hash & oldCap) == 0,
     * 由於e.hash & 0x10000 == 0, e.hash & 0x100000 == 0,如今e.hash的位置是由e.hash & 0x1111肯定,
     * 則e.hash & 0x11111 的值也是同樣的。根據這一個二進制位就能夠判斷下次hash定位在
     * 哪裏了。將hash衝突的元素連在兩條鏈表上放在相應的位置
     */
                  //將位置不變的節點放到鏈表loHead 
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                //將位置變化的節點,放到鏈表hiHead
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                             //將loHead放到新表位置
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            //將hiHead放到新表位置
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    }

3、使用示例

一、重寫equals時也要同時重寫hashCode

在HashMap中,做爲key的對象,若是重寫了equals方法,hashCode也要覆蓋重寫,下面經過一個例子說明不重寫會出現什麼問題:源碼分析

public class HashMapTest {
    public static void main(String[] args) {
        Dog dog = new Dog("test1", 1);

        Cat cat = new Cat("test2", 2);


        Map<Dog, String> map1 = new HashMap<>(1);
        map1.put(dog, "測試1");

        Map<Cat, String> map2 = new HashMap<>(1);
        map2.put(cat, "測試2");

        System.out.println("沒有重寫hashCode方法:" + map1.get(new Dog("test1", 1)));
        System.out.println("重寫hashCode方法:" + map2.get(new Cat("test2", 2)));
    }

    /**
     * 只重寫了equals方法
     */
    static class Dog {
        private String name;
        private Integer id;

        public Dog(String name, Integer id){
            this.name = name;
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj){
                return true;
            }

            if (obj == null || obj.getClass() != this.getClass()){
                return false;
            }

            Dog dog = (Dog) obj;
            return (this.id != null) && (this.id.equals(dog.id));
        }
    }

    /**
     * 重寫了equals和hashCode方法
     */
    static class Cat {
        private String name;
        private Integer id;

        public Cat(String name, Integer id){
            this.name = name;
            this.id = id;
        }
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj){
                return true;
            }

            if (obj == null || obj.getClass() != this.getClass()){
                return false;
            }

            Cat cat = (Cat) obj;
            return (this.id != null) && (this.id.equals(cat.id));
        }

        @Override
        public int hashCode() {
            if (this.id == null){
                return 0;
            }
            return this.id;
        }
    }
}

上述代碼運行結果以下:性能

clipboard.png

能夠看到,沒有重寫hashCode方法的對象做爲key,查詢獲得的是null,由於兩個對象的hashCode的並不一致,因此致使取到的是null。測試

二、HashMap的遍歷方式

HashMap提供了對key-value、key、value等多種遍歷方式,下面經過一個示例演示其用法:

public class HashMapIteratorTest {
    public static void main(String[] args) {
        Map<String, String > map = new HashMap<>();
        for (int i = 0; i < 10; i++) {
            map.put(String.valueOf(i), String.valueOf(i));
        }

        entrySetForeach(map);
        entrySetIterator(map);
        keySet(map);
        valueSet(map);
        foreachJdk8(map);
    }

    /**
     * 獲取Map.Entry,而後遍歷key 和value,經過foreach遍歷
     * @param map
     */
    static void entrySetForeach(Map<String , String > map){
        for (Map.Entry<String , String> entry: map.entrySet()
             ) {
            System.out.print("key:" + entry.getKey() + ",value:" + entry.getValue() + "----");
        }
        System.out.println();
    }

    /**
     * 獲取Map.Entry,而後遍歷key 和value,經過Iterator遍歷
     * @param map
     */
    static void entrySetIterator(Map<String , String > map){
        Iterator<Map.Entry<String , String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry<String , String> entry = iterator.next();
            System.out.print("key:" + entry.getKey() + ",value:" + entry.getValue() + "----");
        }
        System.out.println();
    }

    /**
     * 獲取keySet,遍歷key,一樣支持foreach和Iterator遍歷,只實現foreach
     * @param map
     */
    static void keySet(Map<String , String > map){
        for (String string: map.keySet()
             ) {
            System.out.print("key:" + string + "----");
        }
        System.out.println();
    }

    /**
     * 獲取values,遍歷value,
     * @param map
     */
    static void valueSet(Map<String , String > map){
        for (String string: map.values()
             ) {
            System.out.print("value:" + string + "----");
        }
        System.out.println();
    }

    static void foreachJdk8(Map<String , String > map){
        map.forEach((k, v)-> System.out.print("key:" + k + ",value:" + v + "----"));
        System.out.println();
    }
}

4、總結

  1. 在HashMap的使用中,建議設置已知的大小,由於在擴容的時候,resize方法要重建hash表,嚴重影響性能。
  2. HashMap定義和擴展中,大小必須爲2的次冪,這樣作的緣由以下:

    a、計算位置時:(n - 1) & hash能夠實現一個均勻分佈。
      b、hash%length==hash&(length-1)的前提是length是2的次冪。length是2次冪時,能夠用覺得代替取模,提升效率。
  3. 擴容過程當中會新數組會和原來的數組有指針引用關係,因此將引發死循環問題。JDK1.8中已經解決這個問題了。

對hashCode的hash計算

clipboard.png

相關文章
相關標籤/搜索