HashMap源码解析和设计思路

一、整体结构

HashMap是Java集合框架中一种重要的数据结构,它按照Key-Value键值对的形式存储数据,即每一个值(Value)都由一个唯一的键(Key)与之对应。HashMap的工作原理是使用"拉链法"来解决哈希冲突的问题。

整个HashMap由以下几个主要部分组成,底层的数据结构主要是:数组 + 链表 + 红黑树

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
       
	//初始容量为 16
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

 //最大容量
 static final int MAXIMUM_CAPACITY = 1 << 30;

 //负载因子默认值
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
 //桶上的链表长度大于等于8时,链表转化成红黑树
 static final int TREEIFY_THRESHOLD = 8;

 //桶上的红黑树大小小于等于6时,红黑树转化成链表
 static final int UNTREEIFY_THRESHOLD = 6;

 //当数组容量大于 64 时,链表才会转化成红黑树
 static final int MIN_TREEIFY_CAPACITY = 64;

 //记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast
 transient int modCount;

 //HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化)
 transient int size;

 //存放数据的数组
 transient Node<K,V>[] table;

 // 扩容的门槛,有两种情况
 // 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。
 // 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75
 int threshold;
 
     // 内部使用静态内部类 Node<K,V> 来表示存储的键值对
 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        // 省略构造函数和方法
    }
    
 //红黑树的节点
  static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}

二、Java JDK 1.8中Node内部类的代码实现

NodeHashMap中最核心的内部类,它实现了Map.Entry接口,用于存储键值对数据。下面是Node类的主要属性及其作用:

  1. hash
    这是键值对中键的哈希值,在存储和查找时用于计算该键值对在数组中的存储位置。哈希值是final类型,在创建Node时就已经计算好并存储,以提高效率。

  2. key
    这是键值对中的键,也是final类型,一旦创建便不可修改。

  3. value
    这是键值对中的值,不是final类型,可以修改。

  4. next
    这是一个指向下一个节点的引用,用于解决哈希冲突时构建链表结构。

当插入新的键值对时,HashMap会先根据键的哈希值计算该键值对应存储在数组的哪个位置。如果该位置没有数据,就直接将新的Node存储在该位置;如果该位置已有数据,就需要构建链表结构,将新的Node插入到链表的末尾。

在查找键值对时,HashMap同样会先计算键的哈希值,找到对应的数组位置。如果该位置为空,则直接返回null;如果有数据,则需要遍历该位置上的链表,查找是否有键相同的Node

当链表长度过长时(默认阈值为8),桶大小大于64,为了提高查找效率,HashMap会将链表转化为红黑树结构。当红黑树节点个数较少时(默认阈值为6),则会重新转化为链表结构。

三、Java JDK 1.8中TreeNode内部类的代码实现

TreeNodeHashMap中用于构建红黑树数据结构的内部类。它继承自LinkedHashMap.Entry类,因为HashMap在树化时会复用LinkedHashMap中的双向链表节点,以方便地调整树的结构。

下面是TreeNode类的主要属性及其作用:

  1. parentleftright
    这三个属性分别代表该节点的父节点、左子节点和右子节点,用于构建红黑树的层级关系结构。

  2. prev
    这个属性继承自LinkedHashMap.Entry,用于维护双向链表的前驱指针。当删除节点时,可以方便地将其从双向链表中移除。

  3. red
    这是一个布尔类型的标志位,用于标记当前节点是红色节点还是黑色节点。红黑树通过这种方式,来维护自身的平衡性,从而保证了树的查找、插入和删除操作的时间复杂度为O(logN)

TreeNode继承自LinkedHashMap.Entry类,因此它也具备了键值对的存储功能。但是,TreeNode的主要作用是作为红黑树的节点,用于构建一个平衡的搜索树结构。

三、HashMap的基本工作流程

  1. 根据键值对的键调用散列函数得到一个哈希值。

  2. 将哈希值与(桶数组长度-1)相与运算得到存储在桶数组中的具体位置,即数组下标。

  3. 如果该位置的桶为空,则直接将键值对存储在该位置;如果不为空(哈希冲突),则以链表形式将新的键值对存储在链表后。

  4. 如果存在过多的哈希冲突,会触发树化(当单个链表长度达到8时),并且数组大小大于64,将链表转换为红黑树,以优化查询效率。当链表长度小于6时,红黑树转链表

  5. 当HashMap中存储的键值对达到一定数量(超过加载因子*初始容量)时,会触发重新散列,扩容并重新存储所有键值对。

四、初始化对象

 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);
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

Java JDK 1.8中HashMap的这几个构造函数

  1. public HashMap(int initialCapacity, float loadFactor)

这个构造函数允许用户在创建HashMap时指定初始容量和加载因子。它首先会检查传入的参数是否合法:初始容量不能为负数,加载因子必须是正数。

如果初始容量过大,会被重置为MAXIMUM_CAPACITY(2^30)。然后,它会将加载因子赋值给loadFactor字段,并根据指定的初始容量,计算出实际的阈值(threshold)并赋值。其中tableSizeFor()方法会找到最小的大于等于初始容量的2的幂次方值作为实际容量。

  1. public HashMap(int initialCapacity)

这个构造函数只允许指定初始容量,加载因子使用默认值DEFAULT_LOAD_FACTOR(0.75)。它调用了第一个构造函数,将加载因子设置为默认值。

  1. public HashMap()

这是无参构造函数,创建一个默认的HashMap实例。初始容量是16,加载因子是0.75。所有其他字段都使用默认值。

  1. public HashMap(Map<? extends K, ? extends V> m)

这个构造函数允许用户将一个现有的Map初始化到新创建的HashMap中。它首先将加载因子设置为默认值0.75,然后调用putMapEntries()方法,将传入Map中的所有键值对插入到新的HashMap中。

如果对性能要求不高,使用无参构造函数或只指定初始容量的构造函数就足够了。如果对性能有较高要求,可以适当增加初始容量和调整加载因子,以减少频繁的扩容和重新散列操作。

五、put()源码

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 定义局部变量
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        // 如果哈希桶数组为空或长度为0,先初始化或者扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // 计算该键值对应该存放在哈希桶数组的哪个位置,如果该位置为空,则直接创建一个新节点存放
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        else {
            // 否则,需要遍历该位置上的链表或红黑树
            Node<K,V> e; K k;

            // hash和值相同,如果当前节点就是要put的key,则直接覆盖value值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

            // 如果当前节点是TreeNode红黑树类型,调用红黑树的putTreeVal方法
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

            else {
                // 否则,就是链表操作
                for (int binCount = 0; ; ++binCount) {
                    // 如果链表遍历到尾部,仍未找到相同key的节点,就创建一个新节点加入到链表尾部
                    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的节点,则终止循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 如果找到了相同key的节点
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 如果允许覆盖,就替换value值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 如果是新键值对,modCount加1
        ++modCount;
        // 如果元素个数超过阈值,则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        // 返回null,代表put成功
        return null;
    }
  1. 首先定义了几个变量,分别是table数组、节点p、数组长度n和索引i

  2. 检查table数组是否为空或长度为0,如果是则调用resize()方法重新分配内存。

  3. 根据键的哈希值计算出该键值对在table数组中的存储位置索引i。如果该位置为空,则直接创建一个新的节点存储该键值对。

  4. 如果该位置不为空,意味着发生了哈希冲突,需要遍历冲突链表或红黑树:

    a) 如果遍历到的节点的键与要插入的键相同,则直接覆盖该节点的值。

    b) 如果遍历到的节点是红黑树节点,则调用树的插入方法putTreeVal插入新的键值对。

    c) 如果遍历到的节点是链表节点,则沿着链表继续遍历,直到链表尾部。

    i. 如果遍历到链表尾部都没有找到相同的键,则在链表尾部创建一个新的节点。

    ii. 如果链表长度超过阈值(默认为8),则将链表转化为红黑树。

  5. 如果找到了相同的键,则根据onlyIfAbsent参数决定是否覆盖值。如果覆盖,则返回原来的值。

  6. 如果是一个新的键值对,则增加size计数和modCount计数。如果size超过阈值,则调用resize方法扩容。

  7. 最后返回null,表示put操作成功执行。

核心是通过哈希值找到对应的存储位置,然后处理哈希冲突。通过链表和红黑树的数据结构,HashMap可以高效地存储和查找键值对。


treeifyBin树化方法

    final void treeifyBin(Node<K, V>[] tab, int hash) {
        // 定义局部变量
        int n, index;
        Node<K, V> e;

        // 如果哈希桶数组为空或长度小于树化阈值(64),则先进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();

            // 否则,获取该哈希值对应的链表头节点
        else if ((e = tab[index = (n - 1) & hash]) != null) {

            // 定义树的头节点和尾节点
            TreeNode<K, V> hd = null, tl = null;

            // 遍历整个链表,将链表上的节点替换为树节点
            do {
                TreeNode<K, V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);

            // 将树的头节点赋值给哈希桶数组的对应位置
            if ((tab[index] = hd) != null)
                // 对树进行树化操作,构建一颗红黑树
                hd.treeify(tab);
        }
    }

将特定位置上的链表节点转化为红黑树节点,以提高查找效率

  1. 首先判断哈希桶数组tab是否为空或长度小于树化阈值MIN_TREEIFY_CAPACITY(64)。如果是,则先进行扩容操作,以确保数组足够大。

  2. 获取该哈希值对应的链表头节点e

  3. 定义树的头节点hd和尾节点tl

  4. 遍历链表,将每个节点e转化为TreeNode节点,并用prevnext指针链接起来,组成一个双向链表。同时维护hdtl指针指向头节点和尾节点。

  5. 将树的头节点hd赋值给哈希桶数组tab的对应位置。

  6. 如果hd不为空,则调用treeify方法对树进行树化操作,构建一颗红黑树。

核心当链表长度大于等于 8 时,此时的链表就会转化成红黑树,此方法有一个判断,当链表长度大于等于 8,并且整个数组大小大于 64 时,才会转成红黑树,当数组大小小于 64 时,只会触发扩容resize(),不会转化成红黑树。由于红黑树的特性,查找、插入和删除操作的时间复杂度为O(log n)。相比链表的O(n)时间复杂度,红黑树的查找效率更高。


treeify方法构建一颗红黑树

   final void treeify(Node<K, V>[] tab) {
        // 定义根节点
        TreeNode<K, V> root = null;

        // 遍历双向链表,将每个节点插入到红黑树中
        for (TreeNode<K, V> x = this, next; x != null; x = next) {
            next = (TreeNode<K, V>) x.next;

            // 将当前节点的左右子节点先设置为null
            x.left = x.right = null;

            // 如果是第一个节点,则将该节点设置为根节点
            if (root == null) {
                x.parent = null;
                x.red = false; // 根节点设置为黑色
                root = x;
            }

            // 否则,按照红黑树的规则插入该节点
            else {
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;

                // 从根节点开始遍历,查找插入位置
                for (TreeNode<K, V> p = root; ; ) {
                    int dir, ph;
                    K pk = p.key;
                    if ((ph = p.hash) > h)
                        dir = -1; // 小于当前节点,查找左子树
                    else if (ph < h)
                        dir = 1; // 大于当前节点,查找右子树
                        // 如果哈希值相同,需要比较实际的键值大小
                    else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                        dir = tieBreakOrder(k, pk); // 如果键值相等,则按照系统顺序比较

                    TreeNode<K, V> xp = p;

                    // 如果找到了插入位置,则插入该节点,并调整树的平衡
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        x.parent = xp;
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
                        root = balanceInsertion(root, x); // 调整树的平衡
                        break;
                    }
                }
            }
        }

        // 将根节点移动到链表头部
        moveRootToFront(tab, root);
    }

将双向链表转化为红黑树,以提高查找效率

  1. 定义根节点root

  2. 遍历双向链表,对于每个节点x:

    • x的左右子节点先设置为null
    • 如果root为空,则将x设置为根节点。
    • 否则,按照红黑树的规则插入该节点:
      • 计算节点的哈希值和键值。
      • 从根节点开始遍历,比较哈希值和键值,查找插入位置。
      • 找到合适的插入位置后,插入该节点,并调用balanceInsertion方法调整树的平衡。
  3. 遍历结束后,调用moveRootToFront方法,将根节点移动到双向链表的头部。

在插入节点时,会严格按照红黑树的规则进行操作,包括比较键值、查找插入位置、调整树的平衡等,以确保红黑树的性质。

六、get()源码

    public V get(Object key) {
        Node<K, V> e;
        //key首先要获取hash
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    final Node<K, V> getNode(int hash, Object key) {
        // 定义局部变量
        Node<K, V>[] tab;
        Node<K, V> first, e;
        int n;
        K k;

        // 如果哈希桶数组不为空,并且该哈希值对应的首个节点也不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {

            // 如果首个节点就是要查找的节点,直接返回
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

            // 否则,如果后续节点不为空
            if ((e = first.next) != null) {

                // 如果首个节点是树节点,则在树中查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K, V>) first).getTreeNode(hash, key);
				// 链表查找的关键代码
                // 如果当前节点 hash 等于 key 的 hash,并且 equals 相等,当前节点就是我们要找的节点
                // 当 hash 冲突时,同一个 hash 值上是一个链表的时候,我们是通过 equals 方法来比较 key 是否相等的
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e; // 找到目标节点,返回
                        
                        // 否则,把当前节点的下一个节点拿出来继续寻找
                } while ((e = e.next) != null);
            }
        }

        // 如果遍历完都没找到,返回null
        return null;
    }

getf方法根据给定的哈希值和键,在HashMap中查找对应的节点

  • 根据 hash 算法定位数组的索引位置,equals 判断当前节点是否是我们需要寻找的 key,是的话直接返回,不是的话往下。
  • 判断当前节点有无 next 节点,有的话判断是链表类型,还是红黑树类型。
  • 分别走链表和红黑树不同类型的查找方法。

红黑树getTreeNode查找源码

/**
 * Calls find for root node.
 */
final TreeNode<K,V> getTreeNode(int h, Object k) {
    // 获取当前节点的根节点
    return ((parent != null) ? root() : this).find(h, k, null);
}
  1. return ((parent != null) ? root() : this).find(h, k, null);
    • 它首先检查当前节点(this)是否有父节点(parent != null)。
      • 如果有父节点,则调用root()方法获取整个树的根节点。
      • 如果没有父节点,说明当前节点就是根节点。
    • 然后,无论是获取到的根节点还是当前节点本身,都会调用其find(h, k, null)方法。
      • find方法用于在红黑树中查找具有给定哈希值h和键k的节点。
      • 第三个参数null表示从根节点开始查找,而不是从指定的节点开始查找。

获取红黑树的根节点,然后调用根节点的find方法在整个树中查找具有指定哈希值和键的节点。


红黑树查找实现细节

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    // 从当前节点开始查找
    TreeNode<K,V> p = this;

    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;

        // 如果当前节点的哈希值大于要查找的哈希值,则去左子树查找
        if ((ph = p.hash) > h)
            p = pl;
        // 如果当前节点的哈希值小于要查找的哈希值,则去右子树查找
        else if (ph < h)
            p = pr;
        // 如果当前节点的哈希值等于要查找的哈希值
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            // 如果当前节点的键也相等,则返回当前节点
            return p;
        // 如果当前节点的左子树为空,则去右子树查找
        else if (pl == null)
            p = pr;
        // 如果当前节点的右子树为空,则去左子树查找
        else if (pr == null)
            p = pl;
        // 如果能够进行键的比较操作
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            // 根据键的大小关系,决定去左子树还是右子树查找
            p = (dir < 0) ? pl : pr;
        // 如果去右子树查找,并且找到了结果,则返回结果
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        // 否则,去左子树继续查找
        else
            p = pl;
    // 如果p为null,则说明整棵树都找不到目标节点,结束循环
    } while (p != null);

    // 如果循环结束还没有找到,则返回null
    return null;
}

红黑树中查找具有指定哈希值和键的节点的功能

  1. 从当前节点p开始查找。

  2. 进入一个do-while循环,循环条件是p != null

  3. 在循环中,首先获取当前节点的哈希值ph、键pk以及左右子节点plpr

  4. 根据当前节点的哈希值与目标哈希值的大小关系,决定是去左子树还是右子树继续查找。

  5. 如果当前节点的哈希值等于目标哈希值,则比较键是否相等。如果相等,则返回当前节点。

  6. 如果当前节点的左子树或右子树为空,则根据情况决定去另一侧子树查找。

  7. 如果能够进行键的比较操作,则根据键的大小关系决定去左子树还是右子树查找。

  8. 如果去右子树查找并且找到了结果,则直接返回结果。否则,去左子树继续查找。

  9. 如果循环结束还没有找到目标节点,则返回null

利用了红黑树的二叉查找树特性,通过比较哈希值和键的大小关系,高效地在树中查找目标节点。如果找到了目标节点,则直接返回;如果没有找到,则返回null

七、remove()源码

removeNode方法分析

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    // 定义局部变量
    Node<K,V>[] tab; Node<K,V> p; int n, index;

    // 如果哈希桶数组不为空,并且该哈希值对应的首个节点也不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {

        Node<K,V> node = null, e; K k; V v;

        // 如果首个节点就是要删除的节点,将node赋值为首个节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // 否则,如果后续节点不为空
        else if ((e = p.next) != null) {
            // 如果首个节点是树节点,则在树中查找要删除的节点
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            // 否则,在链表中查找要删除的节点
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }

        // 如果找到了要删除的节点,并且值也匹配(若matchValue为true)
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 如果是树节点,则调用树的删除方法
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 如果是首个节点,则将首个节点设置为其下一个节点
            else if (node == p)
                tab[index] = node.next;
            // 否则,将当前节点的下一个节点设置为要删除节点的下一个节点
            else
                p.next = node.next;
            // 修改modCount和size
            ++modCount;
            --size;
            // 删除节点后的回调方法
            afterNodeRemoval(node);
            // 返回被删除的节点
            return node;
        }
    }

    // 如果没有找到要删除的节点,返回null
    return null;
}
  1. 首先定义了一些局部变量,包括哈希桶数组tab、首个节点p、数组长度n和索引index

  2. 检查哈希桶数组是否为空,长度是否大于0,并获取该哈希值对应的首个节点p。如果这些条件不满足,直接返回null

  3. 如果p不为空,则查找要删除的节点:

    • 如果p就是要删除的节点,则将node赋值为p
    • 如果p不是要删除的节点,则继续遍历后续节点:
      • 如果p是树节点,则调用getTreeNode方法在树中查找要删除的节点。
      • 如果p是链表节点,则遍历链表,查找要删除的节点。
  4. 如果找到了要删除的节点node,并且值也匹配(若matchValuetrue):

    • 如果node是树节点,则调用树的removeTreeNode方法删除该节点。
    • 如果node是首个节点,则将首个节点设置为其下一个节点。
    • 否则,将当前节点的下一个节点设置为要删除节点的下一个节点。
    • 更新modCountsize计数。
    • 调用afterNodeRemoval方法,执行删除节点后的回调操作。
    • 返回被删除的节点。
  5. 如果没有找到要删除的节点,则返回null

八、如何扩容?

  1. 触发扩容的条件

    HashMap会在两种情况下进行扩容:

    • 当HashMap中的元素个数超过阈值时,阈值计算方式是:阈值 = 容量 x 加载因子
    • 在插入新元素时,如果发生了较多的哈希冲突,导致有一个桶位置上的单向链表长度大于等于8时
  2. 扩容的准备

    • 首先计算新的容量和新的阈值,新容量是原容量的2倍,新阈值是新容量乘以加载因子
    • 创建一个长度为新容量的新数组
  3. 重新放置元素

    • 遍历原数组中所有节点
    • 重新计算每个节点在新数组中的位置索引
    • 将节点放置在新数组的相应位置
      • 如果该位置为空,则直接存放
      • 如果该位置已有节点,则形成一个新的单向链表
  4. 处理单向链表过长

    • 如果在放置节点时,某个位置上形成的单向链表长度大于等于8,则需要进一步判断
    • 只有在数组长度大于等于64的情况下,才将该单向链表转化为红黑树,以提高查找性能
    • 如果数组长度小于64,则即使链表过长,也不会树化,而是等待下一次扩容再处理
  5. 扩容完成

    • 经过上述操作,所有节点都已重新计算位置并放置在新数组中
    • 最后,将新数组赋值给HashMap的底层数组引用

HashMap在容量不足时能够扩大容量,减少哈希冲突的概率;同时也处理了单向链表过长的情况,在适当的时候通过树化提高查找性能。

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java语录精选

你的鼓励是我坚持下去的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值