HashMap

1. 基础

1.1 理论

JDK1.8 之前 HashMap数组+链表组成的,每个数组元素存储一个链表的头结点。数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。 可以存储 nullkeyvalue

HashMap

1.2 计算哈希值

使用 key 计算。

哈希表内部数组的大小很重要,要保持一个平衡的数字,不能让哈希碰撞太频繁,也不能占用空间太大。

在哈希表使用的过程中,会不断的调整数组的容量。

  • 调整后的容量是多少?之前的2倍

  • 如何调整?再散列调整数组大小。数组长度小于64,会先扩充数组。

2. 扩容源码

每个节点的定义如下:

在这里插入图片描述

HashMap底层是一个数组,元素的类型是Node节点,源码如下:

在这里插入图片描述

在添加元素put(key, value)的时候,会调用putValue()进行实际的放入过程。

3

putValue()在加入一个节点之后,会判断是否超过容量,如果超了,会调用resize()进行2倍扩容。源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果当前表为null,或者表为空,则初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;    // 通过resize()初始化表,默认表长为10
    // 获取该hash值在表中的位置 i
    // 1. 如果该table[i]为null,说明该hash值没有存放其他值,放入
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 创建一个新节点存放在表中
        tab[i] = newNode(hash, key, value, null);
    // 2. 如果table[i]不为null,说明当前值与表中已有的值冲突了,那么加在该位置的链表最后
    else {
        Node<K,V> e;   // 表中的某个节点,和当前对象的key相等
        K k;
        // 当前的键 key 已存在
        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;
    // 当前容量 +1 后超过阈值,则扩容
    if (++size > threshold)
        resize();    // 2倍扩容
    afterNodeInsertion(evict);
    return null;
}

resize()扩容

在这里插入图片描述

如果进行2倍扩容,也需要将表中已有的值,重新散列到新表中,重新散列的过程是通过位运算计算新键值,而不是重新计算一遍散列函数。resize()中相关代码如下:

在这里插入图片描述

链表转红黑树的方法treeifyBin()

在这里插入图片描述

小纸条:(n-1) & hash

n 是2的幂次数,其二进制表示是 10...0,那么 n-1的二进制表示是01...1

(n-1) & hash 是一个与运算,位与位进行与运算。其结果就是将任意一个值对n的余数,相当于 hash % n。位运算的效率更高。

3. 缺点

hashmap 高度依赖于 hash 算法,如果 key 是自定义类,需要自己重写 hashcode() 方法,写 hash 算法。

4. 并发产生的问题

在调整大小的过程中,有一步是把老数组中的全部元素转移到新数组中。这个过程在并发环境中会发生错误,导致数组链表中的链表形成循环链表。1.8之前是头插法,会导致循环链表的产生。1.8以后是尾插法,不会导致循环链表的产生。

// 在1.8中没有看到这个函数
void transfer( Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++){
        Entry<K,V> e = src[j];
        if (e != null){
            src[j] = null;
            do {
                //假设第一个线程执行到这里因为某种原因挂起
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash,newCapacity);e.next = newTable[i];
                newTable[i] = e;
                e = next;
                }while (e != null);
        }
    }
}

java 8 会出现什么问题?

java 8不会出现再散列时形成的循环链表,会造成数据覆盖。

在插入数据时会判断是否出现哈希碰撞,判断完之后正常插入。这里会出现一个线程的数据覆盖另一个线程的数据。

HashMap 在于并发下的 Rehash 再散列会造成元素的覆盖问题,所以不能在多线程下使用。

在这里插入图片描述
(图片来源于网络)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值