Java 基础——HashMap 底层数据结构与源码分析

news2025/1/16 5:35:47

目录

  • 1.HashMap 简介
  • 2.HashMap 底层数据结构
    • 2.2.JDK1.8 之前
    • 2.3.JDK1.8 及以后
  • 3.常量定义
    • 3.1.默认初始化容量
    • 3.2.最大容量
    • 3.3.负载因子
    • 3.4.阈值
  • 4.HashMap 源码分析
    • 4.1.构造函数
    • 4.2.Node<K, V>
    • 4.2.put(K key, V value)
      • 流程
      • 源码
    • 4.3.get(Object key)
      • 流程
      • 源码
    • 4.4.resize()
      • 流程
      • 源码

参考文章:
HashMap 源码原理详解

相关文章:
Java 基础——HashMap 遍历方式

1.HashMap 简介

(1)HashMap 基于哈希表的 Map 接口实现,主要用来存放键值对(允许使用 null 值null 键),是非线程安全的。
(2)HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。

在这里插入图片描述

2.HashMap 底层数据结构

2.2.JDK1.8 之前

(1)JDK1.8 之前 HashMap 底层是数组 + 链表结合在一起使用,也就是链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (length - 1) & hash 判断当前元素存放的位置(这里的 length 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法,换句话说使用扰动函数之后可以减少 hash 冲突,提高效率

(2)拉链法指将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。如果遇到哈希冲突,则将冲突的值加添加到链表中即可,如下图所示:

在这里插入图片描述

2.3.JDK1.8 及以后

相比于之前的版本, JDK1.8 及以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时:

  • 如果数组 table 的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树;
  • 如果数组 table 的长度大于等于 64,那么将链表转化为红黑树,以减少搜索时间。

在这里插入图片描述

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构

3.常量定义

3.1.默认初始化容量

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

(1)为什么不直接使用 16 代替 1 << 4?

  • 为了避免使用魔法数字,使得常量定义本身就具有自我解释的含义。
  • 强调这个数必须是 2 的幂。

在编程领域,魔法数字指的是莫名其妙出现的数字,数字的意义必须通过详细阅读才能推断出来。

(2)HashMap 中数组 table 长度为什么是 2 的 n 次方?源码中是如何保证的?
① 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值 -2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。

② 我们首先可能会想到通过取余操作来实现。但是与 (&) 运算速度比取余 (%) 取模运算快,并且在取余 (%) 操作中如果除数是 2 的幂次,则等价于与其除数减一的与 (&) 操作,即 hash % length == hash & (length - 1) 的前提是 length 是 2 的 n 次方

③ 此外,如果 HashMap 的长度为 2 的 n 次方,那么在扩容迁移时不需要再重新定位新的位置了,因为扩容后元素新的位置,要么在原下标位置,要么在原下标 + 扩容前长度的位置

④ HashMap 源码中的 tableSizeFor(int cap) 可以保证其长度永远是是 2 的 n 次方。

/**
 * Returns a power of two size for the given target capacity.
 */
//返回大于且最接近 cap 的 2^n,例如 cap = 17 时,返回 32
static final int tableSizeFor(int cap) {
	// n = cap - 1 使得 n 的二进制表示的最后一位和 cap 的最后一位一定不一样
    int n = cap - 1;
    //无符号右移,在移动期间,使用 | 运算保证低位全部是 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;
}

① 无符号右移 >>>:忽略符号位,空位均用 0 补齐,最终结果必为非负数。
具体分析可参考HashMap 的长度为什么必须是 2 的 n 次方?这篇文章。

3.2.最大容量

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

当然了 Interger 能表示的最大范围为 231 - 1,除此之外,231 - 1 是 20 亿,每个哈希条目需要一个对象作为条目本身,一个对象作为键,一个对象作为值。在为应用程序中的其他内容分配空间之前,最小对象大小通常为 24 字节左右,因此这将是 1440 亿字节。但可以肯定地说,最大容量限制只是理论上的,实际内存也没这么大!

3.3.负载因子

(1)当负载因子较大时,给数组 table 扩容的可能性就会降低,所以相对占用内存较少(空间上较少),但是每条 entry 链上的元素会相对较多,查询的时间也会增长(时间上较多)。当负载因子较少时,给数组 table 扩容的可能性就会升高,那么内存空间占用就多,但是 entry 链上的元素就会相对较少,查出的时间也会减少。

(2)所以才有了负载因子是时间和空间上的一种折中的说法,那么设置负载因子的时候要考虑自己追求的是时间还是空间上的少。那么为什么默认负载因子 DEFAULT_LOAD_FACTOR 默认设置为 0.75?

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

其实 Hashmap 源码中给出了解释:

Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
emoval or resizing) they are converted back to plain bins.  In
usages with well-distributed user hashCodes, tree bins are
rarely used.  Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are:
0:    0.60653066
1:    0.30326533
2:    0.07581633
3:    0.01263606
4:    0.00157952
5:    0.00015795
6:    0.00001316
7:    0.00000094
8:    0.00000006
more: less than 1 in ten million

简单翻译一下就是:在理想情况下,使用随机哈希码时节点出现的频率在 hash 桶中遵循泊松分布 (Poisson Distribution),上面的解释给出了桶中元素个数和概率的对照。从中可以看到当桶中元素到达 8 个时,概率已经变得非常小,也就是说用 0.75 作为负载因子,每个碰撞位置的链表长度超过8是几乎不可能的

3.4.阈值

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * The next size value at which to resize (capacity * load factor).
 *
 * @serial
 */
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;

/**
 * The number of key-value mappings contained in this map.
 */
transient int size;
  • TREEIFY_THRESHOLD:使用红黑树而不是列表的 bin count 阈值。当向具有至少这么多节点的 bin 中添加元素时,bin 被转换为树。这个值必须大于 2,并且应该至少为 8,以便与树删除中关于收缩后转换回普通容器的假设相匹配。
  • UNTREEIFY_THRESHOLD:在调整大小操作期间取消(分割)存储库的存储计数阈值。应小于 TREEIFY_THRESHOLD,并最多 6 个网格与收缩检测下去除。
  • MIN_TREEIFY_CAPACITY:最小的表容量,可为容器进行树状排列(否则,如果在一个 bin 中有太多节点,表将被调整大小),至少为 4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间的冲突。
  • threshold:当 size 大于该值时,则调用 resize 方法进行扩容,threshold = capacity * loadFactor,例如初始时的 threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 16 * 0.75 = 12

4.HashMap 源码分析

4.1.构造函数

HashMap 中提供了 4 种构造函数,具体含义源码中的注释已经解释地比较清楚,可以直接查看:

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
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;
    // tableSizeFor(cap) 返回大于且最接近 cap 的 2^n,例如 cap = 17 时,返回 32
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * Constructs a new <tt>HashMap</tt> with the same mappings as the
 * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
 * default load factor (0.75) and an initial capacity sufficient to
 * hold the mappings in the specified <tt>Map</tt>.
 *
 * @param   m the map whose mappings are to be placed in this map
 * @throws  NullPointerException if the specified map is null
 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

4.2.Node<K, V>

HashMap 中数组 table 的元素类型即为 Node<K, V>,具体分析见下面代码:

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K, V>[] table;
    
/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
static class Node<K, V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    //指向下一个节点
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }
	
	//重写 hashCode() 方法
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
	
	//重写 equals() 方法
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

4.2.put(K key, V value)

流程

请添加图片描述

put 方法的执行流程如下:

  • 判断数组 table 是否为空,如果为空则执行 resize() 方法进行扩容;
  • 然后通过 hash 方法计算出 key 的哈希值,然后通过哈希值计算出 key 在数组中的索引位置 i,具体计算方法为 hash % n == hash & (length - 1)(其中 n 为数组 table 的长度,通过位运算替代取余运算可以提高计算效率);
  • 如果 table[i] 为 null,直接新建节点添加;
  • 如果 table[i] 不为空,则判断 table[i] 的首个元素的 key 是否和传入的 key 一样:
    • 如果相同直接覆盖 value,并直接返回旧的 value上图中没有体现出来!);
    • 如果不相同,则判断 table[i] 是否是树节点类型 (TreeNode),即 table[i] 是否是红黑树:
      • 如果是红黑树,则直接在树中插入键值对;
      • 如果不是红黑树,则说明当前索引位置 i 上的是一条链表,那么遍历该链表,当链表长度大于阈值(默认为 8)时:
        • 如果数组 table 的长度小于 64,那么会选择进行数组扩容(上图中没有体现出来!),然后再进行链表的插入操作;
        • 如果数组 table 的长度大于等于 64,那么将链表转化为红黑树,在红黑树中执行插入操作。
  • 遍历过程中若发现 key 已经存在直接覆盖 value 并直接返回旧的 value上图中没有体现出来!);
  • 插入成功后,判断实际存在的键值对数量 size 是否超多了最大容量 threshold,如果超过,则调用 resize 方法进行扩容。

源码

/**
 * Associates the specified value with the specified key in this map.
 * If the map previously contained a mapping for the key, the old
 * value is replaced.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with <tt>key</tt>, or
 *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
 *         (A <tt>null</tt> return can also indicate that the map
 *         previously associated <tt>null</tt> with <tt>key</tt>.)
 */
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    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;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // p 为树节点类型
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	//将链表转换为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                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;
}
/**
 * Replaces all linked nodes in bin at index for given hash unless
 * table is too small, in which case resizes instead.
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //数组⻓度如果⼩于 MIN_TREEIFY_CAPACITY (默认为 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);
    }
}

4.3.get(Object key)

流程

下图来自HashMap详细介绍(源码解析)这篇文章。

在这里插入图片描述

get 方法的执行流程如下:

  • 如果 table 为空,或者其长度为 0,则说明 hashMap 中没有元素,此时直接返回 null 即可;否则进行下面的操作;
  • 通过 hash 方法计算出 key 的哈希值,然后通过哈希值计算出 key 在数组中的索引位置,具体计算方法为 hash % n == hash & (length - 1)(其中 n 为数组 table 的长度,通过位运算替代取余运算可以提高计算效率);
  • 然后再进行如下判断:
    • 如果该索引位置上没有节点,则直接返回 null
    • 如果该索引位置上有节点,则先判断该位置上元素是否与 key 相同(这里的相同指的是 hashCode 以及 equals):
      • 如果相同,则直接返回该元素对应的 Node 中的 value;
      • 如果不相同,则对结点类型进行判断:
        • 如果该结点是红黑树结点 (TreeNode),则调用 getTreeNode 方法从红黑树中来进行搜索;
        • 否则遍历链表来进行搜索;
  • 如果经过以上步骤仍然没有找到,那么说明 hashMap 中不存在该 key,最终返回 null 即可。

源码

/**
 * Returns the value to which the specified key is mapped,
 * or {@code null} if this map contains no mapping for the key.
 *
 * <p>More formally, if this map contains a mapping from a key
 * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
 * key.equals(k))}, then this method returns {@code v}; otherwise
 * it returns {@code null}.  (There can be at most one such mapping.)
 *
 * <p>A return value of {@code null} does not <i>necessarily</i>
 * indicate that the map contains no mapping for the key; it's also
 * possible that the map explicitly maps the key to {@code null}.
 * The {@link #containsKey containsKey} operation may be used to
 * distinguish these two cases.
 *
 * @see #put(Object, Object)
 */
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
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);
            //在链表中查找
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

4.4.resize()

参考文章:
HashMap resize() 方法源码详解

流程

resize 方法实现了 HashMap 的扩容机制,其执行流程如下:

  • 计算扩容后的容量 newCap ,它等于旧容量 oldCap 的 2 倍
  • 创建长度为 newCap 的 Node<K, V> 数组 newTab
  • 遍历旧数组 OldTab 中的每一个节点 e,主要目的在于将旧数组中的节点复制到新数组中,具体操作如下:
    • 如果当前节点 e 为空,则说明旧数组的当前索引上没有元素,判断下一个节点;
    • 如果当前节点 e 不为空,则进行如下判断:
      • 当前节点 e 的下一个节点为空,则说明当前索引上只有 e 这一个节点,那么直接将 e 调整到新数组中索引为 e.hash & (newCap - 1) 的位置上,接着判断下一个节点
      • 当前节点 e 的下一个节点不为空,则说明当前索引上有可能存储的是链表或者红黑树,继续如下判断:
        • 如果节点 e 是树节点类型,调用 TreeNode 中的 split 方法来调整节点;
        • 否则当前索引上存储的是一条链表,那么依次遍历这条链条上的节点,并且将其复制到新数组中;

注意:由于 resize() 源码细节比较多,所以上述流程只抓住了重要部分进行说明。

源码

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
	//1.计算出新的 newCap (扩容后的容量)和 newThr (扩容后阈值)
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    	//如果 oldCap 大于等于最大值就不再扩容,直接返回 oldTab 即可
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果没有超过最大值,那么就扩充为原来的 2 倍
        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);
    }
    //2.根据 newCap 和 newThr 构造出新的 newTab
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
    	// j : 0 ~ oldCap - 1,遍历 table,将每个 bucket 都移动到新的 buckets 中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //如果该索引位上只有一个结点,则将 e 调整到索引为 e.hash & (newCap - 1) 的位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //如果该索引位上的结点是树节点类型,调用 TreeNode 中的 split 方法来调整节点
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //该索引位上是一条链表    
                else { // preserve order
                	/*
						loHead: 表示调整后仍会处于原索引位的节点之间重新链接后的链表的头节点
						loTail: 表示调整后仍会处于原索引位的节点之间重新链接后的链表的尾节点
						hiHead: 表示调整后应该处于新索引位的节点之间重新链接后的链表的头节点
						hiTail: 表示调整后应该处于新索引位的节点之间重新链接后的链表的尾节点
					*/
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        /* 
							(e.hash & oldCap) == 0 表示索引不变
							(e.hash & oldCap) == 1 表示索引变为"原索引 + oldCap"
						*/
                        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);
                    //索引 j 不变
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //索引 j 变为"j + oldCap"
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

下面主要解释一下 resize 方法源码中如何通过 e.hash & oldCap 的值来判断扩容后的索引是否变化的。

(1)设 oldCap 为数组 table 的长度(设为 16),当前 key 的哈希值为 hash,那么 oldCap 一定是 2 的 n 次幂(前面 3.1 节中有解释过),并且当前 key 在 table 中所对应的索引 oldj = hash % oldCap = hash & (oldCap - 1)

(2)现在假设扩容后 table 的长度为 newCap,那么 newCap = oldCap * 2 = 32,当前 key 在扩容后的 table 中所对应的索引为 newj = hash & (newCap - 1),扩容前后的索引的二进制表示如下所示:

	hash		xxxx ... xxxy xxxx		
&	oldCap - 1	0000 ... 0000 1111
	oldj		0000 ... 0000 xxxx

	hash		xxxx ... xxxy xxxx		
&	newCap - 1	0000 ... 0001 1111
	newj		0000 ... 000y xxxx	

由上述二进制表示可知, oldjnewj 是否相等取决于 y 的值:

  • 如果 y == 0,那么说明 oldj == newj,当前 key 不需要调整;
  • 如果 y == 1,那么说明 oldj != newj,当前 key 需要调整到 oldj + oldCap 的位置,因为根据它们的二进制表示可以推出 newj = oldj + oldCap

(3)resize() 源码中通过 hash & oldCap 的值来判断扩容后的索引是否有变化,hasholdCap 的二进制表示如下所示:

	hash		xxxx ... xxxy xxxx		
&	oldCap		0000 ... 0001 0000
	res			0000 ... 000y 0000
  • 如果 y == 0,那么说明 hash & oldCap == 0,即 oldj == newj,所以扩容后的索引不需要调整;
  • 如果 y == 1,那么说明 hash & oldCap == 1,即 newj = oldj + oldCap,所以扩容后的索引调整为 oldj + oldCap

(4)这种判断方法设计的巧妙之处在于:

  • 省去了重新计算 hash 值的时间,因为只需要通过原 key 的 hash(即代码中的 e.hash)与 oldCap 相与的结果即可判断新索引的值;
  • 由于 y 是 0 还是 1 可以认为是随机的,因此扩容时均匀地把之前冲突的节点分散到新的 bucket 中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/443770.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java使用阿里云oss上传文件

java使用阿里云oss上传文件 1、oss 是什么&#xff1f; OSS是阿里云对象存储服务&#xff08;Object Storage Service&#xff09;的一个简称&#xff0c;它是阿里云提供的海量、安全、低成本、高可靠的云存储服务。 即开即用、无限大空间的存储集群。相较传统建服务器存储而…

逻辑回归模型预测

范例题目&#xff1a; 建立一个逻辑回归模型预测一个学生是否通过研究生入学考试。N表示训练集中学生个数&#xff0c;Score1、Score2、 Score3、 Score4是N维数组&#xff0c;分别表示N个学生研究生初试、专业课复试、英语面试成绩、专业课面试成绩。Admitted是N维{0,1}数组&a…

RabbitMQ编程模型

RabbitMQ基础概念 RabbitMQ是基于AMQP协议开发的一个MQ产品。 虚拟主机 virtual host RabbitMQ出于服务器复用的想法&#xff0c;可以在一个RabbitMQ集群中划分出多个虚拟主机&#xff0c;每一个虚拟主机都有AMQP的全套基础组件&#xff0c;并且可以针对每个虚拟主机进行权…

面向对象(高级)-Annotation注解、单元测试的使用

注解&#xff08;Annotation&#xff09; 注解大纲 注解的使用1.Annotation的理解 - 注解&#xff08;Annotation&#xff09;是从JDK5.0开始引入&#xff0c;以注解名在代码中存在。 - Annotation可以像修饰符一样被使用&#xff0c;可用于修饰包、类、构造器、方法、成员变…

LeetCode:59. 螺旋矩阵 II

&#x1f34e;道阻且长&#xff0c;行则将至。&#x1f353; &#x1f33b;算法&#xff0c;不如说它是一种思考方式&#x1f340; 算法专栏&#xff1a; &#x1f449;&#x1f3fb;123 一、&#x1f331;59. 螺旋矩阵 II 题目描述&#xff1a;给你一个正整数 n &#xff0c…

Python中类属性和类方法

1. 类的结构 1.1 术语 —— 实例 使用面相对象开发&#xff0c;第 1 步 是设计 类使用 类名() 创建对象&#xff0c;创建对象 的动作有两步&#xff1a; (1) 在内存中为对象 分配空间 (2) 调用初始化方法 __init__ 为 对象初始化对象创建后&#xff0c;内存 中就有了一个对象…

【敲敲云】零代码实战,主子表汇总统计—免费的零代码产品

近来很多朋友在使用敲敲云时&#xff0c;不清楚如何使用主子表&#xff0c;及如何在主表中统计子表数据&#xff1b;下面我们就以《订单》表及《订单明细》表来设计一下吧&#xff0c;用到的组件有“设计子表”、“公式”、“汇总”等。 《订单》表展示 总金额 订单明细中“小…

C++ Linux Web Server 面试基础篇-操作系统(三、进程通信)

⭐️我叫忆_恒心,一名喜欢书写博客的在读研究生👨‍🎓。 如果觉得本文能帮到您,麻烦点个赞👍呗! 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧,喜欢的小伙伴给个三连支持一下呗。👍⭐️❤️ Qt5.9专栏定期更新Qt的一些项目Demo 项目与…

恢复调度平台mysql主从同步

修复问题 调度平台两台MySQL从节点存在Slave_SQL_Running异常&#xff0c;需要恢复。 部署步骤 一、先停止调度平台core服务与web服务&#xff0c;否则无法正常锁表 1.1停止调度平台core服务 2.1停止web服务 3.确认MySQL所有执行线程是否都已经停止 show processlist; 如…

小红书流量规则是什么,推荐机制解读

当今的互联网自媒体世界&#xff0c;说到底还是一个流量时代&#xff0c;一个流量为王的时代。不管你在小红书也好&#xff0c;还是其他自媒体平台都需要知晓平台的流量规则。今天和大家分享下小红书流量规则是什么&#xff0c;让我们一起通过流量规则分析小红书机制和算法。 一…

人工智能如何助力建筑设计自动化?

ChatGPT和DALL-E等工具使用大规模机器学习(ML)模型&#xff0c;并访问大量有标记和有意义的数据&#xff0c;以对文本和图像中的查询提供有见解的响应。但是&#xff0c;一些行业对训练ML模型的数据集的访问有限&#xff0c;这使得使用生成式AI来解决现实世界问题的好处很难获得…

书写我的人生回忆录-这应该是给父母最好的礼物

作为一个业余的软件开发爱好者&#xff0c;我又捣鼓了一个有意思的小东西 &#xff0c;使用完全免费哈 《书写我的人生回忆录》是一款软件&#xff0c;其中包含70个问题&#xff0c;涵盖了父母的个人喜好、家庭、工作、人生经历和态度等方面。通过回答这些问题&#xff0c;您的…

爬虫请求头Content-Length的计算方法

重点&#xff1a;使用node.js 环境计算&#xff0c;同时要让计算的数据通过JSON.stringify从对象变成string。 1. Blob size var str 中国 new Blob([str]).size // 6 2、Buffer.byteLength # node > var str 中国 undefined > Buffer.byteLength(str, utf8) 6 原文…

Spring开启事务流程和事务相关配置

文章目录 Spring事务Spring快速入门事务相关配置 Spring事务 Spring快速入门 事务作用&#xff1a;在数据层保障一系列的数据库操作同成功同失败 Spring事务作用&#xff1a;在数据层或业务层保障一系列的数据库操作同成功同失败 Spring提供了一个接口PlatformTransactionMa…

Vue可视化项目搭建

安装Nodejs 全局下载Vue项目脚手架 创建项目 运行项目 项目初始化 安装Nodejs 下载地址&#xff1a;https://nodejs.org/zh-cn/ 下载完成之后一路点击下一个安装 全局下载Vue项目脚手架 进入开始菜单以管理员身份运行命令提示符 输入更换镜像源为淘宝源 npm config s…

java线程屏障CyclicBarrier

CyclicBarrier允许一组线程在达到一个公共的屏障点时相互等待。它在涉及固定大小的线程组、并且这些线程必须相互等待的程序中非常有用&#xff0c;CyclicBarrier可以在等待的线程被释放后被重用。 构造方法 CyclicBarrier(int parties) 创建一个新的屏障并设置将要访问这个…

问卷调查样本量的确定方法

我们在进行问卷调查的时候&#xff0c;问卷的收集数量是重要的流程之一。问卷数量取决于几个因素&#xff0c;包括研究的目的和研究的类型。接下来&#xff0c;我们就聊一聊怎么确定所需的调查问卷数量。 1、确定研究目标。 确定所需问卷数量的第一步是明确研究目标。这一步是…

jar包依赖冲突该怎么解决(IT枫斗者)

jar包依赖冲突该怎么解决&#xff08;IT枫斗者&#xff09; maven jar包依赖规则 间接依赖路径最短优先一个项目依赖了a和b两个jar包&#xff0c;其中a-b-c1.0&#xff0c;d-e-c1.0,由于c1.0路径最短&#xff0c;所以项目最后使用的jar包是c1.0pom文件中申明顺序优先有人就问…

使用三轴XYZ平台绘制空心字

1. 功能说明 本文示例将实现R312三轴XYZ平台绘制“机器时代”空心字的功能。 2. 电子硬件 在这个示例中&#xff0c;采用了以下硬件&#xff0c;请大家参考&#xff1a; 主控板 Basra主控板&#xff08;兼容Arduino Uno&#xff09; 扩展板 Bigfish2.1扩展板 SH-ST步进电机扩展…

2023年最系统的自动化测试,测试开发面试题,10k以下不建议看

鉴于现在严峻的就业形势&#xff0c;千万大学生即将出新手村&#xff0c;今天给大家打包好了2023最能避免薪资倒挂的《面试圣经》。不经一番寒彻骨,怎得梅花扑鼻香。这份面试题&#xff0c;与君共勉&#xff01; 一、开场白 Q&#xff1a;简单自我介绍一下吧 Q&#xff1a;项…