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方法执行流程

总结一下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的节点,需要把前面的所有节点遍历一遍,当这个链表非常长的时候,就会特别慢,为了提高查询效率,我们使用二叉树。
二叉树及红黑树讲解请参考:红黑树详解

posted @ 2020-11-08 23:14  Zs夏至  阅读(133)  评论(1编辑  收藏  举报