HashMap的核心理解

我不想画图,直接盗图,啊哈哈!这个就是HashMap的结构了。

![哈希表.png-10.2kB][4]

记住:先了解数据结构和算法,先了解数据结构和算法,先了解数据结构和算法。我们日常用到的很多东西,都是继续数据结构和算法的,你以为和你很远?

1、HashMap如何散列的?

        hashMap是数组和单向链表的组合,把数据放在数组对应下边的链表中。

        下面是HashMap中出现的2个计算数组下边的算法。

        --》h & (length-1);

        --》(h = key.hashCode()) ^ (h >>> 16)

拿第一个为例子,三个例子如下。(如果连hashCode都不知道,先回去补一补这个)

78612533 & (10 - 1)

100 1010 1111 1000 1000 0011 0101
                                                   1001

78612 & (10 - 1)

1 0011 0011 0001 0100
                              1001

786123 & (10 - 1)

1011 1111 1110 1100 1011
                                  1001

看出来了吧?&操作,让结果不会超出10 - 1的范围,其实就是利用key的hashCode值最后的位数和1001 &操作,来分配数组的下边的,然后把数据放在下边对应的链表中。

总结:不管,数组长度如何变化,通过&操作,都可以将不同的hash归类。

*** 这里很重要,数组长度变化了,根据hashCode计算出来的数组下边也许也会变化,那边存储在数组下的链表就需要重组。这个很重要。这个跟扩容原理有关系。

2、如何扩容的?

看了源码,也对比了别人的学习笔记,我整理出通俗易懂的东西出来。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; // 备份下当前的数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // 记录下当前数据的长度
        int oldThr = threshold; // 这个是阈值 作用看看下面的oldCap >= MAXIMUM_CAPACITY就知道了,threshold的计算,看下tableSizeFor方法
        int newCap, newThr = 0; // 新的数组长度和阈值大小
        if (oldCap > 0) { // 这里不比说,判断当前数组
            if (oldCap >= MAXIMUM_CAPACITY) { // 如果数组长度,已经大于当前HashMap设置的最大容量时,1 << 30
                threshold = Integer.MAX_VALUE; // 再次扩容,就直接Integer的最大值给你了。
                return oldTab; // 将当前的数组返回,某些计算需要用到
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) // newCap = oldCap << 1 这里的计算,让新的数组长度比老的翻了一倍,
                newThr = oldThr << 1; // double threshold // 都有备注了,将阈值也翻倍
        }
        else if (oldThr > 0) // initial capacity was placed in threshold // 走到这里,是在当前数组的长度==0的时候,
            newCap = oldThr; // 把当前的阈值长度给到新数组的长度,
        else {               // zero initial threshold signifies using defaults // 如果当前的 数组长度和阈值都为0
            newCap = DEFAULT_INITIAL_CAPACITY; // 默认值给他
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 阈值的大小,就是 最小长度*荷载系数,默认的荷载系数是0.75f,
        }
        if (newThr == 0) { // 若到了这里,新的阈值还是为0?
            float ft = (float)newCap * loadFactor; // 那先算出新的阈值,
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? // 如果计算出来的阈值大于了MAXIMUM_CAPACITY,那就用Integer的最大值。
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr; // 好了,先把算出来新的阈值给到全局阈值
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 搞一个新的数组,这里的数组长度,不出意外,就是之前的2倍了。
        table = newTab; // 当前的数组已经在上面备份起来了,这里先把新的数组给到全局,我这就开始往里头加东西了。
        if (oldTab != null) { // 当前备份起来的数组,不为null时
            for (int j = 0; j < oldCap; ++j) { // 开始循环当前的数据
                Node<K,V> e; // 建立个新节点,等下要用
                // 如果数组的第一个节点不为空,那就赋值给上面创建的节点e。记住,这里的节点后面的数据是个单向链表,
                //
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null; // 那当前数组的j下边置空
                    if (e.next == null) // 如果这个节点就只有一个自己,没有next
                        // 这里直接通过e.hash计算出在新数组中的位置,然后把e丢过去。也许你会想,为什么有这么个判断呢?
                        // 回到上面HashMap是如何离散的,你就明白了。当数组长度变化了,key的hash值&数组长度得出来的新数组的下标也变化了。
                        // 这个判断拿出来玩,只是简述了只有一个的特殊情况,重点还在这个判断的最后面的else
                        newTab[e.hash & (newCap - 1)] = e; // 链表长度不够,没有转成树,这里只能是链表对象。
                    else if (e instanceof TreeNode) // 这里也是非常重要的一点,你在源码中搜索下TREEIFY_THRESHOLD就知道了。HashMap有个变身阈值,当大于TREEIFY_THRESHOLD,就会把单向链表转成树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 树这块大家留言,如果有必要,我再开一篇讲下。
                    else { // preserve order // 这里就是把当前数组的j下标下的链表数据,转移
                        Node<K,V> loHead = null, loTail = null; // 这里为什么有这么多的节点呢?
                        Node<K,V> hiHead = null, hiTail = null; // 这里为什么有这么多的节点呢?
                        Node<K,V> next; // 这个是游标,循环保存数据用的
                        do {
                            next = e.next; // 把e的下一个节点备份下,因为下一次循环,会用到,因为要通过这个next找下一个,
                            if ((e.hash & oldCap) == 0) { // 这里为什么要用oldCap?为什么又不减1呢? 很多人迷茫在这里,标注1
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        // 经过高低位区分,把数组下标为j的链表都分到了2个不同的2个链表中,这里这里,如果不明白,你就没明白这个算法的精妙
                        // 我再说明一点,你就明白为什么用高低位分了。假设当hash&oldCap的时候,hash参与的是0001,1001等10种,当新数组再次排列的时候
                        // 因为扩容的原因,hash的参与的0001,1001等前一位将参与到&计算中,这样一定会把全部的hash分成2中情况。不可能出现其他情况的。
                        // 这下明白了为什么要分高低位了吧。
                        } while ((e = next) != null); 
                        // 然而,经过上面的分离,我们就可以2个不同链表分在不同的数组中,如果高低位都有的话,
                        // 那一定是一个高位一个低位,不可能有例外
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        // 标注1:原来,新旧数组被以oldCap分为高位和地位。这个就是为上面会出现lo和hi开头的Node了。
        // 这里(e.hash & oldCap) == 0,就是关键了。可以这么理解,hash还是之前数据的hash值,hash &上的值,比之前大了1(原本的计算下标的位置是oldCap - 1),
        // 也就是说,如果之前是16(10000),那之前计算下标的时候,就是hash&15(1111),这个时候hash值变化了,结果都会在0-15的下标数组中。这个毋庸置疑的。
        好了,看黑板。重点来了,看看他是这么玩的,怎么重组的。
        // 以16的扩容场景来看,hash&15(1111)时,hash值和他&,都是只有4位的,记住这个,这个和为标注1要用(e.hash & oldCap)来区分高低位了。
        // hash&15(1111)时 hash值可以是0001,0002,1001等16种情况,这个时候,一定是16种,那问题来了。hash的二进制长度肯定不止4位啊,那在0001,0002前面一位是什么呢?
        // 大家知道了,只能是0或1,那标注1用(e.hash & oldCap)就是hash&16(10000)结合上面的分析,如果hash的前一位是0,那这个结果算出来的,(e.hash & oldCap) == 0是成立的
        // 如果前一位是1,那(e.hash & oldCap) != 0。
        // 到这里了,认真看过的同学都知道了,如果新的数组中,出现(e.hash & oldCap) == 0的数据,他就出在lo区域;如果不等于0.说明在新的数组中,他可以处于高位。
        
        // 最后的总结:扩容,就是把当前数组中的属于新数组新加进来的下标的链表分离出来,形成一个链表并保存起来。
        // 此时此刻,扩容原理完成了。
        return newTab;
    }


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值