飞道的博客

HashMap底层实现原理

352人阅读  评论(0)

1.底层结构:

jdk1.7:数组+链表
jdk1.8:数组+链表+红黑树

数组的查询速度快,链表的修改速度快

2.默认值

默认初始容量为16(1<<4),必须是2的幂

如果隐式指定了更高的值,则使用的最大容量,但必须是2的幂并且最大不大于2的30次方

默认负载因子为0.75,负载因子=数据实际大小 / 分配的内存空间。例如数据占用空间为70,分配的内存空间为100,负载因子就是0.7

生成红黑树的阈值

红黑树转链表阈值,为了防止频繁的在临界值增加和删除所以2个阈值不一样

3.如何存储和获取值

jdk1.7

put
我们知道,如果我们想往数组中存入数据,我们首先得有一个数组下标,而我们在进行PUT的时候并不需要再传一个参数来作为数组的下标,那么HASHMAP中下标是怎么获取来的呢?答案为哈希算法,这也是为什么叫HashMap而不叫其他Map。

对于哈希算法,了解过的人应该都知道它需要一个哈希函数,这个函数接收一个参数返回一个HashCode,哈希函数的特点是对于相同的参数那么返回的HashCode肯定是相同的,对于不相同的参数,函数会尽可能的去返回不相同的HashCode,所以换一个角度理解,对于哈希函数,给不相同的参数可能会返回相同的HashCode,这个就叫哈希冲突或哈希碰撞。

但是通过直接hash算法得到的是很大的一个数组,显然不适合作为数组下标
于是解决方案:先将key进行hashCode,然后进行位运算

这个方法中h代表hashcode,length代表数组长度。我们发现它是用的逻辑与操作
对于数组长度为16的集合

h:  0101 0101
15: 0000 1111
  &
    0000 0101

15的高四位都是0,低四位都是1,而与操作的逻辑是两个运算位都为1结果才为1,所以对于上面这个运算结果的高四位肯定都是0,而低四位和h的低四位是一样的,所以结果的取值范围就是h的低四位的一个取值范围:0000-1111,也就是0至15,所以这个结果是符合数组下标的取值范围的。
这样就可以得到数组下标,但是这也会产生一个新问题,就是下标可能会相同,这就是哈希冲突或哈希碰撞,在HashMap中采用链表解决了这个问题,详解请看下一个标题
get
或取值时将key进行hash,然后再运算出下标,通过下标找到链表,然后再遍历链表判断key是否和查询的相同

jdk1.8

put
获取下标的方式同1.7一样

/*K代表键的数据类型,V代表值是数据类型,调用put方法需要传key和value
*调用putVal方法,将key进行一次hash传入第一个参数中
 */
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 { 
        Node<K,V> e; K k; //声明局部变量
        //p.hash是将p节点的key进行hash,并且判断p节点的key是否和要插入的key相同(保证map中的key不能重复)
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //hash值相同,但不一定是同一个key
            e = p; //如果有这个key,就p节点赋值给e,为了下面返回oldValue
        else if (p instanceof TreeNode) //判断p是不是树节点(红黑树)
            e = ((HashMap.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;
                }
                //判断key是否相等
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //此处判断插入数据时是否遇到了同一个key,没有就返回null,有就返回旧节点的value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //扩容,判断是否大于阈值
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

树化,并不是链表长度达到8就树化,具体原因看下面代码

 final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
        int n, index; HashMap.Node<K,V> e;
        //判断数组长度是否小于最小表容量,如果是则不会树化,会通过扩容来增加散列,减小链表长度
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            HashMap.TreeNode<K,V> hd = null, tl = null;
            do {
                HashMap.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);
        }
    }


这个值是可以修改的

4.哈希碰撞

不同的hashCode一定对应不同的对象,
但不同的对象的hashCode可能相同

在上一节中产生的哈希冲突在jdk1.7和jdk1.8中的解决方案稍有不同,1.7采用头插法,1.8采用的时尾插法,这个不同也是因为1.8采用了红黑树

首先来看jdk1.7中,如下图,当Entry4需要插入到Entry1这个位置时,只有2种方案,插入头部或尾部,但是插入到尾部需要遍历影响效率,所以采用头插法。但是该链表是单向链表,插入到头部的数据不能被访问到,所以只将数据插入到头部是行不通的,还需要将链表整体向下位移一位,这样就可以访问链表中的所有数据了

jdk1.8中,HashMap引入了红黑树,在插入数据时需要判断链表是否达到长度8,所以需要遍历链表,所以当遍历到最后一个节点时顺便把数据插入进去,这样就不需要像头插法那样移动链表了

5.扩容机制

1.7中:先扩容,再添加元素
判断集合大小是否大于阈值(阈值=负载因子*容量)并且要插入的元素所在节点是否有元素,有就扩容,为了防止链表太长导致查询效率低
扩容会生成一个新的数组,然后将旧数组上的数据转移到新数组上。当然这并不是简单的按下标赋值,需要重新计算下标,因为数组的长度不一样了,下图这个方法计算的下标旧可能发生变化。因为与运算时(length-1)变化了,这样可以使散列表分布更加分散。

1.8中:先添加元素再扩容
详情请看添加元素的源码注释


转载:https://blog.csdn.net/gtaaaa/article/details/105934300
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场