HashMap方法详解(jdk1.8之后)

系列文章目录

一、位运算

在学习HashMap源码之前先熟悉一下位运算:
Java提供的位运算符有:左移( << )右移( >> )无符号右移( >>> )位与( & )位或( | )位非( ~ )位异或( ^ ),除了**位非( ~ )**是一元操作符外,其它的都是二元操作符。
负数转换为二进制:按照正数转换之后按位取反,最后加一。

  • 左移(<<):低位补0。

首先会将5转为2进制表示形式(java中,整数默认就是int类型,也就是32位):

0000 0000 0000 0000 0000 0000 0000 0101 然后左移2位后,低位补0:

0000 0000 0000 0000 0000 0000 0001 0100 换算成10进制为20

  • 右移(>>):正数高位补0,负数高位补1
    首先会将5转为2进制表示形式(java中,整数默认就是int类型,也就是32位):

0000 0000 0000 0000 0000 0000 0000 0101 然后左移2位后,低位补0:
0000 0000 0000 0000 0000 0000 0000 0001

  • 无符号右移( >>> )
    正数换算成二进制后的最高位为0,负数的二进制最高为为1。
    -5转换为二进制:1111 1011;
    05>>>3结果是: 0001 1111;

通过其结果转换成二进制后,我们可以发现,正数右移,高位用0补,负数右移,高位用1补,当负数使用无符号右移时,用0进行部位(自然而然的,就由负数变成了正数了)

  • 位与( & ):都为1则为1否则为0.
    例子: 5&4
    1101
    0100
    结果为:0100
  • 位非( ~ ):一元操作符,1变成0,0变成1。
    例子:~5
    0000 0101 转换为:
    1111 1010
  • 位或( | ):有一个为1则为1,反之为0。
    例子: 5|4
    1101
    0100
    结果为:1101
  • 位异或( ^ ):两个数不相等则为1,否则为0。
    例子:5 ^ 3
    0000 0101
    0000 0011
    结果:0000 0110

二、四个构造方法

HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)

1、无参构造方法。
2、一个参数初始容量构造方法
3、两个参数初始容量,负载因子构造方法
4、入参map构造方法,使用默认的负载因子(0.75)。
着重讲一下两个参数的构造方法:

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

tableSizeFor方法

    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

假如cap为10,则tableSizeFor返回值是16。
注意 tableSizeFor方法返回的值是初始化容量,而并非初始化容量*负载因子得到的真正的扩容阈值,这是因为初始化阈值放在了put方法中进行初始化了。
**【重点】**下面分析tableSizeFor方法的返回值为什么是16:
当在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。
首先为什么cap - 1,如果不减去1的话,当initialCapacity为2的幂的话,则最后计算的值会大一倍,具体原因请看下面的分析。
第一次右移

n |= n >>> 1;

由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。
第二次右移:

n |= n >>> 2;

注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。
第三次右移:

n |= n >>> 4;

这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。
以此类推
注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就32个1(但是这已经是负数了。在执行tableSizeFor之前,对initialCapacity做了判断,如果大于MAXIMUM_CAPACITY(2 ^ 30),则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作。所以这里面的移位操作之后,最大30个1,不会大于等于MAXIMUM_CAPACITY。30个1,加1之后得2 ^ 30) 。
举一个例子说明下吧。
例子说明
参考连接

三、put方法

简析:
1、tab为空,说明没有初始化,则进行初始化数组。
2、tab不为空,数组长度与hash取模的位置为空,则在该位置插入键值对。
3、到这儿可能产生了哈希冲突,如果插入的key在数组中存在则直接替换,下一步进入步骤6
4、如果p是树结构,则由红黑树来处理。
5、确定p是链式结构,则遍历链表,在尾节点插入键值对。如果链表长度大于等于8则发生树变,链式结构转换为树结构。
6、如果在链表中要插入的key存在,则直接替换,并返回旧的value,否则其他情况都返回null。
7、桶的数量增加,判断是否需要扩容。
流程图如下所示:
网络图片,侵权删除

1.1代码块一:put

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

1.2代码块二:putVal

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        1、初始化map的的节点数组
        Node<K,V>[] tab;
        1.1、当前key对应的节点
        Node<K,V> p; 
        1.2、n:tab的长度,i:要插入的位置。
        int n, i;
        2、tab为null,length为0则调用resize方法进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        2.1、i = (n - 1) & hash:计算要插入索引的位置,若该索引为null则直接插入键值对
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            3.1、发生碰撞,即当前位置已经存在值了
            3.2、e临时节点,起到交换作用
            Node<K,V> e; K k;
            3.3、如果该位置原key与要插入的key相同,则直接更新键值对
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            3.4、如果p是树结构,红黑树来处理
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {

                for (int binCount = 0; ; ++binCount) {
                    3.5、循环直到遍历到尾节点,设置键值对
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        3.6、 如果长度大于等于8时候,进行树变,转换为红黑树
                        对于临界值的分析:
                        假设此次是第六次,binCount == 6,不会进行树变,当前链表长度是7;下次循环。
                        binCount == 7,条件成立,进行树变,以后再put到这个桶的位置的时候,这个else就不走了,走中间的那个数结构的分叉语句啦
                        这个时候,长度为8的链表就变成了红黑树啦
                        if (binCount >= TREEIFY_THRESHOLD - 1)  -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    3.7、当前链表key相同,结束循环,就是没有走到链表的结尾,已经找到相同的key则直接跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    这个就是p.next也就是e不为空,然后,还没有key相同的情况出现,那就继续循环链表,
                     p指向p.next也就是e,继续循环,继续,e=p.next
                    p = e;
                    直到p.next为空,添加新的节点;或者出现key相等,更新旧值的情况才跳出循环。
                }
            }
            只有更新的时候,才走这,才会直接return oldValue
            if (e != null) {  existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        HashMap发生结构变化,变化次数累加,用于Fail-Fast机制进行判断是否抛出异常。
        ++modCount;
        if (++size > threshold)
        个数到达阈值,进行扩容
            resize();
        afterNodeInsertion(evict);
        此处返回null是因为链表新增了节点,所以上一次的值必然为null
        return null;
    }

四、resize()扩容机制

简析:
1、首先认识初始的几个变量
oldCap:原来数组的长度
oldThr:扩容的阈值
newCap:新数组的长度
newThr:新扩容的阈值
2、如果oldCap>0,说明已经初始化过了,先判断是否超过最大值,则不扩容,反之数组大小阈值都扩容一倍。
3、或构造方法指定了大小则直接设置,否则使用默认的默认 16,阈值为12。
4、最后创建新的长度的数组,旧数组数据转移到新的数组中。

2.1代码块一

  final Node<K,V>[] resize() {
        1.1、原数组节点
        Node<K,V>[] oldTab = table;
        1.2、原数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        1.3、要扩容的阈值(数组长度*负载因子如:16*0.75=12int oldThr = threshold;
        1.4、newCap:新的数组长度,newThr:新的扩容阈值
        int newCap, newThr = 0;
        2.1、如果已经初始化过数组的长度
        if (oldCap > 0) {
            2.2、原数组长度大于等于最大值,则不进行扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                2.3、设置扩容的阈值为2^31-1
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            2.4、数组长度没有超过最大值&&阈值为2的倍数,新数组扩容1倍,阈值也扩容1else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            3.1、构造函数设置了初始值,以该值进行初始化
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            3.2、构造函数没有设置初始值,则以默认值的进行初始化  
            newCap = DEFAULT_INITIAL_CAPACITY;   //16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //12 = 16*0.75
        }
        3.4、构造函数设置了默认值,设置扩容阈值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        3.5、设置新的扩容阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        3.6、创建新容量的node数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        4、如果旧数组为空,则在进行初始化,反之遍历旧数组,把数据移动到新的数组中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                5.1、当前节点的值不为空
                if ((e = oldTab[j]) != null) {
                    5.2、把原数组当前位置设置为null,帮助垃圾回收
                    oldTab[j] = null;
                    5.3、e.next为null说明不是链式结构和树结构,直接设置到新数组中
                    if (e.next == null)
                        5.4、取模运算计算数组的索引位置
                        newTab[e.hash & (newCap - 1)] = e;
                    5.5、e是树形结构
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    6.1、该节点为链式结构 ,lo链表 和 hi链表, loHead 和 loTail 分别指向 lo链表的头节点和尾节点, hiHead 和 hiTail以此类推.
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            6.2、遍历单向链表,如果 (e.hash & oldCap) == 0则旧数组索引与新数组索引不变
                                否则,新数组索引为原数组索引+oldCap(原数组大小)
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                //如果尾节点为null,说明还没有数据
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null); //单链表循环结束的条件
                        //如果 (e.hash & oldCap) == 0 则该节点在新表的下标位置与旧表一致都为 j
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //如果 (e.hash & oldCap) == 1 则该节点在新表的下标位置 j + oldCap
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

五、getNode方法

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        1、tab是否为空&&长度是否大于0&&在当前key索引的位置的value是否为空
        2(n - 1) & hash就求当前key在数组中索引的位置的
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            3、如果在索引位置的key与入参的key相同则直接返回。
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            4、说明是链式结构,可能是链表或者红黑树
            if ((e = first.next) != null) {
                4.1、是红黑树,则在红黑树中获取节点
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                4.2、遍历单向链表找到对应的key,返回对应的value
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值