JDK8中HashMap源码分析

本文探讨了HashMap在JDK8中的实现变化,包括从链表到红黑树的转换条件,以及put操作的具体流程。JDK8通过引入红黑树解决了链表长度过长导致的性能问题。

JDK7及以前,HashMap底层基于数组(Entry数组)和链表实现,计算hash时发生hash冲突将该节点以链表形式存储,当hash冲突过多会导致单个链表过长,查找节点时将耗费O(n)的查找时间

JDK8:底层基于数组(Node数组)、链表、红黑树实现,当同一hash值的节点数超过阈值(8),链表结构将会被转换为红黑树进行数据存储

底层以Node数组存储,对应TreeNode红黑树的实现

transient Node<K,V>[] table;

当hash冲突节点大于指定阈值8时,将链表结构转换为红黑树进行存储

//由链表转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;

//由红黑树转换为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;

//哈希表的最小树形化阈值
static final int MIN_TREEIFY_CAPACITY = 64; 

执行put操作源码分析

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;
//当前map无数据,则执行resize()方法,并返回n
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
//当前需要存储键值对的位置没有元素,则将键值对封装为Node存放在该位置即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    //当前位置已有元素占有
        Node<K,V> e; K k;
    //判断该位置元素的key与要插入元素的key是否一致,一致就直接替换
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
    //如果是树形结构,则执行putTreeVal方法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
       //遍历链表数据,添加新元素
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
           //判断是否大于等于需要转换树形结构的阈值,大于则执行treeifyBin方法将链表转换为红黑树结构
                    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;
//当size大于阈值,就执行resize方法扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

转换为树形结构的treeifyBin方法分析

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
//如果当前哈希表为空或哈希表元素个数小于树形化的最小阈值 就去新建或扩容
    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 {
        //新建树形节点,将链表节点元素信息添加到树形节点
            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);
    }
}

将二叉树转换为红黑树treeify方法分析

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        //第一次进入循环,设置头结点为黑色
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            //遍历所有节点与当前节点x进行比较,调整位置
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                //当前比较节点哈希值比x大 dir = -1
                if ((ph = p.hash) > h)
                    dir = -1;
                //当前比较节点哈希值比x小 dir = 1
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    //将当前节点设置为x的父节点
                    x.parent = xp;
                    //将当前比较节点的哈希值与x比较 比x大则x为左节点,否则x为右节点
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}
<think>嗯,用户让我介绍一下JDK8HashMap源码。首先,我得先回想一下HashMap的基本结构。在JDK8之前HashMap是数组加链表的结构,当哈希冲突较多时,链表会变得很长,查询效率会下降。而JDK8之后,当链表长度超过8时,会转为红黑树,这样查询的时间复杂度就从O(n)变成O(log n)了。这个优化应该是个重点。 接下来,我需要考虑HashMap的几个核心部分:数据结构、哈希计算、put方法、扩容机制、红黑树转换条件等。用户可能对这些部分感兴趣,特别是和之前版本不同的地方,比如红黑树的引入。 然后,数据结构方面,HashMap内部有一个Node数组,每个Node可能是一个链表节点或者树节点。需要解释一下Node类的结构,包括hash、key、value、next这些属性。当链表转成树时,会用TreeNode,这可能继承自LinkedHashMapEntry,但具体的继承关系可能需要查证一下。 哈希计算方面,JDK8的hash方法做了优化,将高位的信息也参与到索引的计算中,减少哈希冲突。具体来说,hashCode()的高16位和低16位进行异或,这样在数组长度较小时,高位的变化也能影响最终的索引位置,减少碰撞。 put方法的流程是关键。当插入一个键值对时,首先计算哈希值,找到数组下标。如果该位置为空,直接插入;如果是链表或树节点,则遍历查找是否存在相同的key,存在则替换,否则添加到链表末尾或树中。当链表长度达到8且数组长度大于等于64时,链表红黑树,否则进行扩容。这个阈值需要注意,可能用户会问到为什么是8或者64。 扩容机制,也就是resize方法。当元素数量超过容量乘以负载因子时,数组会扩容为原来的两倍。扩容后,原来的节点会重新计算位置,要么在原位置,要么原位置+旧容量。这里可能涉及到高位判断,通过(e.hash & oldCap)是否为0来决定位置。同时,在JDK8中,链表在扩容时会被拆分成两个链表,保持顺序,避免死循环的问题,而JDK7之前可能因为头插法导致死循环。 线程安全方面,HashMap不是线程安全的,多线程环境下可能出现问题,比如数据覆盖、死循环(在JDK7中)等。需要提到ConcurrentHashMap或者使用Collections.synchronizedMap来保证线程安全。 另外,初始容量和负载因子的默认值,初始容量是16,负载因子0.75,这些参数的作用和调整的影响也需要解释一下。比如负载因子高可以减少空间开销,但增加查找成本;负载因子低则相反。 红黑树转换的条件需要明确,链表长度超过8且数组长度至少64才会转换,否则只是扩容。这是为了避免在哈希表还很小的时候,不必要的树化,因为扩容可能更有效。 可能用户还想知道源码中的一些具体实现细节,比如TreeNode的结构,红黑树的插入、删除操作是如何实现的,但可能这部分太深入,需要根据用户的背景决定是否展开。 在解释的时候,需要注意避免过于技术化的术语,用比较易懂的语言,同时结合代码结构,比如putVal方法、resize方法的关键步骤。可能还需要提到modCount用于快速失败机制,迭代器在检测到结构变化时抛出异常。 用户可能的疑问点包括:为什么选择红黑树而不是其他数据结构?为什么阈值是8?扩容为什么是两倍?这些可能需要准备答案,比如红黑树在查询和插入之间取得平衡,阈值8是基于泊松分布统计,两倍扩容是为了保持容量为2的幂,方便位运算计算索引。 总结下来,应该按照数据结构、哈希计算、put流程、扩容机制、树化条件、线程安全等方面来组织回答,重点突出JDK8的改进,比如红黑树的引入和扩容优化。</think>在 JDK8 中,`HashMap` 是一个基于哈希表实现的键值对容器,其源码设计在性能和数据结构上进行了优化(如引入红黑树)。以下是对其核心实现的介绍: --- ### **1. 数据结构** JDK8 的 `HashMap` 使用 **数组 + 链表 + 红黑树** 的复合结构: - **数组(`Node<K,V>[] table`)**:存储链表的头节点或红黑树的根节点。 - **链表Node)**:解决哈希冲突的基础结构,每个节点存储键值对和下一个节点的引用。 - **红黑树(TreeNode)**:当链表长度超过阈值(默认 8)且数组长度 ≥ 64 时,链表转换为红黑树,提升查询效率。 ```java static class Node<K,V> implements Map.Entry<K,V> { final int hash; // 哈希值 final K key; V value; Node<K,V> next; // 链表的下一个节点 } ``` -- ### **2. 哈希计算** 通过 `hash()` 方法将键的哈希码分散到数组中,减少碰撞: ```java static final int hash(Object key) { int h; // 高16位与低16位异或,增加低位的随机性 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` -- ### **3. `put()` 方法流程** 1. **计算哈希值**:根据 `key` 计算哈希值,确定数组下标 `(n-1) & hash`。 2. **处理空桶**:若目标位置为空,直接插入新节点。 3. **处理冲突**: - **链表**:遍历链表,若 `key` 已存在则更新值,否则添加到链表尾部。 - **红黑树**:调用树的插入方法。 4. **树化检查**:链表长度 ≥ 8 时尝试树化(需数组长度 ≥ 64,否则优先扩容)。 5. **扩容检查**:元素总数超过 `容量 * 负载因子`(默认 0.75)时触发扩容。 --- ### **4. 扩容机制(`resize()`)** - **新容量**:原容量的 2 倍(保持为 2 的幂)。 - **数据迁移**: - 链表节点根据 `(e.hash & oldCap)` 是否为 0 分配到原位置或新位置。 - 红黑树节点会拆分为两个链表,若长度 ≤ 6 则退化为链表。 -- ### **5. 红黑树转换条件** - **链表红黑树**:链表长度 ≥ 8 **且** 数组长度 ≥ 64。 - **红黑树链表**:树节点数 ≤ 6 时退化为链表。 --- ### **6. 关键参数** - **默认初始容量**:16。 - **负载因子(`loadFactor`)**:默认 0.75(平衡空间与时间效率)。 - **扩容阈值**:`容量 * 负载因子`。 - **树化阈值**:链表长度 ≥ 8,数组长度 ≥ 64。 --- ### **7. 线程安全性** `HashMap` **非线程安全**,多线程操作可能导致数据不一致。替代方案: - 使用 `ConcurrentHashMap`。 - 通过 `Collections.synchronizedMap()` 包装。 -- ### **8. 性能优化点** - **红黑树**:避免长链表的低效查询(时间复杂度从 O(n) 优化到 O(log n))。 - **高位异或哈希**:减少哈希碰撞。 - **扩容优化**:避免 JDK7 及之前版本的头插法导致的死循环问题。 -- ### **源码分析示例(简化的 `put()` 流程)** ```java 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 { // 处理哈希冲突... } // 检查扩容... } ``` --- 通过以上设计,JDK8 的 `HashMap` 在高并发和高负载场景下显著提升了性能,同时保持了合理的空间利用率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值