基于openJDK 11 的HashMap 分析【超级大白话,充满细节】

基于openJDK 11 的HashMap 分析

在这里插入图片描述
HashMap及抽象类,接口和变量


使用

构造方法

基于openJDK 的HashMap构造方法,很多人在使用的时候其实并没有注意到构造方法内的参数的存在(比如本人,平常没有注意到这种小细节,面试被问到的时候非常之难受,决心总结一下);

HashMap有以下四种构造方法

  1. 包含指定初始化容量和负载因子数的构造函数(其他的构造函数本质是调用改构造函数)
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0) //初始化值小于0时,报非法数据异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) // 初始化容量大于最大容量值时,初始化容量设置为最大值(默认:1 << 30)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity); // tableSizeFor() 函数将初始化容量转换为超过传入值的最小的2的N次幂。因为HashMap要求容量均为2的次幂,这个方法保证了容量永远是2的次幂
    }
  1. 包含初始化容量的构造函数
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  1. 无参构造函数 ,其中的初始容量和负载因子都为默认值,分别为(16和0.75),负载因子及初始容量的概念将在后面详写
 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  1. 将其他的Map的键值对放入新构造的map中
public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false); //这里不涉及构造方法内容,不展开讲
    }

在这里插入图片描述

实例方法

在这里插入图片描述
对于HashMap中的方法,最常用的无非是 put,remove,get等,我们从put方法开始将,将HashMap及其内部结构进行具体的描述。

在这里插入图片描述
对于上图的理解,将在源代码的逐行解释中,变得逐渐清晰

  1. put()putVal()resize()方法
    每当新加入一个元素的时候,不可避免的有可能使用到上面的几种方法,那么我们就来简单的介绍一下其中的源代码吧。

当尝试用一句话概括这个过程的话,就是插入,检测哈希碰撞,发生哈希碰撞时的应对策略(链表链接,红黑树),以及为了预防哈希碰撞而进行的扩容(resize)

public V put(K key, V value) {
		//此处的hash()方法,内部还有可以使得结果更加散列的运算内容,减少哈希碰撞的发生
        return putVal(hash(key), key, value, false, true);
    }
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        //由于和(length-1)运算,length 绝大多数情况小于2的16次方。
        //所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列!!!
        //所以这样高16位是用不到的,如何让高16也参与运算呢。
        //所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。
        //所以(h >>> 16)得到他的高16位与hashCode()进行^运算。
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //一些会被用到的变量,此时的变量都为初始值( null,或者0)
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果 发现tab 或 table 为null(table是存放KV数据的数组。第一次使用的时候被初始化)
        //即 当前桶不存在
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //判断将要置入的tab位置是否为空 为null则可以直接将在此处新建一个Node<K,V>
        //((n-1)&hash 本质上是 hash % n ,这是在进行哈希运算)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null); // 调用class Node<K,V>的构造方法新建一个节点
        else { // 如果p不为null 则说明tab该下标位置发生了哈希碰撞,
            Node<K,V> e; K k;
            //在hash值相同的情况下,key也相同,则说明 就是当前的这个Node 所以直接赋值 e = p
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果key不同,仅仅是发生哈希碰撞,则要考虑链表或者放入树。
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	//这里的binCount也就是用来统计当前这个链表的长度啊有没有超过树化阈值(TREEIFY_THRESHOLD)
                for (int binCount = 0; ; ++binCount) {
                	//p,e就像一个一个遍历过程一样,p用来跑遍历 e = p.next; 底下有 p = e 即(p = p.next)
                	//如果遍历到最后了以依然没有,就新加进去好了。
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果超过了树化阈值 就把他转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //这里是用来判断是不是tab数组中的该点的链表中有这个key了
                    //如果是,则说明找到了对应的key的位置了,所以break就好了
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //结合上文 表达的是 p = p.next;
                    p = e;
                }
            }
            //说白了 找到了对应键的node,但它的值已经存在了
            //要根据 onlyIfAbsent这个参数的策略 来判断 是改变值还是保留原值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                这个方法在HashMap里是空的 ,这是为了LinkedHashMap(一个继承HashMap的子类)的方法
                //不展开讲,大概意思将该点移动到最后
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //计数,用来记录HashMap已经被修改过的次数
        ++modCount;
        //如果当前的map大小已经超过了阈值(最大容量*负载因子),就应该进行扩容
        //如果不进行扩容,在新插入<K,V>时,更容易导致发生哈希碰撞,严重影响效率
        if (++size > threshold)
            resize();
         //同样是LinkedHashMap的方法,删除eldest的元素
        afterNodeInsertion(evict);
        return null;
    }

上面putVal()方法解释了HashMap的存值,在存值中有*一个方法resize(),他的作用是重新分配调整map的大小,在初始化时,或者是阈值达到负载因子*tab数组的长度,都会调用这个方法。

下面我们来详细从源码介绍一下resize()方法。 值得注意的是resize()方法,返回值为Node<K,V>数组。

final Node<K,V>[] resize() {
		//table是存放KV数据的数组。第一次使用的时候被初始化
		//所以每次更新就将table存为oldTab ,即代表旧表 因为table要装入新表的内容
        Node<K,V>[] oldTab = table;
        //oldCap和oldThr 记录 之前的容量和阈值 
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //旧容量和旧阈值大于0时,就说明 不是初始化
        if (oldCap > 0) {
        //如果之前的容量已经达到了最大容量就不能扩容了,只能返回旧表了
            if (oldCap >= MAXIMUM_CAPACITY) { //MAXIMUM_CAPACITY = 1 << 30
            	//阈值调整为 INT_MAX
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果容量为原来的两倍时,不超过最大容量,同时原来的容量也大于等于默认初始化容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                     //tab容量扩大为原来的两倍的同时,阈值扩容了原来的两倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold 初始容量处于阈值
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults 零初始阈值表示使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

源码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值