28

逐行解读HashMap源码

 3 years ago
source link: http://www.cnblogs.com/ystblog/p/13043141.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】

一、写在前面

相信读者也看过了不少讲解 HashMap 源码的文章了,笔者认为,一切脱离源码去讲原理的都是泛泛而谈。一些所谓的原理大都是阅读源码之后的个人概括,这些概括参差不齐,再加上没有阅读源码,读者们是很难有切身体会的。正因如此,笔者逐行分析了 HashMap 的源码后,写下了本篇文章。

笔者在阅读 HashMap 源码的时候,曾对每个内部属性,每个内部方法和方法调用逻辑做了简要注释,但在整理成文的时候,还是遇到了略微的困难。对于一些内部属性的解释,需要结合它在一些方法的使用中发挥的作用来综合说明,笔者打算按照从浅到深的顺序,先带读者熟悉 HashMap 的宏观设计思想,再通读一遍源码,然后讲解源码的设计细节。要让读者边看文字边对照源码进行学习,形成自己的领悟和体会,避免造成笔者一人的泛泛而谈。

无论自己的领悟是深是浅,终归是自己的,无论别人的领悟多么高深,那也是别人的。希望每个读者都能有自己的收获和体会。

由于 HashMap 源码中涉及到较多的位运算的知识,对这方面基础薄弱的读者可以阅读我之前发布在 GitChat 的 文章

Java版本

$ java -version
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

二、HashMap官方说明

以下内容主要对HashMap的一些特性和注意事项做了简单的说明,笔者对一些比较重要的知识点做了加粗处理。

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作, 并允许使用 null 值和 null 键 。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它 不保证该顺序恒久不变

此实现假定哈希函数将元素正确分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代集合视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)的和成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。

HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。 容量 是哈希表中桶的数量 ,初始容量只是哈希表在创建时的容量。 加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度 。当哈希表中的条目数超出了 加载因子与当前容量的乘积 时,通过调用 rehash 方法将容量翻倍。

通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地降低 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。

注意,此实现 不是同步的 。如果多个线程同时访问此映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的不同步访问,如下所示:

Map m = Collections.synchronizedMap(new HashMap(...));

由所有此类的“集合视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间任意发生不确定行为的风险。

注意,迭代器的快速失败(fast-fail)行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常程序的方式是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

此类是 Java Collections Framework 的成员。

三、HashMap存储结构

学习源码之前,最好在脑海中明确 HashMap 的存储结构,不然后面阅读源码,遇到容量、数量等参数可能会有些许困惑。

相信你已经有所耳闻,HashMap 内部包含了一个 Node 类型的数组 table。Node 存储着键值对。

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

它包含了四个字段,从 next 字段我们可以看出 Node 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用链地址法来解决哈希冲突,即同一个链表中存放 哈希值和散列桶取模运算结果相同 的 Node。HashMap 存储结构如下图所示:

77FvMj3.jpg!web

四、HashMap静态属性

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认的 table 容量,1<<4 = 2^4 = 16,规定必须是2的幂。

static final int MAXIMUM_CAPACITY = 1 << 30;

最大的 table 容量,规定必须是 2 的幂,类型为 int,掐指一算,那只能是 0100 0000 0000 0000 0000 0000 0000 0000。因为 Java中 int 有 32 位,除第 1 位符号位外,数值部分只有 31 位,只有 1<<30 满足条件。

这里需要说明一下,对于 2 的幂,在计算机的世界里是一群特殊的存在,它们的二进制中只有一个1,其余位全为 0 。这个特性会影响与它进行位运算后的结果具有一些特殊效果。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

构造函数中未指定时使用的负载因子。

static final int TREEIFY_THRESHOLD = 8;

相信很多人知道链表长度大于 8 会转为红黑树,依据也就是这个字段的注释说明。但其实这只是条件之一,另一个条件就是下面这个参数。

static final int MIN_TREEIFY_CAPACITY = 64;

在进行链表转红黑树的时候,第一步是检查链表的长度是否大于等于 8,第二步会检查 table 数组的容量是否小于此数值,若小于,则取消转为红黑树,只对 table 数组进行扩容。

static final int UNTREEIFY_THRESHOLD = 6;

当键值对数量过多时需要对 table 数组进行扩容,并且将每个键值对放到新的桶中(或者不变)。原来的桶的内部结构有可能是链表,也有可能是红黑树,经过一番洗牌之后,如果桶结构为红黑树的键值对数量过低,就会重新转变为链表。低到哪个程度呢?就是这个字段对应的大小。

五、HashMap成员属性

transient Node<K,V>[] table;

HashMap 的内部 Node 类型的数组,属性名为 table。

transient int modCount;

该字段起标记作用,值是对该 HashMap 进行结构修改的次数,主要用于迭代器访问时检测 HashMap 是否因为删除等其它操作内部机构发生变化,从而支撑起所谓的快速失败行为。

transient Set<Map.Entry<K,V>> entrySet;

HashMap 内部有很多内部类,扩展了 HashMap 的一些功能,EntrySet 类就是其中一种,该类较为简单,无内部属性,你可以理解为一个工具类,对 HashMap 进行了简单的封装,提供了方便的遍历、删除等操作。

调用 HashMap 的 entrySet() 方法就可以返回 EntrySet 实例对象,为了不至于每次调用该方法都返回新的 EntrySet 对象,所以设置该属性,缓存 EntrySet 实例。

transient int size;

键值对的数量。

int threshold;

size 的临界值,当 size 大于 threshold 就必须进行扩容操作。

final float loadFactor;

负载因子,被 final 修饰,在构造方法中就被初始化,不指定就用默认的。

六、HashMap构造方法

HashMap 的共有三个构造方法,源码如下:

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

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

初看源码,可以发现,被 final 修饰的 loadFactor 一定会在构造方法中被初始化。

带参构造方法主要做的就是对 initialCapacity 和 loadFactor 的校验工作,并且会通过 tableSizeFor 方法计算其合理的初始容量,由于此时 table 数组尚未实例化,所以该合理的初始容量被暂存在 threshold 属性中,由其代为保管。

七、tableSizeFor(int cap)方法

tableSizeFor(int cap) 静态方法的作用是计算其合理的初始容量,也就是满足 2 的幂,且大于等于参数 cap,最接近的那一个数即可。该方法使用位运算设计了高效的算法逻辑,方法源码如下:

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

参数 cap 的值肯定大于 0,故 n 大于等于 0 ,假设 n = 0,经过右移之后,依旧为 0 ,0 与 0 异或依旧为 0 ,通过 return 语句的 n+ 1 计算得 1,即 2 的 0 次幂。

当 n 大于 0 时,n 的二进制位肯定会有位的值为 1,即 001xx..xx 的形式,接着,对 n 右移 1 位得 0001xx..xx,再进行位或,由于 1 与 0 或 1 异或结果都为 1,所以结果必为 0011xx..xx 的形式。以此类推,位移 2 位、4 位、8 位、 16 位,最终该算法能让最高位的 1 后面的所有位全变为 1 。

下面用一张图举例说明一下:

iaa63iM.jpg!web

该算法的最后再让结果值 n + 1,所得即为 2 的整数次幂。为了让结果使结果大于等于参数 cap ,在算法开始时,令 cap - 1。

八、hash(Object key)方法

该方法是 HashMap 的核心静态方法,用于计算 key 的 hash 值,该方法的源码如下:

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

方法逻辑很简单,key 为 null 返回 0 ,根据桶下标计算公式 (capacity - 1) & hash ,以及 0 与 0 或 1 位与结果均为 0,可知,null 键对应的桶下标为 0 。

当 key 不为 null 时,调用 key.hashCode() 并将 hashCode 的低 16 位与高 16 位异或。

这步操作是因为 table 数组使用 2 的幂作为容量,且计算桶下标是通过 (capacity - 1) & hash ,所以仅在当前容量上方的位中变化的 hashCode 将始终发生冲突。

举个例子,初始容量为 16,hash值按 32 位计算,

16 = 10000(二进制)
16 - 1 =  0000 0000 0000 0000 0000 0000 0000 1111
hash1 = 0000 0000 0000 0000 0000 0000 0001 1111
hash2 = 0000 0000 0000 0000 0000 0000 0011 1111

因为 0 与任何数位与 & 的结果都是 0 ,所以无论 hash 值是多少,计算后除后 4 位外的位必定都为 0,如果 hash 值只在前 32 - 4 = 28 位发生变化,计算后的结果都是相同的,都会放入同一个桶中,始终发生冲突。

因此,设计者们在速度,实用性和位扩展质量之间进行权衡后应用了一种变换,通过将 hashcode 的低 16 位与高 16 位异或向下传播较高位的影响。

由于许多常见的哈希值已经合理分布(比如 hash 值的前 16 位都为 0 的一些数,它们的低 16 位与 高 16 位异或后值不变,因此无法从扩展中受益),并且由于我们使用树来处理容器中的大量冲突,因此我们仅以最方便的方式对一些移位后的位进行异或,以减少系统损失,以及合并最高位的影响,否则由于 table 数组容量的限制,这些位将永远不会在索引计算中使用。

九、桶下标计算公式

计算桶下标时,需要先通过 HashMap 内部的 hash()方法计算其 hash 值, 然后将 hash 值对桶个数取模,即

hash % capacity

如果能保证 capacity 为 2 的幂,那么就可以将这个操作转换为高效的位运算,也就是 HashMap 源码中的桶下标计算公式:

(capacity - 1) & hash

以上便是 HashMap 内部数组的容量为 2 的幂的其中一个原因,另一个原因是在 resize() 扩容方法中可以更高效的重新计算桶下标。

十、put(K key, V value)方法

该方法将指定值与该映射中的指定键相关联。若是 Key 已存在,则覆盖并返回旧的 Value(可为 null);若是没有键值对映射,则返回 null。该方法源码如下:

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

可以看到,该方法实际是封装的 putVal() 方法。putVal() 方法有 5 个参数,依次为 key 的 hash 值,key 本身,value 本身,参数 onlyIfAbsent 表示是否在键已存在的情况放弃覆盖旧值,参数 evict 作为 afterNodeInsertion(evict) 方法的参数,并不为 HashMap 所用,而是交由 LinkedHashMap 处理。

该方法的源码如下:

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为空则创建
    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 {
        // 步骤③:发生hash碰撞,若键已存在就返回该Node,并用属性 e 引用,若键不存在就创建一个新的Node,并直接插入到桶中
        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);
                    // 链表长度大于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;
            }
        }
        // 步骤④:该键已经存在
        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;
}

十一、resize()方法

该方法的作用是初始化 table 数组,或增加 table 数组的大小。

如果 table 数组为 null,则根据字段 threshold 中保持的初始容量进行分配。否则扩容,因为我们使用的是 2 的幂,所以每个桶中的元素必须保持相同的索引,或者在新 table 中以 2 的幂偏移。

举个例子,例如容量从 16 扩展为 32 时,具体变化如下:

16-1  =  0000 0000 0000 0000 0000 0000 0000 1111
hash1 =  0000 0000 0000 0000 0000 0000 0000 1111
hash2 =  0000 0000 0000 0000 0000 0000 0001 1111
// 桶下标为
(16-1)&hash1 = 0000 0000 0000 0000 0000 0000 0000 1111
(16-1)&hash2 = 0000 0000 0000 0000 0000 0000 0000 1111

容量为 16 时,hash1 和 hash2 经过桶下标计算后结果相同,会进入同一个桶中。当容量扩展为 32 后,新的桶下标计算过程如下所示:

32-1  =  0000 0000 0000 0000 0000 0000 0001 1111
hash1 =  0000 0000 0000 0000 0000 0000 0000 1111
hash2 =  0000 0000 0000 0000 0000 0000 0001 1111
// 桶下标为
(32-1)&hash1 = 0000 0000 0000 0000 0000 0000 0000 1111
(32-1)&hash2 = 0000 0000 0000 0000 0000 0000 0001 1111

hash1 和 hash2 经过桶下标公式重新计算之后,hash1的结果不变,所以依旧在原来的桶里;而 hash2 的结果比原来多了 1 位,即 2^4 = 16,也就是偏移了原来的容量大小。如下图所示:

MVNVRbe.jpg!web

因此,在扩充 HashMap 的时候,不需要重新计算 hash,只需要检查二进制 hash 中与二进制桶下标中新增的有效位的位置相同的那个位(以下简称“新增位”)是 0 还是 1 即可,是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap”。

如何检查新增位是 0 还是 1 呢?HashMap 中使用 hash & oldCap 位与运算检查该新增位。oldCap 是 2 的幂,故二进制表示只有 1 位是 1,且该位正好与之对应。不得不说这个设计还是非常巧妙的,既省去了重新计算 hash 值的时间,而且,由于新增的 1 位是 0 还是 1 可以认为是随机的,因此在扩容的过程,均匀的把之前碰撞的节点分散到新旧桶中。

resize() 方法的源码如下:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 步骤①:根据 oldCap 判断是扩容还是初始化数组,若是扩容..
    if (oldCap > 0) {
    	// 超过最大容量就不再扩容,随它去碰撞
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 没超过最大值,就扩容为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 步骤②:若是初始化数组,若是字段 threshold 中已经保存初始容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 步骤③:若是初始化数组,若是字段 threshold 中没有保存初始容量,则使用默认容量
    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"})
    // 步骤⑤:实例化新的 table 数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 步骤⑥:将每个桶及内部节点都移到新的 table 数组中
        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;
                        // 新增位是 0 放原桶
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 新增位是 1 放新桶
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原桶放新 table 数组
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 新桶放新 table 数组
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

十二、get(Object key)方法

【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】

该方法返回指定键所映射到的值,如果不包含该键对应的映射关系,则返回 null。

返回值为 null 不一定表示不包含该键对应的映射关系,也可能表示该键对应的值为 null。 如果需要验证是否存在该键对应的映射关系可以调用 containsKey() 方法。

该方法的源码如下:

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

方法内部调用的 getNode() 方法,参数为 key 的 hash 值与 key 本身,方法的源码如下:

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) {
        // 步骤②:每次 get 都要先检查该桶头节点是否是要找的键值对,因为不存在哈希碰撞的可能性较大
        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;
}

十三、HashMap序列化与反序列化

《请前往微信公众号“代码艺术”阅读全文》

十四、treeifyBin(Node[] tab, int hash)方法

《请前往微信公众号“代码艺术”阅读全文》

十五、TreeNode.treeify(Node[] tab)方法

《请前往微信公众号“代码艺术”阅读全文》

十六、TreeNode.balanceInsertion(TreeNode root,TreeNode x)

《请前往微信公众号“代码艺术”阅读全文》

版权声明

【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】

QnuuamU.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK