从源码分析jdk1.8中HashMap的put过程

本文详细解读了HashMap中的核心变量,如默认容量、最大容量、负载因子等,以及put方法的工作流程,包括计算哈希值、数组操作、链表与红黑树的转换机制。
摘要由CSDN通过智能技术生成

hashMap中定义了一系列变量,我们先来解读一下hashMap中定义的几个变量的含义

// 这是hashMap的默认容量,必须为2的幂,至于为什么是2的幂,后面我们会提,默认是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 这是容量的最大值,容量不能超过这个值
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认加载因子,表示hash允许的饱和程度
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表树化阈值,我们都知道hashmap中链表过长会向红黑树转化,这个值表示当链表长度超过时就会向红黑树转化
static final int TREEIFY_THRESHOLD = 8;

// 链表过长会向红黑树转化,同理,树的长度过短也会向链表转化,这个值就表示当红黑树长度向链表转化的最小长度
static final int UNTREEIFY_THRESHOLD = 6;

// 最小树化数组长度,看过一部分源码的都会觉得链表长度大于8时就会向红黑树转化,实则不然,有一个前提条件,就是数组长度要大于一个阈值
// 这个值就表示链表向红黑树转化时数组的最小长度
static final int MIN_TREEIFY_CAPACITY = 64;
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

点击put方法进来,我们看到put方法内部调用了putVal方法,我们接下来分析putVal方法

首先计算hash,这个hash的计算很巧妙,从源码来看是将key的hashCode值与hashCode值无符号右移16位进行异或运算得到,至于为什么要这样计算hash值,目的是为了减少hash冲突,至于为什么能减少hash冲突,我们等会解释

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

我们知道1.8中hashMap是数组+链表+红黑树的结构,这里table变量便是这个数组,首先判断数组长度,如果数组为空或者长度为0,调用resize方法进行数组初始化,默认数组长度为16;

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

如果数组初始化了,计算key的下标位置,这里通过上述得到的hash值与数组长度减1进行&运算来得到下标位置,为什么这样能够得到下标位置?还有就是为什么能保证计算出来的位置不会超出数组的长度?

请看,假设数组长度为16

hash: 0101 1010
n-1   : 0000 1111
i        : 0000 1010

由于数组长度前面我们提到一定是2的幂,所以n-1的二进制表示一定是 0000 1…1的形式,由于n-1的前几位是0,又因为是&运算,所以最终运算结果,n-1为0的部分一定是0,不为0的部分则是n-1不为0的部分对应hash的部分。这个范围刚好是0~n-1,也就是长度为n的数组的索引范围,所以为什么这样计算得出的下标不会超出数组索引,如果数组长度不为2的幂这里得出的索引就没有这个规律,因而限定数组长度为2的幂;另外我们注意到最终结果的得出只与hash的后面部分有关,这样就会有一个问题,这样不会大大提高hash冲突的概率吗?这也是为什么hash值的得出要使用hashcode与hashcode右移16位运算来得出,hashcode是32位,右移16位刚好是hashcode的高位和低位都参与运算来得出的hash值,这样在后面计算索引位置时尽管仍然是hash的后面部分参与计算,但是hash是hashcode高位与低位运算所得出,因而能降低hash冲突的概率。

计算出元素的位置后,判断该位置是否存在元素,如果该位置没有元素,就直接插入元素

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

如果该位置有元素,比较原有的key和新插入的是否是同一个key

if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;

如果不是同一个key,判断当前元素是不是树节点

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

遍历链表,如果链表中不存在要插入的key,则将元素插入到链表末尾(尾插法,jdk1.7是头插法),插入到链表后,会进行链表长度判断,如果链表长度大于树化阈值(TREEIFY_THRESHOLD),调用treeifyBin方法,treeifyBin方法内部会进行判断,只有数组长度超过最小树化数组长度(MIN_TREEIFY_CAPACITY)才会进行链表向红黑树的转化,所以并不是链表长度大于树化阈值(TREEIFY_THRESHOLD)链表就会转化为红黑树;而如果数组长度没有超过阈值只会进行hashMap的扩容。

if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();

这里在判断要插入的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;
}

如果要插入的key不存在,程序就会走到这里,这里已经插入了新的key,会判断是否需要扩容,判断当前元素是否超过扩容的阈值,扩容的阈值由数组容量乘以加载因子得出(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY),当元素个数超过这个值就会进行hash的扩容。

++modCount;
if (++size > threshold)
    resize();
// 这个方法由HashMap子类LinkedHashMap实现,探讨hashMap不用关注
afterNodeInsertion(evict);
return null;

至此,关于hashMap的put过程依据源码已经分析完毕,如果理解错误,欢迎指正

  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值