Java 7、8 HashMap源码详解与分析

news2024/11/20 2:39:32

文章目录

  • 一、哈希表的简介
  • 二、JDK1.7 HashMap
    • 1、构造方法
    • 2、添加方法
      • put()方法
      • addEntry()方法
    • 3、存在的问题
  • 三、JDK1.8 HashMap
    • 1、红黑树TreeMap
    • 2、属性
    • 3、存储的结构
    • 4、构造方法
    • 5、添加方法
      • put(K, V)方法
      • resize扩容方法
    • 5、putAll()方法
    • 6、移除方法
      • remove(Object key)
    • 8、查找方法
      • get(Object key)方法
    • 9、HashMap中JDK1.8的树化方法

可以参考视频:
Java 7/8 HashMap源码详解

一、哈希表的简介

  • 核心就是:基于哈希值的桶和链表
    • 一般就是用数组来实现桶
    • 发生碰撞的时候,用链表来链接发生碰撞的元素

假设我们hash(x) = x % 16, 对下面所有元素进行hash并放入到hash表中。
在这里插入图片描述

  • O(1)的平均查找、插入、删除时间.

  • 致命缺点是哈希值的碰撞(collision)

    • 哈希碰撞:元素通过哈希函数计算后,会被映射到同一个桶中。上面的26, 126就发生了哈希碰撞。

二、JDK1.7 HashMap

对于HashMap的基本概念和在Java中的继承关系,相信都有一定的了解。
下面我只是对JDK 1.7 中不足的地方做简单分析。

1、构造方法

无参构造方法

// 默认初始化容量 2^4 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

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

// 默认负载因子 0.75,用于计算阈值,进行扩容操作。选择0.75是一种在时间和空间上的折中选择 
// 如果当前哈希表中的元素个数 >= 容量 × 负载因子,就会进行扩容,否则可能就会发生严重的哈希碰撞
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 此处调用了含容量和负载因子的构造方法来进行初始化操作
public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

含容量的构造方法

 public HashMap(int initialCapacity) {
 		// 调用了含容量和负载因子的构造方法来进行初始化操作,其中容量为传入的容量,负载因子为默认负载因子0.75
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

含容量和负载因子的构造方法

public HashMap(int initialCapacity, float loadFactor) {
    	// 如果传入的初始化容量小于0,则抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
    
    	// 如果传入的容量大于最大容量,就初始化为最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
    
    	// 如果负载因子小于0,或者是非法的浮点数,抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
    
    
		// 根据传入的负载因子给负载因子赋值
        this.loadFactor = loadFactor;
   
    	// int threshold
    	// 阈值,容量×负载因子。目前大小为initialCapacity(还未扩容)
    	// 超过阈值进行扩容操作
        threshold = initialCapacity;
    
    	// 此处的init()方法是一个空方法,在向哈希表添加元素之前,不会真正地创建哈希表(以免占用过多的内存)
        init();
}

// 空方法
void init() {
}

可见,无论调用哪种构造函数来初始化HashMap,最终调用的都是含容量和负载因子的构造方法,并且都没有真正的开辟出需要的内存空间

2、添加方法

put()方法

// 空集合,用于判断表是否为空。Entry为hashMap的静态内部类
static final Entry<?,?>[] EMPTY_TABLE = {};

public V put(K key, V value) {
    // 如果表是空的,就通过inflateTable()方法进行扩容
    if (table == EMPTY_TABLE) {
        // 等到真正向哈希表中添加元素时,才开辟内存空间
        inflateTable(threshold);
    }
    
    if (key == null)
        return putForNullKey(value);
    
    // 计算要插入元素的哈希值
    int hash = hash(key);
    
    // 根据哈希值来判断插入元素应该放在哪个桶中
    // 该方法决定了为什么哈希表的容量是2的幂
    int i = indexFor(hash, table.length);
    
    // 遍历哈希表
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        
        // 看待插入元素在哈希表中是否已近存在了,如果存在了,就进行覆盖操作
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    
   	// 真正的添加操作,采用头插法,下面会单独来说
    addEntry(hash, key, value, i);
    return null;
}


// 哈希表扩容函数
private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
    	// 让容量向上舍入变为2的幂。比如toSize = 10 就会变为 16。
        int capacity = roundUpToPowerOf2(toSize);
		
    	// 阈值,向上取整后的容量×负载因子 或 最大容量+1,取其中的较小值
    	// 该变量在第一次放入操作时不会用到
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    
    	// 根据capacity创建哈希表
        table = new Entry[capacity];
     	
    	// 创建了一个哈希种子,重构String的hash算法,在后面的潜在安全漏洞会谈到
        initHashSeedAsNeeded(capacity);
}

private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
    	// 如果容量大于最大容量,就返回最大容量。
    	// 否则调用Integer.highestOneBit()方法让其向上舍入为2的幂
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

// 通过一系列移位操作与异或操作获得元素的哈希值。 JDK 8 中已不再使用该方法
final int hash(Object k) {
        int h = hashSeed;
    	// 如果哈希种子存在,并且进行哈希的元素的String类型
        if (0 != h && k instanceof String) {
            // 就让String使用另一种hash算法
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

为什么哈希表大小一定是2的幂?
要回答这个问题,我们需要先看看哈希表中一个重要的方法:static int indexFor(int h, int length),该方法会根据插入元素的哈希值决定该元素应该被放在桶中。

static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        // 将传入的哈希值与其长度-1进行按位与操作,并返回其结果
        return h & (length-1);
}

对于哈希表,其实最致命的缺点就是哈希碰撞,也就是多个值会被放置在同一个桶中。最极端的碰撞如下:
在这里插入图片描述
想要避免发生哈希碰撞,就需要分别分到不同的桶中,对于indexFor函数中的h & (length - 1)就是这样一个操作,根据元素的哈希值和哈希表的长度-1来按位与,并且与运算的速度快,而且效率高。

如何体现出来呢?这和普通的 hash value = x % 10的优势在哪里?

当哈希表的大小长度为2的幂时,他的二进制表示是10000,让其中的长度-1之后就是1111。
在这里插入图片描述
当一个二进制与全为1的数进行按位与时,其结果就是等于 该数本身并且小于等于桶的最大数量,这样以来,只要数不同,那么他们按位与下来的值也就不同了,所以我们需要哈希表的容量为2的幂,这样可以提高计算效率。

如果是2的幂,可以理解为是一种截断,假设我们哈希长度还是16 ,按照indexFor运算。199hash值是:
在这里插入图片描述
为什么使用位运算,而不是直接取模?

位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。

addEntry()方法

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果哈希表中的元素个数超过了阈值,并且该元素应该放入的桶中已经有了元素
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 进行扩容,扩容大小为原大小的2倍,以保证扩容后容量仍为2的幂
        // 并将扩容前哈希表中的元素全部重新计算哈希值,并放入到扩容后的桶中
        resize(2 * table.length);
        
        // 重新计算哈希值
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    // 创建节点,采用头插法将其放在对应的桶中
    createEntry(hash, key, value, bucketIndex);
}

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
		
    	// 扩容为新容量
        Entry[] newTable = new Entry[newCapacity];
    
    	// 重新计算元素哈希值,再放入到扩容后的哈希表中
        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;
            }
        }
}

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
    	
    	// 将新节点放在桶的第一个位置,也就是采用头插法进行插入
    	void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
}

3、存在的问题

1 容易发生死锁
因为HashMap本身是线程不安全的,所以在多线程环境下,可能会发生死锁问题。JAVA HASHMAP多线程下的死循环

2 非常大的安全问题
和上文所提过的那样,HashMap可能会退化成一个单链表。
而且我们知道,HashMap是通过indexFor对对象计算出的hashcode值作为输入然后输出哈希表桶的位置,之后对桶后链表进行遍历,调用对象的equals方法,判断是要新建一个entry,还是在链表中存在相同的entry(当然,这里是通过key来判断的)。

String的哈希算法很容易就能产生多个哈希值相同字符串。所以,很可能很多的字符串就会放到同一个桶中,也就大概率会退化成单链表。

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
}

但是,为了避免这种攻击,在 JDK 7 的哈希表中,重构了String类型计算哈希值的方法。

三、JDK1.8 HashMap

极端情况下, 有可能HashMap会退化成链表的形状 。这就违背了查询的高效性。而今天HashMap使用红黑树就是为了解决这个问题。
JDK1.8 HashMap不再使用数组 + 链表的方式,而是使用数组+ 链表 + 红黑树的方式。

1、红黑树TreeMap

基本介绍
一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑**(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍**,因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树

性质

  • 每个节点非红即黑
  • 根节点(root)是黑的
  • 不能有两个红色的节点连在一起(黑色可以)
  • 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的
  • 如果一个节点是红的,那么它的两儿子都是黑的
  • 对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点
  • 每条路径都包含相同的黑节点

在这里插入图片描述
下面是HashMap中的结构
在这里插入图片描述
为什么用到了红黑树
让我们来看看注释

/*
HashMap 底层是基于散列算法实现,散列算法分为散列再探测和拉链式。HashMap 则使用了拉链式的散列算法,并在 JDK 1.8 中引入了红黑树优化过长的链表
    实现注意事项。
    链表结构(这里叫 bin ,箱子)
    Map通常充当一个binned(桶)的哈希表,但是当箱子变得太大时,它们就会被转换成TreeNodes的箱子,每个箱子的结构都类似于java.util.TreeMap。
    大多数方法都尝试使用普通的垃圾箱,但是在适用的情况下(只要检查一个节点的实例)就可以传递到TreeNode方法。
    可以像其他的一样遍历和使用TreeNodes,但是在过度填充的时候支持更快的查找
    然而,由于大多数正常使用的箱子没有过多的填充,所以在表方法的过程中,检查树箱的存在可能会被延迟。
    树箱(bins即所有的元素都是TreeNodes)主要是由hashCode来排序的,但是在特定的情况下,
    如果两个元素是相同的“实现了Comparable接口”,那么使用它们的比较方法排序。
    (我们通过反射来保守地检查泛型类型,以验证这一点——参见方法comparableClassFor)。
    使用树带来的额外复杂,是非常有价值的,因为能提供了最坏只有O(log n)的时间复杂度当键有不同的散列或可排序。
    因此,性能降低优雅地在意外或恶意使用hashCode()返回值的分布很差,以及许多key共享一个hashCode,只要他们是可比较的。
    (如果这两种方法都不适用,同时不采取任何预防措施,我们可能会在时间和空间上浪费大约两倍的时间。
    但是,唯一已知的案例源于糟糕的用户编程实践,这些实践已经非常缓慢,这几乎没有什么区别。)
    因为TreeNodes大小大约是普通节点的两倍,所以只有当容器包含足够的节点来保证使用时才使用它们(见treeifythreshold)。
    当它们变得太小(由于移除或调整大小),它们就会被转换回普通bins。
    在使用良好的用户hashcode的用法中,很少使用树箱。
    理想情况下,在随机的hashcode中,箱子中节点的频率遵循泊松分布(http://en.wikipedia.org/wiki/Poisson_distribution),
    默认大小调整阈值为0.75,但由于调整粒度的大小有很大的差异。
    忽略方差,list的长度 k=(exp(-0.5) * pow(0.5, k) / factorial(k))
    第一个值是:
            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 如果再多的话,概率就小于十万分之一了
    树箱(tree bin很难翻译啊!)的根通常是它的第一个节点。
    然而,有时(目前只在Iterator.remove)中,根可能在其他地方,但是可以通过父链接(方法TreeNode.root())恢复。
    所有适用的内部方法都接受散列码作为参数(通常由公共方法提供),允许它们在不重新计算用户hashcode的情况下调用彼此。
    大多数内部方法也接受一个“标签”参数,通常是当前表,但在调整或转换时可能是新的或旧的。
    当bin列表被树化、分割或取消时( treeified, split, or untreeified),我们将它们保持在相同的相对存取/遍历顺序(例如
    字段Node.next)为了更好地保存位置,并稍微简化对调用迭代器的拆分和traversals的处理(splits and traversals that invoke iterator.remove)。
    当在插入中使用比较器时,为了在重新平衡中保持一个总排序(或者在这里需要的接近),我们将类和标识符码作为连接开关。
    由于子类LinkedHashMap的存在,普通(plain)与树模型(tree modes)之间的使用和转换变得复杂起来。
    请参阅下面的hook方法,这些方法在插入、删除和访问时被调用,允许LinkedHashMap内部结构保持独立于这些机制。
            (这还要求将Map实例传递给一些可能创建新节点的实用方法。)
    concurrent-programming-like SSA-based编码风格有助于避免在所有扭曲的指针操作中出现混叠错误。
*/

所以总结一下是:
之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢

而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了** 查找数据快O(log n),解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当桶中元素大于8并且桶的个数大于64**的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

链表转红黑树体现了空间换时间的思想

2、属性

JDK1.8 中HashMap的属性

// 初始容量 16 (2的4次方)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

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

// 默认负载因子,默认为0.75,用于和容量一起决定扩容的阈值
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;

// Node数组,也就是桶
transient Node<K, V>[] table;


// 缓存entrySet
transient Set<Map.Entry<K,V>> entrySet;

// map中k-v键值对的个数
transient int size;

// 修改访问标记
transient int modCount;

// 扩容阈值,等于 容量 × 负载因子,初始化时值为向上取2的幂
int threshold;

// 负载因子
final float loadFactor;

3、存储的结构

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; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    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、构造方法

无参构造方法

public HashMap() {
    // 初始化了负载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

HashMap(int initialCapacity)构造方法

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

HashMap(int initialCapacity, float loadFactor)方法

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);
}

// 容量向上取整为2的幂
static final int tableSizeFor(int cap) {
        int n = cap - 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;
}

5、添加方法

put(K, V)方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// 因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希寻址是忽略容量以上的高位的
// 那么这种处理就可以有效避免类似情况下的哈希碰撞
static final int hash(Object key) {
        int h;
    	// 如果key不为null,就让key的高16位和低16位取异或
    	// 和Java 7 相比,hash算法确实简单了不少
    	// 使用异或尽可能的减少碰撞
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 放入元素的操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    	// tab相当于哈希表
        Node<K,V>[] tab; 
    	Node<K,V> p; 
    
    	// n保存了桶的个数
    	// i保存了应放在哪个桶中
    	int n, i;
    
    	// 如果还没初始化哈希表,就调用resize方法进行初始化操作
    	// resize()方法在后面分析
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    
    	
    	// 这里的 (n - 1) & hash 相当于 Java 7 中的indexFor()方法,用于确定元素应该放在哪个桶中
    	// (n - 1) & hash 有以下两个好处:1、放入的位置不会大于桶的个数(n-1全为1) 2、用到了hash值,确定其应放的对应的位置
        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;
            // 如果采用了红黑树结构,就是用红黑树的插入方法
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 否则,采用链表的扩容方式
            else {
                // binCount用于计算桶中元素的个数
                for (int binCount = 0; ; ++binCount) {
                    // 找到插入的位置(链表最后)
                    if ((e = p.next) == null) {
                        // 尾插法插入元素
                        p.next = newNode(hash, key, value, null);
                        // 如果桶中元素个数大于阈值,就会调用treeifyBin()方法将其结构改为红黑树(但不一定转换成功)
                        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;
    
    	// 如果size大于阈值,就进行扩容操作
        if (++size > threshold)
            resize();
    
        afterNodeInsertion(evict);
        return null;
}

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;
    
    // 原容量大于0(已近执行了put操作以后的扩容)
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 原容量扩大一倍后小于最大容量,那么newCap就为原容量扩大一倍,同时新阈值为老阈值的一倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 调用了含参构造方法的扩容
    // 原容量小于等于0,但是阈值大于0,那么新容量就位原来的阈值(阈值在调用构造函数时就会确定,但容量不会)
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    
    // 调用了无参构造方法的扩容操作
    else {               
        // zero initial threshold signifies using defaults
        // 如果连阈值也为0,那就调用的是无参构造方法,就执行初始化操作
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // 如果新阈值为0,就初始化它
    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
                    // lo 和 hi 分别为两个链表,保存了原来一个桶中元素被拆分后的两个链表
                    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;
}

图解扩容对链表的重构
比如哈希表中桶的个数是4个,其中0、4、8、12因为低两位都是0,与 4-1=3(11) 进行按位与后,都被放在了第一个桶中。
在这里插入图片描述
然后开始了扩容操作。将元素哈希值按位与旧容量(不是旧容量-1)为0的放在lo链表中,不为0的放在hi链表中。

do {
    next = e.next;
    // 当前元素的hash值和容量进行按位与,决定被分配到哪个链表中
    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);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
遍历链表,将lo中的放在原来的桶中,hi中的放在增加的桶中

// 通过头指针直接将链表放入桶中
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

在这里插入图片描述
总结:

  • 先将原桶中的元素的hash值与旧容量进行按位与操作

    • 如果结果为0,就放入lo链表中
    • 如果结果不为0,就放入hi链表中
  • lo链表中的元素继续放在新的哈希表中原来的位置

  • hi链表中的元素放在新的哈希表中,扩容后相对于原来的位置上(j+oldCap)

    • 两个桶之间的间隔数就为增加原来哈希表的容量
      好处
  • 顺序放入,减少了发生死锁的几率

  • 使得元素相对均匀地存在于哈希表中

5、putAll()方法

public void putAll(Map<? extends K, ? extends V> m) {
    putMapEntries(m, true);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    	// 获得待插入表的大小
        int s = m.size();
    
    	// 如果待插入表中有元素
        if (s > 0) {
            // 如果当前哈希表为空
            if (table == null) { // pre-size
                // 容量调整过程,如果超过阈值,就调用tableSizeFor()方法扩容(直接扩容,因为此时哈希表为空)
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            // 超过阈值,就调用resize()方法扩容
            else if (s > threshold)
                resize();
            // 把元素依次放入到哈希表中
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
}

这里用到了hash(),具体可以研究:知乎:hash()分析的最透彻的文章

6、移除方法

remove(Object key)

public V remove(Object key) {
    Node<K,V> e;
    // 调用removeNode()方法,返回其返回的结果
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

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;
    
    	// 桶中有元素, p保存了桶中的首个元素
        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);
                }
            }
            
            // 该元素不为空
            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;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
}

8、查找方法

get(Object key)方法

public V get(Object key) {
    Node<K,V> e;
    // 通过key的哈希值和key来进行查找
    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;
            
            // 如果第一个元素不是,就继续往后找。找到就返回,没至找到就返回null
            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;
}

9、HashMap中JDK1.8的树化方法

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    
    // 如果当前哈希表中桶的数目,小于最小树化容量,就调用resize()方法进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    
    // 当桶中元素个数大于8,且桶的个数大于64时,进行树化
    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);
    }
}

链表变为红黑树的条件:元素个数大于8同时桶的个数大于64
推荐阅读:Hashmap链表长度为8时转换成红黑树,你知道为什么是8吗?
当某个桶中的元素个数大于8时,会调用 treeifyBin()方法,但并不一定就会变为红黑树
当哈希表中桶的个数大于64时,才会真正进行让其转化为红黑树

为什么桶中元素多于了8个,桶的个数小于64,调用resize()方法就可以进行调整?

因为调用resize()方法进行扩容时,会让同一个桶中的元素进行桶的重新分配。一部分会被放新哈希表中在原来的位置上,一部分会被放在扩容后的位置上。

为什么桶一定要大于64


以上就是文章的全部:
推荐阅读:
HashMap源码分析
【动图演示】重点!多图演示红黑树的概念及插入操作(附考研模拟题)

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

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

相关文章

Salesforce官方_中文学习、考证资源

Salesforce将Trailhead描述为学习热门技能的有趣且免费的平台。该平台有助于缩小技能差距&#xff0c;是所有Salesforce用户的宝藏资源。 Trailhead适合所有学习者。它涵盖了适用于Salesforce任何角色的主题和学习模块&#xff0c;从管理员、开发人员、销售主管到最终用户。学…

数据库基础篇 《12.MySQL数据类型精讲》

1. MySQL中的数据类型 2. 整数类型 2.1 类型介绍 整数类型一共有 5 种&#xff0c;包括 TINYINT、SMALLINT、MEDIUMINT、INT&#xff08;INTEGER&#xff09;和 BIGINT。 它们的区别如下表所示&#xff1a; 2.2 可选属性 整数类型的可选属性有三个&#xff1a; 2.2.1 M …

【Python】【进阶篇】3、Django ORM模块精讲

目录 3、Django ORM模块精讲1. 什么是 ORM&#xff1f;2. Django中定义数据表1) 模型类2) 定义数据表 3. ORM 管理器对象4. ORM优势与不足 3、Django ORM模块精讲 Django 框架向我们提供了丰富的模块&#xff0c;避免程序员在开发的过程中重复“造轮子”&#xff0c;提高了开发…

Docker开发基础使用(针对开发者足够)

一.Docker概述 容器就是虚拟化吗&#xff1f; 是&#xff0c;但也不竟然。我们用一种简单方式来思考一下&#xff1a; 虚拟化使得许多操作系统可同时在单个系统上运行。 容器则可共享同一个操作系统内核&#xff0c;将应用进程与系统其他部分隔离开。 这意味着什么&#xf…

Kotlin 用于数据科学的基础库(深度学习、数据挖掘)

Kotlin 用于数据科学 从构建数据流水线到生产机器学习模型&#xff0c; Kotlin 可能是处理数据的绝佳选择&#xff1a; Kotlin 简洁、易读且易于学习。静态类型与空安全有助于创建可靠的、可维护的、易于故障排除的代码。作为一种 JVM 语言&#xff0c;Kotlin 提供了出色的性…

机器视觉工程师买车就买“宝马”车-德国制造-世界精工

世界离开德国,整个地球的制造业将会落后五百年。 说起德国制造 在光学领域最牛的卡尔蔡司公司是制造相机镜头的世界级企业。,在机器视觉行业里面公司Mvtec,我们机器视觉工程师大多数用的halcon,就是来自于德国Mvtec,电气工程师使用的西门子PLC,西门子是是全球电子电气工程及…

兼容性测试用例

兼容性测试用例 兼容性测试是软件测试中非常重要的一块&#xff0c;它主要测试两个方面&#xff1a; 1.同一软件系统&#xff0c;不同版本之间的兼容性 在实际项目中&#xff0c;我们会遇到多种不同版本的软件系统&#xff0c;比如 Windows和 Linux&#xff0c;甚至还有 Unix、…

操作系统原理 —— 什么是进程?进程由什么组成?有什么特征?(六)

进程的概念 在我小时候&#xff0c;刚刚接触电脑的时候&#xff0c;只会在浏览器中输入 4399 搜索小游戏玩一玩&#xff0c;到后来&#xff0c;我学会了安装游戏&#xff0c;然后知道安装完成之后&#xff0c;找到对应的 .exe 的图标就可以运行游戏。 好&#xff0c;那么什么…

2.数据库开发

二.数据库开发 1.开发数据库流程 2.数据库,数据表,数据字段的命名 3.数据库字符集和排序规则设置 4.数据表的引擎选择 二.数据库开发 1.开发数据库流程 ①建立数据库

虚幻图文笔记:面部动画基本原理以及在UE5中如何导入面部动画

0. 面部动画的基本原理 之前做过的项目没有涉及过面部动画&#xff0c;所以最这方面不是很了解&#xff0c;一直以为面部动画也是通过骨骼来驱动的&#xff08;理论上用骨骼驱动当然也是可以的&#xff09;&#xff0c;但很多时候面部动画更多是使用Morph Target&#xff08;有…

SLAM论文速递【SLAM—— PLD-SLAM:一种基于点线特征的室内动态场景RGB-D SLAM新方法—4.23(1)

论文信息 题目&#xff1a; PLD-SLAM:A New RGB-D SLAM Method with Point and Line Features for Indoor Dynamic Scene PLD-SLAM:一种基于点线特征的室内动态场景RGB-D SLAM新方法论文地址&#xff1a; https://www.mdpi.com/2220-9964/10/3/163发表期刊&#xff1a; ISPR…

MySQL数据落盘原理(redo、undo、binlog、2PC、double write等。)

文章目录 前言一、架构图1、MySQL架构图2、InnoDB架构图 二、落盘分析1.第一阶段2.第二阶段3.第三阶段4.第四阶段5.第五阶段6.第六阶段 前言 在上一章中我们聊到了事务有四大特性&#xff1a;原子性、一致性、隔离性、持久性。本篇文章就持久性重点聊一下&#xff0c;在高性能…

离子交换法处理含铬废水

含铬废水是从哪里来的&#xff1f; 含铬废水来自&#xff1a;冶金、化工、矿物工程、电镀、制铬、颜料、制药、轻工纺织、铬盐及铬化物的生产等一系列行业&#xff0c;都会产生大量的含铬废水。 含铬废水危害有多大&#xff1f; 1、铬化合物具有致癌作用&#xff1b; 2、铬…

做SSM项目的步骤和优化

SSM框架整合 这里说的SSM整合&#xff0c;主要说的是Spring和mybatis之间的整合。因为spring和springMVC都是spring生态系统中的框架&#xff0c;所以spring和springMVC之间的整合是无缝的整合&#xff0c;即&#xff0c;我们在不知不觉中&#xff0c;其实spring和springMVC已…

【C++】list的使用

文章目录 1. list的使用1. 构造函数2.迭代器的使用和数据访问3. 容量相关4. 数据修改1.数据插入2. 数据删除 5.其他接口 1. list的使用 首先&#xff0c;在使用list之前&#xff0c;我们得先了解list到底是个什么东西&#xff0c;查看文档可以了解到&#xff0c;list的底层是一…

使用EasyExcel导出模板并设置级联下拉及其原理分析

一、概述 项目中有时会遇到需要导出一个Excel模板&#xff0c;然后在导出的Excel中填充数据&#xff0c;最终再调用接口批量把Excel中的数据导入到数据库当中的需求。 其中级联下拉选择&#xff0c;手机号校验&#xff0c;性别校验等都是比较常见的校验。 这里就已上面三种情…

县级医院手术麻醉管理系统源码 医院手麻系统源码 C/S架构 系统成熟稳定完整二次开发

医院手麻系统详细功能介绍和说明&#xff1a; ▶手术管理功能包括&#xff1a;手术申请、手术安排、查看手术申请单、手术通知单、填写病人术前会诊记录、谈话记录、麻醉记录、手术记录、附加手术、术后信息及手术回顾等功能。 ▶手术麻醉管理系统包括&#xff1a;手术申请、…

openEuler 欧拉 安装Oracle19c数据库RPM包安装

一、准备工作 将安装部署包上传到服务器上&#xff0c;我安装包放到/home目录下 二、安装依赖包 yum -y install binutils compat-libcap1 compat-libstdc-33 compat-libstdc-33*.i686 elfutils-libelf-devel gcc gcc-c glibc*.i686 glibc glibc-devel glibc-devel*.i686 ksh…

“烧钱”的大模型:初探成本拆解与推理优化方法

编者按&#xff1a;大模型的成本问题一直以来是大家重点关注的问题&#xff0c;本文重点讨论了训练大型语言模型&#xff08;LLMs&#xff09;需要的成本&#xff0c;并简要介绍什么是LLM以及一些用于优化大模型推理表现的技术。 虽然很难准确预测LLMs未来会怎么发展&#xff0…

热血

周五的晚上&#xff0c;决定去看「灌篮高手」电影了。 那还是很多年以前&#xff0c;樱木双手插进裤腰歪头扭嘴吹着口哨&#xff0c;那不羁的样子像极了一只从上往下看的沙雕。 而全国赛的樱木&#xff0c;多少是成熟了很多&#xff0c;是会说一些犯二的话&#xff0c;会和流川…