设为首页 加入收藏

TOP

HashMap源码,看我这篇就够了(三)
2023-07-23 13:41:47 】 浏览:62
Tags:HashMap 源码
hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//节点key已经有值了,直接用新值覆盖 //该链是红黑树 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeva l(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; } //key已经存在直接覆盖value 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;//用作修改和新增快速失败 if (++size > threshold)//超过最大容量,进行扩容 resize(); afterNodeInsertion(evict); return null; }

①、判断键值对数组 table 是否为空或为null,否则执行resize()进行扩容;

②、根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③、判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④、判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤、遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥、插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容。

⑦、如果新插入的key不存在,则返回null,如果新插入的key存在,则返回原key对应的value值(注意新插入的value会覆盖原value值)

注意1:其中代码:

if (++size > threshold)//超过最大容量,进行扩容
    resize();

这里有个考点,我们知道 HashMap 是由数组+链表+红黑树(JDK1.8)组成,如果在添加元素时,发生冲突,会将冲突的数放在链表上,当链表长度超过8时,会自动转换成红黑树。

  那么有如下问题:数组上有5个元素,而某个链表上有3个元素,问此HashMap的 size 是多大?

  我们分析代码,很容易知道,只要是调用put() 方法添加元素,那么就会调用 ++size(这里有个例外是插入重复key的键值对,不会调用,但是重复key元素不会影响size),所以,上面的答案是 7。

10 扩容机制

扩容(resize),我们知道集合是由数组+链表+红黑树构成,向 HashMap 中插入元素时,如果HashMap 集合的元素已经大于了最大承载容量threshold(capacity * loadFactor),这里的threshold不是数组的最大长度。那么必须扩大数组的长度,Java中数组是无法自动扩容的,我们采用的方法是用一个更大的数组代替这个小的数组,就好比以前是用小桶装水,现在小桶装不下了,我们使用一个更大的桶。

JDK1.8融入了红黑树的机制,比较复杂,这里我们先介绍 JDK1.7的扩容源码,便于理解,然后在介绍JDK1.8的源码。

//参数 newCapacity 为新数组的大小
    void resize(int newCapacity) {
        Entry[] oldTable = table;//引用扩容前的 Entry 数组
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//扩容前的数组大小如果已经达到最大(2^30)了
            threshold = Integer.MAX_VALUE;///修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//初始化一个新的Entry数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity));//将数组元素转移到新数组里面
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阈值
    }
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//遍历数组
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);//重新计算每个元素在数组中的索引位置
                e.next = newTable[i];//标记下一个元素,添加是链表头添加
                newTable[i] = e;//将元素放在链上
                e = next;//访问下一个 Entry 链上的元素
            }
        }
    }

通过方法我们可以看到,JDK1.7中首先是创建一个新的大容量数组,然后依次重新计算原集合所有元素的索引,然后重新赋值。如果数组某个位置发生了hash冲突,使用的是单链表的头插入方法,同一位置的新元素总是放在链表的头部,这样与原集合链表对比,扩容之后的可能就是倒序的链表了。

下面我们在看看JDK1.8的。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//原数组如果为null,则长度赋值0
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//如果原数组长度大于0
            if (oldCap >
首页 上一页 1 2 3 4 5 下一页 尾页 3/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Java线程同步的四种方式详解(建议.. 下一篇Jmix 中 REST API 的两种实现

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目