• hashmap源码阅读


    hashmap源码分析

    什么是map

    在学习java时,在集合部分我们学习了,列表List,集合Set,这两个接口都是继承自Collection接口,还有一个映射集合Map。
    查看map源码注释,我们看源码是怎么介绍Map这个接口的:

    An object that maps keys to values.  A map cannot contain duplicate keys;
    each key can map to at most one value.
    
    • 是一个将key映射到值的对象。一个map不能包含重复的key,每一个key可以映射最多一个值。也就是说key-value是一一对应的。
    This interface takes the place of the <tt>Dictionary</tt> class, which
      was a totally abstract class rather than an interface.
    
    • 是一个替代dictionary字典类的接口。

    什么是hashmap

    hashmap是基于hash表的map接口的实现。
    hashmap的底层实现:

    • 在jdk8前,使用数组+链表实现
    • 在jdb8后,使用数组+链表+红黑树实现。

    主要以jdb8的源码来学习。

    数组+链表+红黑树

    在jdb8中,hashmap使用hash桶来存储数据,源码见下:

            /**
             * 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;
    

    可以看出hash桶就是一个数组,也就是hash存储结构中的数组。这个数组中存的是Node,查看Node源码:

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

    Node是hashmap的一个内部类,查看这个类的内容,可以看出这个Node节点有 hash,key,value,next等属性,重点在next属性,next的类型仍然是Node节点。
    那么Node节点不断next下去就形成了链表。
    至此我们已经查看到了hashmap的数组和链表的实现,我们前面说jdk8之前就是用这两种来做hashmap的底层存储的。那么jdk8后加入了红黑树在哪儿实现的?

        /**
         * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
         * extends Node) so can be used as extension of either regular or
         * linked node.
         */
        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;
            // ...具体内容省略
        }
    

    这一部分就是红黑树节点的实现代码。此处了解一下,后面再详细分析红黑树。
    了解至此,hashmap的底层数据结构已经了解的很清楚了。那么我们首先思考一个问题:

    hashmap为什么要选择这样的存储方式?

    首先我们思考一下需求:hashmap需要实现什么功能?hashmap是一个映射集合。集合都有什么功能?
    读和写,也就是说要往集合中存数据,还要能取出来,能遍历。
    思考数组和链表的特性:

    • 数组查询快,增删改慢
    • 链表查询慢,增删改快

    hashmap使用数组+链表的方式,同时使用了两种方式的优点,降低时间复杂度。

    数组和链表在hashmap中是如何组合的?

    hashmap在调用构造方法时,会传入一个initialCapacity参数,表示hashmap的初始容量

        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    

    当没有传这个值时,取默认值。

        /**
         * The load factor used when none specified in constructor.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        /**
         * Constructs an empty <tt>HashMap</tt> with the default initial capacity
         * (16) and the default load factor (0.75).
         */
        public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    

    先对hashmap的属性进行一个了解:

    • table: 也就是hash表,是Node数组,我们也叫hash桶。存储数据的底层结构。
    • entrySet: hashmap中所有的键值对的集合
    • loadFactor: 加载因子,用来判断是否需要扩容
    • threshold: 阈值,用来判段是否需要扩容
    • size:包含的键值对的数量
    • modCount: hashmap中元素修改的次数

    当我们调用hashmap的构造方法创建对象时,如果调用无参构造,那么就会使用默认的加载因子0.75,并在第一次往hashmap中存数据的时候初始化此hash桶。

        /**
         * The default initial capacity - MUST be a power of two.
         */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
        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;
            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);
                            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;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    

    注意看putval方法中,首先会判断当前table属性是否为空,如果为空的话,调用resize()方法。

        final Node<K,V>[] resize() {
            ...
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            ...
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                ...
            }
            return newTab;
        }
    

    在resize方法中,初始化一个初始容量为16的Node数组。

    如果调用的是有参构造制定了初始大小,那么hashmap会对这个初始大小进行计算:

        public HashMap(int initialCapacity, float loadFactor) {
            ...
            this.loadFactor = loadFactor;
            this.threshold = tableSizeFor(initialCapacity);
        }
    
        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;
        }
    

    重点看这个tableSizeFor方法,这个方法做了什么事呢?
    它把传过来的容量进行了位运算,通过高16位和低16位的异或运算,得到大于等于指定的initialCapacity的最小的2的幂。
    分析tableSizeFor方法:
    如果指定的cap为2的幂:

    • cap=32: 经过cap-1,得n=31;经过n|=n>>>1,得n=31;经过n|=n>>>2,得n=31;经过n|=n>>>4,得n=31;经过n|=n>>>8,得n=31;
      经过n|=n>>>16,得n=31;最终经过判断是否超过最大值,返回结果为32。
      如果指定的cap不为2的幂:
    • cap=27: 经过cap-1, 得n=26;经过n|=n>>>1,得n=31;后面就跟上面一样了,最终返回结果还是32。

    当cap为2的幂时,那么经过cap-1后,转换为2进制,无论怎么|运算值都不变。例如:32-1=31,31的二进制为11111,无论左移多少位进行或运算,最终结果都是31,返回值31+1=32。
    当cap不为2的幂时,经过cap-1后,转换为2进制,经过不断的或运算, 因为是左移,因此最高位是不会变的,就是1,后边经过多次位移并或运算后,总能将后面所有的数都变为1,因此最终得到的是
    当前数的最小2的幂-1,最终返回的就是大于等于当前数的2的幂。
    得出结论:无论指定的初始容量是多少,最终hashmap的容量都是2的幂。
    我们回到有参构造的方法,可以看到tableSizeFor方法计算的结果,赋值给了threshold属性。

    什么是阈值threshold?

    在hashmap中,阈值=容量加载因子。也就是说threshold=容量加载因子。
    容量就是Node[]数组的长度。
    当hashmap的size大于阈值时就会出发扩容,代码如下:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            ...
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    

    在put方法中,数据存入成功后,++size,并用当前size与阈值判断,如果当前size大于阈值的话,就开始扩容。

    hashmap容器初始化

    了解了阈值之后我们再次回到初始化的方法里。现在的问题是,我们现在知道了阈值=容量*加载因子。
    但是在上面的有参构造中,我们将传的初始容量赋值给了阈值。???此时想的肯定是这是什么操作。
    在构造方法中,只是对阈值和加载因子设置指定值,并没有初始化map容器。
    然后在上面我们说了,初始化容器是在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;
    // 当创建map对象成功后,第一次调用put方法时,此时table肯定是空的,然后就会进入下面的条件
            if ((tab = table) == null || (n = tab.length) == 0)
                // 初始化容器
                n = (tab = resize()).length;
            ...
            ...
        }
    

    现在我们进入的是初始化容器,也就是说现在整个hashmap容器的table还是空的,需要对Node数组初始化,然后此时要记住
    我们目前只对threshold和加载因子赋值了,如果没有指定值就是取默认值,然后此时的threshold的值就是构造方法中指定的容器大小。
    然后我们进入resize方法:

    final Node<K,V>[] resize() {
            // 上面说过,resize方法是扩容,如果容器是空的话,那么就是初始化容器。
    // 假设我们调用有参构造时传的initialCapacity是27,那么最终经过tableSizeFor方法位运算后,最终的容量就是32,然后赋值给了threshold。
    // 加载因子取默认值0.75。 以上就是前提条件,在这个条件下我们继续往下走
    
            // 第一次初始化容器,因此table是空的。
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            // 但是因为指定初始化容量的缘故,因此此时的threshold的值是:32,注意这个值赋给了oldThr。
            int oldThr = threshold;
            int newCap, newThr = 0;
            // 因为第一次,所以oldCap在上面给的值是0,进入else if
            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
            }
            // 上面我们给oldThr的值32,因此进入此条件。
            else if (oldThr > 0) // initial capacity was placed in threshold
                // 注意这里将oldThr的值给了newCap,newCap也就是新的容量32。
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                // 当调用无参构造时,会进入这里,使用默认的容量大小
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            
            // newThr值并没有被修改,因此还是0
            if (newThr == 0) {
                // 在这里计算了新的容器的阈值ft=24,这个ft在下面又被赋给了newThr,
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }   
            // 将新的阈值给threshold,也就是说,当运行到这里的时候,我们的阈值就是我们期望的容量*加载因子
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
            // 在这一步对table进行了初始化,初始化容量值为newCap,也就是我们调用构造方法赋值时传的参数32。
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            ...
            return newTab;
        }
    

    上面演示了一个hashmap容器初始化的过程。下面我们继续往下看hashmap的扩容。

    hashmap扩容

    我们之前说,当容器的size大于阈值的时候就会触发扩容,扩容也是调用resize方法,上面跟踪了初始化的过程,下面继续跟踪扩容的过程。

    final Node<K,V>[] resize() {
    // 此时调用resize方法作为扩容时,那么首先table是不为空的。也就是说table的size以及大于阈值了。才会触发扩容,到达这里。
            // 将table赋值给变量oldTab,并获取oldTab的容量和阈值
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            // 因为是扩容,所以oldCap肯定大于0,进入条件
            if (oldCap > 0) {
                // 如果table的容量已经大于等于最大值的话,那么就没办法扩容了,仍然返回旧的table
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                // 否则的话,可以扩容。新容器的大小newCap=oldCap<<1,旧的table容量右移1位,也就是变成原容量的2倍。阈值也变成2倍。
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            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);
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            // 执行到这里,就已经得到新的容器的容量以及阈值了,并且初始化了一个容器,新容器的大小是旧的2倍。
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            // 下面就是将旧的容器中的数据存入新的容器中
            if (oldTab != null) {
                // 通过for循环,遍历老容器中的数据--链表
                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;
                                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);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

    上面就是一个hashmap的扩容过程。
    为什么要扩容?
    为了减少hash碰撞,因为如果不扩容的话,那么指定初始大小后,如果存放的数据非常多的话,就会造成hash碰撞肯定非常多,那么就会出现单链表长度过长的情况,
    链表的查询速度是非常低的,就会造成hashmap的查询效率非常低,而扩容之后,所有节点会重新计算下标,原来hash碰撞的节点,扩容后可能就不碰撞了,减少了链表的长度
    提高了查询效率。

    小结:

    经过上面的了解,我们现在关于hashmap知道了什么?

    • hash表就是Node[],Node数组的大小就是容量,Node[]也叫hash桶。使用了数组(Node[])+链表(Node)+红黑树(TreeNode)存储数据。
    • hashmap的容量永远是2的幂
    • 当前数量超过阈值时,就会进行扩容。
    • 扩容每次的容量为原容量的2倍。扩容是数组的大小扩大为2倍。
    • 创建map对象后,在第一次调用put方法时,才会初始化容量

    然后我们来针对上面的总结思考问题:

    为什么hashmap的容量必须是2的幂?

    在讲这个问题前,首先来学习一下,hashmap的工作原理是什么?
    我们使用hashmap常用的方法是什么?get和put,就是读数据和写数据,那么hashmap是如何将数据存入hash表中,又是如何取出来的呢?
    查看put方法的源码:

        public V put(K key, V value) {
            // 当调用put方法,并传入参数:key-"name",value-"zhangsan"时
            // 调用putVal方法时传的参数注意 hash(key),也就是对name计算hash
            return putVal(hash(key), key, value, false, true);
        }
        
        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;
                // i=(n-1)&hash 计算name这个key要存放到hash表中时,存放的下标,
                // 并获取这个下标的头节点,如果为空,则直接创建新节点并放入当前下标
                if ((p = tab[i = (n - 1) & hash]) == null)
                    tab[i] = newNode(hash, key, value, null);
                else {
                // 根据key的hash计算下标已经存放节点,发生hahs碰撞。
                // 当代码执行到这里时,我们看一下当前变量的值, p:hash桶中碰撞节点的头节点,key:name;value:zhangsan
                    Node<K,V> e; K k;
                    if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        // 通过equals对第一个节点的key进行判断,如果key已存在的话,就取出来这个节点
                        e = p;
                    else if (p instanceof TreeNode)
                        // 向红黑树节点中插入数据,遍历获取红黑树中此key的节点
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                    else {
                        // 如果p的key与要put的key不一致,则遍历其他的节点,获取此key的节点
                        for (int binCount = 0; ; ++binCount) {
                            if ((e = p.next) == null) {
                                // 如果下一个节点是空,那么说明遍历完此链表中还没有存入过此key,那么创建一个新的节点添加到此链表
                                p.next = newNode(hash, key, value, null);
                                // 这个条件是如果当前这个bitCount超过7的话,那么就将此链表转为红黑树。
                                // 但是此时因为在上面新增了一个节点,因此此时的链表长度其实为8,也就是说虽然bitCount是7,但是实例链表长度
                                // 是8,所以说当链表长度超过8时,转为红黑树
                                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;
                        }
                    }
                    // 如果e不为空,说明此hash表中已经存在此节点,那么只需要替换值就行了,因为是替换,所以长度不会改变,不需要考虑链表转红黑树的事
                    if (e != null) { // existing mapping for key
                        V oldValue = e.value;
                        if (!onlyIfAbsent || oldValue == null)
                            e.value = value;
                        afterNodeAccess(e);
                        return oldValue;
                    }
                }
                // 扩容
                ++modCount;
                if (++size > threshold)
                    resize();
                afterNodeInsertion(evict);
                return null;
            }
        }
    
        // 将链表转为数
        final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index; Node<K,V> e;
            // 注意:当hash桶的容量小于64时,只会触发扩容
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                resize();
            else if ((e = tab[index = (n - 1) & hash]) != null) {
            // 链表转红黑树
                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);
            }
        }
    
    总结一下hashmap调用put方法时的工作原理:

    调用put方法时,一定会传入key,value:

    1. 对key取hash值
    2. 根据第一步求的的hash值与hash桶的长度进行位运算,得到这个key最终要存放在hash桶的下标。
      也就是说,我hash桶是一个Node数组,里面存放了好多的Node节点,这些Node每一个都是一个链表,也就是说我数组中,存放了好多的链表,我要计算新来的key到底该
      放在哪儿个链表里。怎么计算呢? 
      我们上面说了,hash桶的长度永远都是2的幂,因此假设长度是n的话,那么就使用 hash&(n-1) 计算下标,为什么要n-1? 
      1. n-1正好是数组的下标最大值
      2. 因为长度是2的幂,那么如果减1就能保证最高位往后都是1。比如(8-1)的二进制为 0111,(16-1)的二进制为 1111,(32-1)的二进制为 0011 1111。
      这样的话,通过 hash&(n-1),最终无论hash值有多大,最终的结果都在(n-1)的范围内。假设我现在的hash是79,hash表容量是8,那么最终的运算就是
      0100 1111&0000 0111 = 0111,那么这个key就是放在7这个下标,再比如hash值是65,转为二进制就是 01000001&0111=0001,因此此key的下标就是1。
      
    3. 计算出下标以后,就拿这个下标当前链表的头节点:
      • 如果这个下标还没有节点,是null的,那么就创建一个新的节点放入这个下标。
      • 如果这个下标已经有节点了,说明发生了hash碰撞。那么获取这个下标的头节点,也就拿到了整条链表,如果此节点是红黑树节点,那么也就拿到了根节点。
        然后在通过 next 获取子节点,通过.equals()方法,将当前节点的key与要put的key进行对比,如果key相等的话,那么说明此key已经存在,更新key的值,并返回旧的值。
        put方法至此结束。
        如果遍历到最后一个节点,仍然没有找到key值一样的(.equals()匹配),那么则创建一个新节点,并插入当前链表的末尾。如果当前,链表长度超过8的话,那么就将链表转为
        红黑树。
        如果桶的容量小于64,则只会发生扩容,桶容量大于64时,如果链表超过8才会转为红黑树
    4. 数据插入成功,判断当前是否需要扩容,如果需要则扩容。

    在来看下get方法的源码:

        public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    
        /**
         * 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) {
            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;
        }
    

    前面看了put方法的源码后,get方法就比较简单了。与put方法类似,调用get方法一定后传一个key值。只需要对这个key取hash。然后通过位运算计算下标。
    获取下标节点,通过 next 方法遍历链表或通过红黑树遍历节点,通过.equals()方法判断key是否相等,如果相等则返回此节点的值。如果查不到相等的节点则返回null。

    再来看remove方法源码:

        public V remove(Object key) {
            Node<K,V> e;
            return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
        }
    
        /**
         * Implements Map.remove and related methods.
         *
         * @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;
            // 根据key的hash值找到在hash桶中哪儿个节点下
            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;
                // 判断链表的头节点是否匹配key
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    node = p;
                else if ((e = p.next) != null) {
                    // 如果是树节点,则从树节点中获取此key的节点
                    if (p instanceof TreeNode)
                        node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                    else {
                    // 遍历链表的子节点,获取匹配的key的节点
                        do {
                            if (e.hash == hash &&
                                ((k = e.key) == key ||
                                 (key != null && key.equals(k)))) {
                                node = e;
                                break;
                            }
                            p = e;
                        } while ((e = e.next) != null);
                    }
                }
                // 如果查到匹配的节点
                if (node != null && (!matchValue || (v = node.value) == value ||
                                     (value != null && value.equals(v)))) {
                    // 是树节点就从树节点中删除,是头节点的话,就移除头节点,是子节点的话,就将上一个节点的next指针指向下一个节点。
                    // 比如说链表 a->b->c,如果要移除b,那么只需要将a节点的下一个节点指向c,也就是a.next=b.next,就移除了b。
                    if (node instanceof TreeNode)
                        // 需要注意移除红黑树节点的话,如果树的节点树小于6,那么就将树降为链表
                        ((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;
        }
    

    其实上面学了put方法后,下面的会简单很多。remove方法仍是根据key的hash以及.equals()定位节点,如果没有找到节点,则返回null。
    如果找到此节点的话,并移除此节点,如果是红黑树数的话,在移除数据后,如果数据量小于6,则将红黑树恢复成链表。

    hash

    我们注意到,无论是put,get,remove,还是hashmap这个名字,都有一个重要的单词--hash。 再回过头看一下hash这个方法:

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

    可以看到,hash的取值是通过 (h=key.hashCode())^(h>>>16) 计算出来的。
    为什么hash要使用异或?
    主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。

    hash: Object类的hashCode方法,类重写此方法,调用时会将类对象计算出一个hash值。
    hash值相等,两个对象不一定相等。 a.hash == b.hash, a.equals(b)不一定为true。
    两个对象相等,那么hash值一定相等。 a.equals(b) == true, a.hash == b.hash.

    为什么要设计阈值这个东西?

    设计阈值就是为了提高效率,如果没有阈值的话,也就是说阈值是1,那么空间利用率高了,那么扩容的触发条件也就变高了, 如果hash桶足够打,那么触发一次扩容需要的数据
    也就非常多了,这样就会造成每个节点下的链表长度都会很长,链表长的话,查询效率就降低了。有了阈值,就可以控制什么时候该扩容,提高查询效率。
    那么为什么阈值要为0.75呢?
    因为阈值如果高于0.75就会出现上面的情况,如果阈值低的话,那么空间利用率就会降低,频繁的触发扩容,扩容是消耗性能的。因为hash桶是用数组,数组是定长的,
    那就意味着,扩容就需要创建一个新的数组,并将原数组中的所有Node,重新计算下标,放入新的数组中。

    为什么创建hashmap推荐指定初始大小

    因为如果创建hashmap后,需要存入的数据非常多的话,不指定初始大小,就会使用默认的16,16大小肯定不够,就会频繁的触发扩容,前面也说了,频繁扩容影响性能。
    如果创建时就指定合适的初始大小,那么在容器初始化时,就会初始化比较大的容量,避免了很多次扩容。提高效率。

    为什么要设计红黑树

    为了提高查询性能。如果只使用链表存储的话,那么如果某一条链表非常长的话,就会造成hashmap的查询速度非常慢。那么为了提高查询效率,自然就想到使用二叉树,
    但是使用平衡二叉树的话,在某种特殊情况下,还是变成了链表。那么就想到了平衡二叉树,平衡二叉树要求比较严格,为了维护平衡二叉树所付出的代价太大。所以hashmap使用
    了平衡二叉树。

    红黑树

    上面在很多地方都出现了红黑树,那么红黑树是什么?
    是一种特殊的平衡二叉树。

    红黑树的特点

    • 每个节点非红即黑
    • 根节点为黑色
    • 所有叶子节点都为黑色的空节点
    • 如果节点是红色的,那么他的子节点一定是黑色的(从每个叶子到根的所有路径上不能有两个连续的红色节点,反之不一定)
    • 从根节点到叶节点或空自节点的每条路径,必须包含相同数目的黑色节点(即相同的黑节点高度)。

    红黑树在线生成网站
    在jdk8前,我们使用链表存储数据,那么上面也说了,链表的缺点就是查询速度慢,因为链表查询一条数据需要从头节点开始,遍历整条链表。我们看这样一条链表:
    1->2->3->4->5->6->7->8->9->10
    如果要查找10的节点,需要把前面的所有节点遍历一遍,当这个链表非常长的时候,就会特别慢,为了提高查询效率,我们使用二叉树。
    二叉树及红黑树讲解请参考:红黑树详解

  • 相关阅读:
    QQ音乐 删除历史登录设备
    mweb发布文章为什么默认TinyMCE编辑器?
    Mac 安装 Homebrew
    uniapp配置scss支持
    PHPRedis教程之geo
    CentOS7通过YUM安装NGINX稳定版本
    CentOS7通过YUM安装MySQL5.6
    更换composer镜像源为阿里云
    使用chattr禁止文件被删除
    centos 7 源码安装 mysql 5.6
  • 原文地址:https://www.cnblogs.com/Zs-book1/p/13946405.html
Copyright © 2020-2023  润新知