为什么会出现哈希冲突

哈希冲突是指不同内容的对象调佣hashCode方法获取的哈希值相同,常见的原因之一就是,哈希值是一个int值,当计算过程中数值超过了int的最大值就会发生数据溢出,导致不同内容的对象,可能产生相同的哈希值。另外,就HashMap而言,由于为了获取桶索引的而进行的取模运算,导致即使不同的哈希值也可能得到相同的桶索引,也算是一种哈希冲突。


为什么HashMap的数组长度为2的n次幂、为什么(n - 1) & hash总是能落在数组范围内

首先思考问题,如何经过使用int值hash经过计算得到一个数组范围内的数字,答案是取模运算即hash % n , 又经过公式转换 当数组长度n为2的n次幂(两处的n并无关系)时 (n - 1) & hash 的结果与 hash % n 完全相等,但在计算机中 取模运算% 的效率不如 按位与 &,所以就用了(n - 1) & hash,且(n - 1) & hash的结果总是在数组范围内,并且这也解释了为什么HashMap的数组长度为2的n次幂。


hashCode和equals的关系

根据以上分析可以得出结论,哈希值相同的对象,equals不一定相同,但equals相同的对象则哈希值一定相同。

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;
    if ((tab = table) == null || (n = tab.length) == 0)
        // 当map还没有初始化时,初始化数组table容量为16,设置负载因子loadFactor为0.75 设置阈值threshold为12(16 * 0.75)
        n = (tab = resize()).length;
    // 根据当前key的哈希值和table数组容量,计算出数组索引
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 若该索引位置未被占用,就说明当前key不重复(因为相同的key的哈希值一定相同,相同的哈希值计算出的索引一定相同),则直接新增node节点加入到数组中
        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))))
            // key重复,则将当前索引位置节点p赋值给节点e
            e = p;
        // 若当前节点key不相同,判断是否为树节点
        else if (p instanceof TreeNode)
            // 若该节点为树节点,则调用putTreeVal方法,该方法查找是否存在当前key的节点,若存在则返回,若不存在则新增节点插入到树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 循环遍历链表节点,结束条件是:没找到重复key,新增节点、找到重复key,修改节点
            for (int binCount = 0; ; ++binCount) {
                // 将下一个节点赋值给e ,并判断是否null
                if ((e = p.next) == null) {
                    // 为null则说明这是最后一个节点,则说明链表内不存在重复key,则创建新的节点并插入链表
                    p.next = newNode(hash, key, value, null);
                    // 判断当前链表内节点数量是否大于等于8(因为从0开始遍历,所以需要+1)
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        // 若数量大于等于8,则将链表转为红黑树
                        treeifyBin(tab, hash);
                    // 新增数据成功,循环结束
                    break;
                }
                // 判断当前节点key与当前key是否相同
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相同则结束循环,当前节点已经赋值给e,后续代码会更改新的value
                    break;
                // 若当前节点不是最后一个,key也不相同,则将当前节点赋值给p,以便继续循环能拿到下一个节点
                p = e;
            }
        }
        // 以上代码,在找到重复key时,会给当前节点赋值给e
        if (e != null) { 
            // 保存当前节点value为旧的value以便后续返回
            V oldValue = e.value;
            // 因为put方法调用putVal方法时onlyIfAbsent=false,所以一定能进来
            if (!onlyIfAbsent || oldValue == null)
                // 更改当前节点value为新的value
                e.value = value;
            // 空方法
            afterNodeAccess(e);
            // 返回旧的value
            return oldValue;
        }
    }
    // 修改次数+1(根据观察,新增元素才会+1,修改不会)
    ++modCount;
    // map元素数量+1,并判断是否超过阈值
    if (++size > threshold)
        // 若map元素size超过了阈值threshold则扩容数组table为原来容量二倍,阈值变为原来二倍,并重新计算索引、分配数据
        resize();
    // 空方法
    afterNodeInsertion(evict);
    // 新增数据返回null
    return null;
}

putVal方法流程总结

默认容量16,阈值12,负载因子0.75

1.根据key的哈希值和容量计算得到数组索引。

2.若索引位置未被占用,就说明当前key不重复,新增节点加入数组。

3.若索引位置已被占用,则比较头结点,若相同则准备重写value。

4.若头结点不相同,则遍历链表或红黑树,找到相同key节点则准备重写value,若无相同key节点则新增链表节点或树节点,并加入链表或树

4.1新增链表节点时,判断当前链表内节点数量是否大于等于8,大于等于8则将链表转为红黑树。

5.若有相同key节点则将节点value更新,并返回旧的value,方法结束。

6.若新增节点,则修改次数modCount+1,并判断size是否大于阈值threshold,大于边扩容,扩容为当前容量二倍,阈值也是二倍,返回null方法结束。