JDK源码解析之HashMap类

本文深入探讨了HashMap的工作原理,包括其内部结构、冲突解决策略及在JDK 8中的改进。详细介绍了HashMap如何通过拉链法处理冲突,并在冲突数量达到阈值时转为红黑树以提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

以下解析基于JDK8.0

HashMap的继承关系如下所示:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
Map是一个顶级接口,跟Collection接口并没有什么关系。HashMap的默认容量是16(1<<4),默认负载因子是0.75,扩容机制是左移一位,也就是乘以2,容量大小必须保证是2的整数次方。采用一个内部类Node来存储元素,该内部类有四个成员变量如下所示:

final int hash;    //存储key的hash值,该值并不是key的hashCode,不过跟key的hashCode有关。
final K key;    //存储key
V value;    //存储value
Node<K,V> next;  //指向下一个Node节点
从上面我们可以看出来,HashMap解决冲突的办法是“拉链法”。

由于拉链法在冲突较多的情况下,效率低下的缺点(因为查找链表中的元素时可能要遍历整个链表的长度),在JDK8.0中HashMap有一个重大改进就是当冲突的元素超过某一阈值时会转换成用红黑树来存储冲突的元素,这样的话查找元素等操作的时间复杂度会降低,因此在HashMap中还存在一种树节点,用内部类TreeNode表示,它继承自上面所说的Node节点(间接继承)。默认情况下,当一条链中的冲突元素超过8的时候将由链表转化成红黑树进行存储。

Node节点中的hash值并不是key的hashCode值,而是对key的hashCode值进行了一层包装,如下所示:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
将key的hashCode与hashCode左移16位之后进行异或操作,因为在根据key定位元素位置的时候,采用的办法是将key的hash值与n-1进行相与,n代表当前hash表的长度,该长度是2的整数次方,假设当前长度是2^4(即初始长度16),则(n-1) & hash即0x000F&hash,hash前面的很多位相与之后全部是0,真正起作用的是hash的最后4位,也就是说,hash值的高位不管怎么变对(n-1) & hash的结果没有什么影响,不管高位怎么变,只要最后四位是一样的就会产生冲突,这样的话对性能会产生影响,因此不直接用key的hashCode的值,而是再通过一个hash()函数,让高位也能参与进来。我们来看一下获取元素的过程:

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    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) {    //定位元素的时候正如上面所说是用(n-1) & hash;而该hash值是通过hash(key)得到的
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))  //首先比较Node节点中存储的hash值是否与hash(key)得到的hash值相同,相同再判断是否equals
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)  //如果是树节点,则调用getTreeNode方法在红黑树中进行操作
                    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元素的过程,调用put(key, value)的时候,首先会用hash(key)得到key的hash值,之后会通过(n-1) & hash定位到hash表中的某个位置,如果该位置为空,则将该元素放在此位置,如果该处不为空,则判断要插入的元素是否与该位置的元素hash值相等,并且key也equals,如果是则将新的value覆盖旧的value,如果不equals,则顺着该链表或者是顺着红黑树进行查找,如果找到key一致的元素(key一致的含义是hash相等,并且两个key进行equals返回true),则将新的value覆盖旧的value,如果没有找到一致的key,则在链表的最后或者红黑树的最后插入该元素。

水平有限,如有不当之处,还望指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值