写点什么

🔎【Java 源码探索】深入浅出的分析 HashMap(JDK8)

发布于: 2021 年 05 月 28 日
🔎【Java源码探索】深入浅出的分析HashMap(JDK8)

【每日一句】

一个人最大的挑战,是如何去克服自己的缺点。

【基本原理】

  • HashMap 是一个基于 map 接口实现的散列表,存储内容是键值对 (key-value) 映射,并且键和值都可以使用 null,因为 key 不允许重复,因此只能有一个键为 null

  • HashMap 使用 hash 算法进行数据的存储和查询。

  • HashMap 的实现用的是数组+链表+红黑树的结构,也叫哈希桶。在 jdk 1.8 之前都是数组+链表的结构,因为在链表的查询操作都是 O(N)的时间复杂度,如果当节点数量多,转换为红黑树结构,那么将会提高很大的效率,因为红黑树结构中,增删改查都是 O(log n)。

【基本特性】

  • HashMap 的散列表是懒加载机制,在第一次 put 的时候才会创建

  • 它根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的

  • HashMap 最多只允许一条记录的键(Key)为 null,允许多条记录的值为 null

  • HashMap 是无序不重复的,而且 HashMap 是线程不安全的。

  • HashMap 默认情况下使用一个 Entry 表示键值对 key-value,用 Entry 的数组保存所有键值对,Entry 通过链表的方式链接后续的节点 (1.8 后会根据链表长度决定是否转换成一棵树类似 TreeMap 来节省查询时间,Node 节点会采用 LinkedHashMapEntry 的属性),Entry 通过计算 key 的 hash 值来决定映射到具体的哪个数组(也叫 Bucket) 中。

  • HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。

  • 如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。

【优化点】

链表改为红黑树:时间复杂度(O(N) —> O(log(N)))

【原理简述】

【put 方法】

  • 输入参数

  • key 值,value 值

  • 运作流程

  • 首先针对于传入的对 Key 求 hash 值,然后再计算下标

  • 如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的 hash 值相同,需要放到同一个 bucket 中,代表着属于链表)。

  • 如果 hash 值发生碰撞后,以链表的方式链接到后面

  • 如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于 6,就把红黑树转回链表。

  • 如果 key 的 hashcode 相同且 value 也相同的情况下,就替换旧值

  • 如果桶到达阈值(Threshold)后(初始化容量(16)以及加载因子(0.75)),就需要 resize(扩容 2 倍后并且进行重排(重新 hash 和重新排版))

数据结构图

HashMap 属性代码

首先,需要记住的是,JCF 的一个传统模式,就是集成 AbstractXXX 抽象类和实现所有的基础接口 XXX,XXX(Map,List,Set,Collection 等),并且可以实现序列化和克隆


属性默认值


public class HashMap<K,V> extends AbstractMap<K,V>    implements Map<K,V>, Cloneable, Serializable {    //序列号,序列化的时候使用    private static final long serialVersionUID = 362498820763181265L;    //默认容量,为2的4次方,即为16,   必须为 2 的 n 次方 (一定是合数)    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;    //最大容量,为2的30次方。    static final int MAXIMUM_CAPACITY = 1 << 30;    //加载因子,用于扩容使用。    static final float DEFAULT_LOAD_FACTOR = 0.75f;   //链表转成红黑树的阈值。即在哈希表扩容时,当链表的长度(桶中元素个数)超过这个值的时候,进行链表到红黑树的转变    static final int TREEIFY_THRESHOLD = 8;    //红黑树转为链表的阈值。即在哈希表扩容时,如果发现链表长度(桶中元素个数)小于 6,则会由红黑树重新退化为链表    static final int UNTREEIFY_THRESHOLD = 6;    //当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。  //HashMap 的最小树形化容量。这个值的意义是:位桶(bin)处的数  //据要采用红黑树结构进行存储时,整个Table的最小容量(存储方式由  //链表转成红黑树的容量的最小阈值) 当哈希表中的容量大于这个值  //时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是  // 树形化为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 *  // TREEIFY_THRESHOLD    static final int MIN_TREEIFY_CAPACITY = 64;}
复制代码


属性参数


public class HashMap<K,V> extends AbstractMap<K,V>    implements Map<K,V>, Cloneable, Serializable {
transient Node<K,V>[] table; //将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。 transient Set<Map.Entry<K,V>> entrySet; //元素数量 transient int size; //统计该map修改的次数,用来记录 HashMap 内部结构发生变化的次数,主要用于迭代的快速失败机制 transient int modCount; //HashMap 的门限阀值/扩容阈值,所能容纳的 key-value 键值对极 // // 限,当size>=threshold时,就会扩容,计算方法:容量capacity * 负 // 载因子load factor 。 int threshold; //加载因子 final float loadFactor;}
复制代码


  • Node[] table:的初始化长度 length(默认值是 16),loadFactor 为负载因子 (默认值 DEFAULT_LOAD_FACTOR 是 0.75),threshold 是 HashMap 所能容纳的最大数据量的 Node(键值对) 个数

  • threshold = length * loadFactor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

  • **这里我们需要加载因子 (load_factor),加载因子默认为 0.75,当 HashMap 中存储的元素的数量大于 (容量 × 加载因子),也就是默认大于 16*0.75=12 时,HashMap 会进行扩容的操作 **。

  • size:这个字段其实很好理解,就是 HashMap 中实际存在的键值对数量注意和 table 的长度 length、容纳最大键值对数量 threshold 的区别。

  • modCount:字段主要用来记录 HashMap 内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化。

  • put 新键值对,某个 key 对应的 value 值被覆盖不属于结构变化。


HashMap 的内部功能实现很多,本文主要从根据 key 获取哈希桶数组索引位置、put 方法的详细执行、扩容过程等具有代表性的点深入展开讲解。


构造函数


第一个默认初始化+默认加载因子,第二个设置初始容量+初始化默认加载因子,第三个设置初始容量和加载因子


  // 默认初始化容量+默认负载因子    public HashMap() {        this.loadFactor = DEFAULT_LOAD_FACTOR;    }   // 自定义初始化容量+默认负载因子    public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }   // 自定义初始化容量以及负载因子    public HashMap(int initialCapacity, float loadFactor) {        if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal initial capacity: " +                                               initialCapacity);        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        if (loadFactor <= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);        this.loadFactor = loadFactor;        this.threshold = tableSizeFor(initialCapacity);    }
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //获取该map的实际长度 int s = m.size(); if (s > 0) { //判断table是否初始化,如果没有初始化 if (table == null) { // pre-size /**求出需要的容量,因为实际使用的长度=容量*0.75得来的,+1是因为小数相除,基本都不会是整数,容量大小不能为小数的,后面转换为int,多余的小数就要被丢掉,所以+1,例如,map实际长度22,22/0.75=29.3,所需要的容量肯定为30,有人会问如果刚刚好除得整数呢,除得整数的话,容量大小多1也没什么影响**/ float ft = ((float)s / loadFactor) + 1.0F; //判断该容量大小是否超出上限。 int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); /**对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32**/ if (t > threshold) threshold = tableSizeFor(t); } //如果table已经初始化,则进行扩容操作,resize()就是扩容。 else if (s > threshold) resize(); //遍历,把map中的数据转到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); } } }
复制代码


节点对象


HashMap 内部类 TreeNode,该类是一个红黑树结构


static final class TreeNode<K,V> extends   LinkedHashMap.LinkedHashMapEntry<K,V> {     // red-black tree links        TreeNode<K,V> parent;        TreeNode<K,V> left;        TreeNode<K,V> right;    // needed to unlink next upon deletion        TreeNode<K,V> prev;        boolean red;        TreeNode(int hash, K key, V val, Node<K,V> next) {            super(hash, key, val, next);        }}
复制代码


HashMap 内部类 Node, 结构为单向链表。


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;        }    }
复制代码


哈希方法


解决 Hash 的的冲突的 hash()方法,HashMap 的 hash 计算时先计算 hashCode(), 然后进行二次 hash


// 计算二次Hashint hash = hash(key.hashCode());// 通过Hash找数组索引int i = hash & (tab.length-1);
static final int hash(Object key) { int h; // 先获取到key的hashCode,然后进行移位再进行异或运算,为什么这 //么复杂,不用想肯定是为了减少hash冲突 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
复制代码


这个方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置,而 HashMap 底层数组的长度总是 2 的 n 次方。当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计


  • 假设 h=5,length=16, 那么 h & length - 1 将得到 5;

  • 假设 h=6,length=16, 那么 h & length - 1 将得到 6

  • 假设 h=15,length=16, 那么 h & length - 1 将得到 15;

  • 但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了。这样保证计算得到的索引值总是位于 table 数组的索引之内


添加元素


   public V put(K key, V value) {    // 对key的hashCode()做hash    return putVal(hash(key), key, value, false, true);}
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table为空或者length=0时,以默认大小扩容,n为table的长度 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 计算index,并对null做处理,table[i]==null if ((p = tab[i = (n - 1) & hash]) == null) // (n-1)&hash 与Java7中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。 // 直接插入 tab[i] = newNode(hash, key, value, null); else { // 若i位置上的值不为空,判断当前位置上的Node p 是否与要插入的key的hash和key相同 Node<K,V> e; K k; // 若节点key存在,直接覆盖value if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 判断table[i]该链是否是红黑树,如果是红黑树,则直接在树中插入键值对 else if (p instanceof TreeNode) // 不同,且当前位置上的的node p已经是TreeNode的实例,则再该树上插入新的node e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // table[i]该链是普通链表,进行链表的插入操作 else { // 在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。 for (int binCount = 0; ; ++binCount) { // 如果遍历到了最后一个节点,说明没有匹配的key,则创建一个新的节点并添加到最后 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 链表长度大于8转换为红黑树进行处理 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 遍历过程中若发现 key 已经存在直接覆盖 value 并跳出循环即可 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 已经存在该key的情况时,将对应的节点的value设置为新的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 是否超多了最大容量 threshold,如果超过,进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}
复制代码


红黑树结构的 putVal 方法


final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {    Class<?> kc = null;    boolean searched = false;    TreeNode<K,V> root = (parent != null) ? root() : this;    for (TreeNode<K,V> p = root;;) {        int dir, ph; K pk;        if ((ph = p.hash) > h)            dir = -1;        else if (ph < h)            dir = 1;        else if ((pk = p.key) == k || (k != null && k.equals(pk)))            return p;        else if ((kc == null &&                    (kc = comparableClassFor(k)) == null) ||                    (dir = compareComparables(kc, k, pk)) == 0) {            if (!searched) {                TreeNode<K,V> q, ch;                searched = true;                if (((ch = p.left) != null &&                        (q = ch.find(h, k, kc)) != null) ||                    ((ch = p.right) != null &&                        (q = ch.find(h, k, kc)) != null))                    return q;            }            dir = tieBreakOrder(k, pk);        }        TreeNode<K,V> xp = p;        if ((p = (dir <= 0) ? p.left : p.right) == null) {            Node<K,V> xpn = xp.next;            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);            if (dir <= 0)                xp.left = x;            else                xp.right = x;            xp.next = x;            x.parent = x.prev = xp;            if (xpn != null)                ((TreeNode<K,V>)xpn).prev = x;            moveRootToFront(tab, balanceInsertion(root, x));            return null;        }    }}
复制代码


总结 put()方法大致的思路为:


  • 对 key 的 hashCode()做 hash,然后再计算 index;

  • 如果没碰撞直接放到 bucket 里;

  • 如果碰撞了,以链表的形式存在 buckets 后;

  • 如果碰撞导致链表过长 (大于等于 TREEIFY_THRESHOLD=8),就把链表转换成红黑树;

  • 如果节点已经存在就替换 old value(保证 key 的唯一性)

  • 如果 bucket 满了 (超过 load factor*current capacity),就要 resize


具体步骤为


  1. 如果 table 没有使用过的情况(tab=table)==null || (n=tab.length) == 0,则以默认大小进行一次 resize

  2. 计算 key 的 hash 值,然后获取底层 table 数组的第 (n-1) & hash 的位置的数组索引 tab[i] 处的数据,即 hash 对 n 取模的位置,依赖的是 n 为 2 的次方这一条件

  3. 先检查该 bucket 第一个元素是否是和插入的 key 相等 (如果是同一个对象则肯定 equals)

  4. 如果不相等并且是 TreeNode 的情况,调用 TreeNode 的 put 方法否则循环遍历树节点,

  5. 如果找到相等的 key 跳出循环否则达到最后一个节点时将新的节点添加到链表最后, 当前面找到了相同的 key 的情况下替换这个节点的 value 为新的 value。

  6. 最后如果新增了 key-value 对,则增加 size 并且判断是否超过了 threshold, 如果超过则需要进行 resize 扩容


扩容尺寸


  • 扩容 (resize) 就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素

  • 当然 Java 里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶

  • 由于需要考虑 hash 冲突解决时采用的可能是链表也可能是红黑树的方式,因此 resize 方法相比 JDK7 中复杂了一些

  • rehashing 触发的条件:

  • 超过默认容量 * 加载因子

  • 加载因子不靠谱,比如远大于 1


在 HashMap 进行扩容时,会进行 2 倍扩容,而且会将哈希碰撞处的数据再次分散开来,一部分依照新的 hash 索引值呆在 “原处”,另一部分加上偏移量移动到新的地方


  • 具体步骤为:

  • 首先计算 resize() 后的新的 capacity 和 threshold 值

  • 如果原有的 capacity 大于零则将 capacity 增加一倍,否则设置成默认的 capacity

  • 创建新的数组,大小是新的 capacity

  • 将旧数组的元素放置到新数组中


final Node<K,V>[] resize() {    // 将字段引用copy到局部变量表,这样在之后的使用时可以减少getField指令的调用    Node<K,V>[] oldTab = table;    // oldCap为原数组的大小或当空时为0    int oldCap = (oldTab == null) ? 0 : oldTab.length;    int oldThr = threshold;    int newCap, newThr = 0;    if (oldCap > 0) {        if (oldCap >= MAXIMUM_CAPACITY) {            // 如果超过最大容量1>>30,无法再扩充table,只能改变阈值            threshold = Integer.MAX_VALUE;            return oldTab;        }        // 新的数组的大小是旧数组的两倍        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                    oldCap >= DEFAULT_INITIAL_CAPACITY)            // 当旧的的数组大小大于等于默认大小时,threshold也扩大一倍            newThr = oldThr << 1;    }    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);    }    threshold = newThr;    @SuppressWarnings({"rawtypes","unchecked"})    // 创建容量为newCap的newTab,并将oldTab中的Node迁移过来,这里需要考虑链表和tree两种情况。    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];    table = newTab;    // 将原数组中的数组复制到新数组中    if (oldTab != null) {        for (int j = 0; j < oldCap; ++j) {            Node<K,V> e;            if ((e = oldTab[j]) != null) {                oldTab[j] = null;                if (e.next == null)                    // 如果e是该bucket唯一的一个元素,则直接赋值到新数组中                    newTab[e.hash & (newCap - 1)] = e;                else if (e instanceof TreeNode)                    // split方法会将树分割为lower 和upper tree两个树,如果子树的节点数小于了UNTREEIFY_THRESHOLD阈值,则将树untreeify,将节点都存放在newTab中。                    // TreeNode的情况则使用TreeNode中的split方法将这个树分成两个小树                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                else { // preserve order 保持顺序                    // 否则则创建两个链表用来存放要放的数据,hash值&oldCap为0的(即oldCap的1的位置的和hash值的同样的位置都是1,同样是基于capacity是2的次方这一前提)为low链表,反之为high链表, 通过这种方式将旧的数据分到两个链表中再放到各自对应余数的位置                    Node<K,V> loHead = null, loTail = null;                    Node<K,V> hiHead = null, hiTail = null;                    Node<K,V> next;                    do {                        next = e.next;                        // 按照e.hash值区分放在loTail后还是hiTail后                        if ((e.hash & oldCap) == 0) {                            // 运算结果为0的元素,用lo记录并连接成新的链表                            if (loTail == null)                                loHead = e;                            else                                loTail.next = e;                            loTail = e;                        }                        else {                            // 运算结果不为0的数据,用li记录                            if (hiTail == null)                                hiHead = e;                            else                                hiTail.next = e;                            hiTail = e;                        }                    } while ((e = next) != null);                    // 处理完之后放到新数组中                    if (loTail != null) {                        loTail.next = null;                        // lo仍然放在“原处”,这个“原处”是根据新的hash值算出来的                        newTab[j] = loHead;                    }                    if (hiTail != null) {                        hiTail.next = null;                        // li放在j+oldCap位置                        newTab[j + oldCap] = hiHead;                    }                }            }        }    }    return newTab;}
复制代码


获取元素


get(key) 方法时获取 key 的 hash 值,计算 hash & (n-1) 得到在链表数组中的位置 first=tab[hash&(n-1)],先判断 first 的 key 是否与参数 key 相等,不等就遍历后面的链表找到相同的 key 值返回对应的 Value 值即可。


    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; }
复制代码


红黑树结构


static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {    TreeNode<K,V> parent;  // red-black tree links 父节点    TreeNode<K,V> left;    // 左子树    TreeNode<K,V> right;   // 右子树    TreeNode<K,V> prev;    // needed to unlink next upon deletion    boolean red;           // 颜色属性    TreeNode(int hash, K key, V val, Node<K,V> next) {        super(hash, key, val, next);    }}
复制代码


树形化操作



根据哈希表中元素个数确定是扩容还是树形化,如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容// MIN_TREEIFY_CAPACITY 的值为 64,若当前 table 的 length 不够,则 resize() // 将桶内所有的 链表节点 替换成 红黑树节点


final void treeifyBin(Node<K,V>[] tab, int hash) {    int n, index; Node<K,V> e;    // 如果当前哈希表为空,或者哈希表中元素的个数小于树形化阈值(默认为 64),就去新建(扩容)    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)        resize();    // 如果哈希表中的元素个数超过了树形化阈值,则进行树形化    // e 是哈希表中指定位置桶里的链表节点,从第一个开始    else if ((e = tab[index = (n - 1) & hash]) != null) {        // 红黑树的头、尾节点        TreeNode<K,V> hd = null, tl = null;        do {            // 新建一个树形节点,内容和当前链表节点 e 一致            TreeNode<K,V> p = replacementTreeNode(e, null);            // 确定树头节点            if (tl == null)                hd = p;            else {                p.prev = tl;                tl.next = p;            }            tl = p;        } while ((e = e.next) != null);        // 让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了        if ((tab[index] = hd) != null)            hd.treeify(tab);    }}TreeNode<K,V> replacementTreeNode(Node<K,V> p,                   Node<K,V> next){    return new TreeNode<>(p.hash, p.key, p.value, next);}
复制代码


删除元素


下面再来看看删除方法 remove。


    public V remove(Object key) {        //临时变量        Node<K,V> e;        /**调用removeNode(hash(key), key, null, false, true)进行删除,第三个value为null,表示,把key的节点直接都删除了,不需要用到值,如果设为值,则还需要去进行查找操作**/        return (e = removeNode(hash(key), key, null, false, true)) == null ?            null : e.value;    }        /**第一参数为哈希值,第二个为key,第三个value,第四个为是为true的话,则表示删除它key对应的value,不删除key,第四个如果为false,则表示删除后,不移动节点**/    final Node<K,V> removeNode(int hash, Object key, Object value,                               boolean matchValue, boolean movable) {        //tab 哈希数组,p 数组下标的节点,n 长度,index 当前数组下标        Node<K,V>[] tab; Node<K,V> p; int n, index;        //哈希数组不为null,且长度大于0,然后获得到要删除key的节点所在是数组下标位置        if ((tab = table) != null && (n = tab.length) > 0 &&            (p = tab[index = (n - 1) & hash]) != null) {            //nodee 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value            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不再是数组下标的节点,而是要删除结点的上一个结点**/                        p = e;                    } while ((e = e.next) != null);                }            }            //找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true            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;                /**此方法在hashMap中是为了让子类去实现,主要是对删除结点后的链表关系进行处理**/                afterNodeRemoval(node);                //返回删除的节点                return node;            }        }        //返回null则表示没有该节点,删除失败        return null;    }
复制代码


删除还有 clear 方法,把所有的数组下标元素都置位 null。


size()方法


HashMap 的大小很简单,不是实时计算的,而是每次新增加 Entry 的时候,size 就递增。删除的时候就递减。空间换时间的做法。因为它不是线程安全的。完全可以这么做,效率高

用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论 (2 条评论)

发布
用户头像
对于链表转为树的条件是为 链表长度大于8且整个hash桶的长度大于64的时候会进行相关的转为真正转为树节点实现的结构
2021 年 05 月 28 日 22:13
回复
用户头像
如果有问题,请及时通知我,谢谢
2021 年 05 月 28 日 15:33
回复
没有更多了
🔎【Java源码探索】深入浅出的分析HashMap(JDK8)