HashMap源码解读(下篇)

简介: HashMap源码解读(下篇)

前言

上一篇博主对HashMap中的属性和Put方法进行了逐句解读,链接如下:

HashMap源码解读(中篇)

本篇将解读HashMap的resize()方法,构造方法,以及拓展一些HashMap中的特性。


一、Put方法核心流程

1 若HashMap还未初始化,先进行哈希表的初始化操作(默认初始化为16个桶)

2.对传入的Key值做hash,得出要存放该元素的桶编号

  • a.若没有发生碰撞,即头节点为空,将该节点直接存放到桶中作为头节点
  • b.若发生碰撞

(1) 此时桶中的链表已经树化,将节点构造为树节点后加入红黑树
(2) 链表还未树化,将节点作为链表的最后一个节点入链表

3.若哈希表中存在key值相同的元素,替换为最新的value值

4.若桶满了(++size是否大于threshold),调用resize()扩容哈希表。(threshold = 容量*负载因子)

二、自定义类型当作Key传入

现自定义一个Student类:

class Student {
    private String name;
    private int age;
}

若自定义的类型需要保存到Map的Key中,则需要同时覆写hashCode和equals,equals相同的俩对象,hashCode必须相同,反之不一定。

覆写后,Student类如下:

class Student {
    private String name;
    private int age;

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Student) {
            Student stu = (Student) obj;
            return this.age == stu.age && this.name.equals(stu.name);
        }
        return false;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

注意:代码中返回的是Object的工具类Objects,传入的属性值相同返回相同的hash值。

在这里插入图片描述

三、HashMap的构造方法

3.1 无参构造器如下:

在这里插入图片描述

当new一个HashMap的时候,若没有指定HashMap大小,默认调用无参构造器,可是此时内部数组还没有初始化,代码中只是简单的把负载因子初始化了,只有当第一次调用put方法时才初始化内部哈希桶数组(懒加载模式)。 第一次使用(添加)时才初始化相应的内存。

3.2 有参构造器如下:

在这里插入图片描述

如图红色框框,代码检查传入的参数是否2的n次幂,若不是调整为最接近 $2^n$ 的数。

在这里插入图片描述

例如:传入31,实际上初始化后threshold为32。

三、resize方法

源码如下:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else 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
            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;
    }

源码解读如下

1.若oldTab 为 null ,则证明还没有初始化。

在这里插入图片描述2. 当旧数组不为空的时候,新数组拓展为原数组的一倍。(扩容操作)

在这里插入图片描述
3.初始化操作在这里插入图片描述

4.元素搬移操作,把链表上的元素头插到扩容后的哈希桶中。

在这里插入图片描述

四、Set集合和Map集合的关系(拓展)

先下结论:

Set集合的子类实际上在存储元素时就是放在了Map集合的Key中:

在这里插入图片描述

这也是为啥Set是不可重复的!!!

  • HashSet其实就是使用HashMap保存的
  • TreeSet其实就是使用TreeMap保存的

Set就是用的Map的子类来存储元素,Set的不可重复就是因为元素保存在了Map的Key上。

在这里插入图片描述

  • HashSet 可以保存null ,因为HashMap的key可以为null
  • TreeSet 不可以保存null ,因为TreeMap的key 不能为null。

总结

以上三篇就是HashMap的主要源码解读了,有帮助的话可以点个赞收藏起来慢慢学习~

相关文章
|
2月前
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
31 2
|
2月前
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
39 2
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
62 0
|
7月前
|
存储 安全 Java
HashMap源码全面解析
HashMap源码全面解析
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
63 5
|
2月前
|
算法 索引
让星星⭐月亮告诉你,HashMap的resize()即扩容方法源码解读(已重新完善,如有不足之处,欢迎指正~)
`HashMap`的`resize()`方法主要用于数组扩容,包括初始化或加倍数组容量。该方法首先计算新的数组容量和扩容阈值,然后创建新数组。接着,旧数组中的数据根据`(e.hash & oldCap)`是否等于0被重新分配到新数组中,分为低位区和高位区两个链表,确保数据迁移时的正确性和高效性。
69 3
|
2月前
|
Java 索引
让星星⭐月亮告诉你,HashMap中红黑树TreeNode的split方法源码解读
本文详细解析了Java中`HashMap`的`TreeNode`类的`split`方法,该方法主要用于在`HashMap`扩容时将红黑树节点从旧数组迁移到新数组,并根据`(e.hash & oldCap)`的结果将节点分为低位和高位两个子树。低位子树如果元素数少于等于6,则进行去树化操作;若多于6且高位子树非空,则进行树化操作,确保数据结构的高效性。文中还介绍了`untreeify`和`replacementNode`方法,分别用于将红黑树节点转换为普通链表节点。
29 2
|
2月前
|
存储 Java
HashMap之链表转红黑树(树化 )-treefyBin方法源码解读(所有涉及到的方法均有详细解读,欢迎指正)
本文详细解析了Java HashMap中链表转红黑树的机制,包括树化条件(链表长度达8且数组长度≥64)及转换流程,确保高效处理大量数据。
106 1
|
2月前
|
存储 缓存 Java
HashMap源码解析
HashMap源码解析
|
7月前
|
Java
IDEA debug HashMap源码的心得
IDEA debug HashMap源码的心得
69 0
下一篇
DataWorks