HashMap底层源码解析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


1.哈希表(Hash)

1.1 哈希表概述

​ 在看HashSet集合前,先来了解一下哈希表。散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

​ 那么在Java中,哈希表是如何构建的的呢?

1.2 哈希表的结构

底层结构:

JDK1.8之前:数组+链表

JDK1.8之后:数组+链表+红黑树

底层结构图:

在这里插入图片描述

1.3 哈希表添加元素的步骤(方法)

1.计算添加元素(新元素)的哈希值,计算哈希值的过程称为哈希算法。

Java中计算对象的哈希值是通过,hashCode()方法。
    1.类中未重写hashCode方法,其计算出的哈希值是对象的内存地址值。
    2.从写hashCode方法,其值就按重写的规则进行计算。

2.计算元素在数组的位置:通过计算出添加元素(新元素)的哈希值与数组的长度进行取模运算,得出的值为元素在数组中的位置(索引值)。

3.添加操作(重点是去重操作):判断计算出的索引位置上是否有元素内容。

  • a.若该索引位置上的元素内容为null,则直接添加元素。
  • b.若该索引位置上有元素,则要添加的元素(新元素)要与索引位置对应链表中所有的元素进行一一比较。如果没有相同的元素就在链表的末尾进行添加元素。如果有重复的元素,就进行去重操作(不添加该元素)。去重操作:新元素.equals(老元素)

1.4 Java中哈希表底层扩容原理

1.初始化时,内存会默认创建一个长度为16,加载因子(阈值)为0.75的数组。

加载因子:数组扩容的时机
数组扩容的时机 = 哈希表中的元素节点个数 > 数组的长度 × 加载因子(阈值)

2.当哈希表中的元素节点个数达到阈值(加载因子)时,数组就会扩容。数组每次扩容的容量都是原先的2倍。并且哈希表中所有的元素节点都会重新计算在其数组中的位置(索引值)。

3.在JDK1.8版本之后,有引入了红黑树结构,用来提升哈希表的检索效率。

引入转换规则:当单个链表上的节点个数大于等于8且底层数组长度大于64时,链表就转换成红黑树结构。

4.在JDK1.8版本之前,当数组中某一个索引对应的值为null时,数组就不会扩容。JDK1.8之后,就取消了该机制。

2.HashMap源码解析

2.1 HashMap相关概述

HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。首先,先来看看注释中的解释。

对注释进行分析

-- 第一段注解:
	Hash table based implementation of the Map interface.
	此类实现了Map接口
	This implementation provides all of the optional map operations, and permits null values and the null key. 
	此实现类提供了可选的Map相关操作,键可以为空,值可以为空
	(The HashMap class is roughly equivalent to Hashtable, except that it isunsynchronized and permits nulls.) 
	这个HashMap类与Hashtable类大致相似,只是HashMap类是非同步的集合(线程不安全集合),且键和值允许为空。
	This class makes no guarantees as tothe order of the map; in particular, it does not guarantee that the orderwill remain constant over time.
     此map类不保证存取的顺序,并且不保证该类的存储顺序会不会随着时间的推移而改变。
        
-- 第二段注解
	This implementation provides constant-time performance for the basicoperations (get and put), assuming the hash function disperses the elements properly among the buckets. 
    这个实现为基本操作(get和put)提供了固定时间的性能,前提是哈希函数将元素适当地分散到存储桶中。 
        
    Iteration over collection views requires time proportional to the "capacity" of the HashMap instance(the number of buckets) plus its size (the number of key-value mappings).  
    遍历集合视图所需的时间与HashMap实例的“容量”(桶的数量)加上它的大小(键值映射的数量)成正比。
        
    Thus, its very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
     因此,如果迭代性能很重要,那么初始容量不要设置得太高(或者负载因子太低)是非常重要的。
 
-- 第三段注解
	An instance of HashMap has two parameters that affect its performance: initial capacity and load factor.  	  HashMap实例有两个参数会影响其性能:初始容量和加载因子。
        
    The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created.  
    容量是哈希表中桶的数量,初始容量是哈希表创建时的容量。  
        
    The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.  
	负载因子是在哈希表容量自动增加之前,允许哈希表达到的满度的度量。 
        
    When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
    当哈希表中的条目数量超过负载因子和当前容量的乘积时,哈希表将被重新哈希(即重新构建内部数据结构),这样哈希表的存储桶数量大约是原来的两倍。    

-- 第四段注解
    As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. 
    一般来说,默认的负载因子(.75)在时间和空间成本之间提供了一个很好的权衡 
        
    Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). 
    较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括get和put)The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. 
    在设置map的初始容量时,应该考虑map中预期的条目数量及其负载因子,以尽量减少rehash操作的数量。
        
    If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.
	如果初始容量大于最大条目数除以负载因子,则不会发生重hash操作。

对源码注释的总结:

-- 第一段:再说HashMap集合的相关特点
    1.Map接口的实现类,并实现了Map接口的所有方法。
    2.集合的key()Value()都可以为空,但是key()只能有一个为空,Value()可以有多个为空。
    3.HashMap是线程不安全集合,也就是没有synchronized同步操作。
    4.HashMap是存取无序的集合。
-- 第二、三、四段都是关于集合的扩容原理
    1.初始化时,内存会默认创建一个长度为16,加载因子(阈值)0.75的数组。
    2.当 哈希表中的元素节点个数 大于 数组的长度 × 加载因子(阈值)时就扩容
    3.数组每次扩容的容量都是原先的2倍。
    4.元素的去重操纵。(重点关注)

其实总的来说,HashMap 的大致结构如下图所示,其中哈希表是一个数组,我们经常把数组中的每 一个节点称为一个桶,哈希表中的每个节点都用来存储一个键值对。在插入元素时, 如果发生冲突(即多个键值对映射到同一个桶上)的话,就会通过链表的形式来解 决冲突。因为一个桶上可能存在多个键值对,所以在查找的时候,会先通过 key 的哈希值先定位到桶,再遍历桶上的所有键值对,找出 key 相等的键值对,从而来获 取 value。

在这里插入图片描述

2.2 HashMap中相关属性

再来看看 HashMap 类中包含了哪些重要的属性,这对下面介绍 HashMap 方法的实 现有一定的参考意义。

// DEFAULT_INITIAL_CAPACITY : 初始化底层数组的长度 1 << 4 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// MAXIMUM_CAPACITY 最大容量,如果隐式指定了更高的值则使用 : 1 << 30 = 1072741824
static final int MAXIMUM_CAPACITY = 1 << 30;

// DEFAULT_LOAD_FACTOR:默认的加载因子  0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// TREEIFY_THRESHOLD:是否转换成树结构的临界值 8
static final int TREEIFY_THRESHOLD = 8;

// UNTREEIFY_THRESHOLD 恢复链式结构的临界值为 6
static final int UNTREEIFY_THRESHOLD = 6;

// MIN_TREEIFY_CAPACITY 当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采取扩容来尝试减少冲突
static final int MIN_TREEIFY_CAPACITY = 64;

// table :哈希表也就就是存储链式结构的底层数组。
transient Node<K,V>[] table;

// entrySet 用来保存Map中的键值对的集合。常用于遍历操作
transient Set<Map.Entry<K,V>> entrySet;

// size : 集合的容量,实际键值对的个数
transient int size;

// modcount :哈希表被修改的次数,也可以理解为扩容的次数
transient int modCount;
 
// 它是通过 capacity*load factor 计算出来的,当 size 到达这个值时,就会进行扩容操作
int threshold;

// loadFactor:负载因子/加载因子
final float loadFactor;

下面是 Node 节点类的定义,它是 HashMap 中的一个静态内部类,哈希表中的每一个 节点都是 Node 类型。我们可以看到,Node 类中有 4 个属性,其中除了 key 和 value 之外,还有 hash 和 next 两个属性。hash 是用来存储 key 的哈希值的,next 是在构建链表时用来指向后继节点的。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 用来存储key(键)的哈希值的
    final K key; // 键,final修饰,一旦赋值不再发生变化
    V value;	// 值,可以发生改变
    Node<K,V> next; // 指向下一个节点元素

    // 构造方法,创建节点对象,并初始化赋值
    // 因为有带参构造,所以无法用默认构造初始化Node节点对象
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    // 获取节点的key键值
    public final K getKey()        { return key; }
    // 获取节点的value值
    public final V getValue()      { return value; }
    // 重写toString方法,键值对展示
    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;
    }

    // 判断两个节点对象是否相同
    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;
    }
}

2.3 HashMap中的构造方法

无参构造

public HashMap() {
    // 使用默认初始容量构造一个空的HashMap(16)和默认负载因子(0.75)。
	this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

双参数构造方法

// 双参构造,initialCapacity:初始集合的容量,loadFactor :负载因子
public HashMap(int initialCapacity, float loadFactor) {
    // 提高效率,判断传入的初始容量是否为正数,若小于0则抛出IllegalArgumentException异常
	if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    
    // 判断初始容量是否大于,集合的最大设定容量,若为true,则初始容量等于最大容量
	if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
    
    // 校验负载(加载)因子是否为nan值或小于0的值,若是抛出IllegalArgumentException异常
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    
    // 走到最后,初始化加载因子
	this.loadFactor = loadFactor;
    
    // 数组按initialCapacity调整容量的大小,该方法是用来保证每次扩容都是原来的2的倍数
	this.threshold = tableSizeFor(initialCapacity);
}

// 调整大小方法:返回给定目标容量的2次方大小。返回给定目标容量的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;
}

单参数构造方法

// 其实与就是调用双参数构造方法,只不过这里的负载(加载)因子直接为默认值
public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 创建一个足够容量的集合,来存储m集合,其默认负载(加载)因子为0.75
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

putMapEntries方法

// 实现了Map.putAll和Map构造函数
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    // 接收m集合中的容量大小
    int s = m.size();
    // 判断容量是否大于0,若大于0,证明m集合中有元素
	if (s > 0) {
        // 判断table哈希表底层数组是否为空
    	if (table == null) { 
            // 计算扩容时机capacity * loadfactor
        	float ft = ((float) s / loadFactor) + 1.0F;
            int t = ((ft < (float) MAXIMUM_CAPACITY) ?(int) ft : MAXIMUM_CAPACITY);
            
            // 判断哈希表底层数组是否需要扩容
            if (t > threshold)
            	threshold = tableSizeFor(t);
            
    } 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);
	}
}

总结分析:

1.默认无参构造,会创建一个初始容量为16,默认负载(加载)因子为0.75的集合。

2.双参构造,创建一个容量为initialCapacity,加载因子为loadFactor的集合

3.单参构造

  • 第一种,创建容量为initialCapacity的集合,其底层还是调用的双参构造方法,只不过负载因子使用了默认负载因子0.75
  • 第二种,创建一个包含m集合的新集合。其底层是获取了m集合的长度,然后利用m集合长度创建新集合,并循环将老集合中的元素赋值给新的集合。

2.4 put(K key,V value)添加操作

put 方法的具体实现也是在 putVal 方法中,所以我们重点看下面的 pu tVal 方法

put方法

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

putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; // 哈希表
    Node<K,V> p; // 节点元素
    int n, i; // 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;
        
        //如果桶上节点的 key 与当前要添加节点的 key 重复
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;// 保存要添加的元素节点
        
        //如果是采用红黑树的方式处理冲突,则通过红黑树的 putTreeVal 方法去插入这个键值对
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        
        else { // 否则是通过传统的链表添加
            
            // 采用循环遍历的方式,判断链中是否有重复的 key
            for (int binCount = 0; ; ++binCount) {
                // 到了链尾还没找到重复的 key,则说明 HashMap 没有包含该键
                if ((e = p.next) == null) {
                    // 创建一个新节点插入到尾部
                    p.next = newNode(hash, key, value, null);
                    //判断是否要转成红黑树结构,如果链的长度大于 TREEIFY_THRESHOLD 这个临界值,则把链变为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                
                // 循环判断,到了这一步,证明找到了重复的 key
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 这里表示在上面的操作中找到了重复的键,所以这里把该键的值替换为新值
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    // 集合底层更改的次数
    ++modCount;
    
    // 判断是否是否需要扩容
    if (++size > threshold)
        resize();
    
    afterNodeInsertion(evict);
    
    return null;
}		

总结:

  1. 先计算新添加的元素哈希值,看看是映射到那一个桶(数组的索引位置)。
  2. 判断是否有冲突。(冲突:即多个键值对映射到同一个桶上)
    • 若无冲突,该桶上(计算的索引位置)的内容为空,则直接创建节点,添加元素
    • 若有冲突(哈希表上的数组索引对应的位置有值)
      • 第一种:按照红黑树原则来处理冲突,则通过红黑树的方式添加元素
      • 第二种:若按传统的链表来解决冲突,按传承的链表方式添加元素。如果链的长度到达临界值,则把链转变为红黑树。
  3. 若有相同的键,则改其键对应的值。
  4. 如果 size 大于阈值,则进行扩容。

2.5 get操作的源码分析

get 方法主要调用的是 getNode 方法,所以重点要看 getNode 方法的 实现

get方法

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

getNode方法

final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; 
    Node<K,V> first, e; 
    int n; 
    K k;
    
    //如果哈希表不为空 && key 对应的桶上不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
    	
        //是否直接命中
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
        	return first;
        
        //判断是否有后续节点
        if ((e = first.next) != null) {
            
            //如果当前的桶是采用红黑树处理冲突,则调用红黑树的 get 方法去获取节点
        	if (first instanceof TreeNode)
               return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            
            //不是红黑树的话,那就是传统的链式结构了,通过循环的方法判断链中是否存在该 key
            do {
               if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                   return e;
            } while ((e = e.next) != null);
        }
    }
	return null;
}

总结分析:

  1. 计算哈希值,通过哈希值来判断在那个桶上(数组索引位置)。
  2. 此时桶上的key就是要查找的key,直接命中,找到。也就说,索引指向的第一个头节点中的key是否是要找的key,是,则直接命中
  3. 若不是,查找后续节点
    • 若是采用红黑树解决的冲突,则采用红黑树方式查找
    • 若采用链表解决的冲突,则采用链表的方式查找

2.6 remove移除一个元素的操作

理解了 put 方法之后,remove 已经没什么难度了,所以重复的内容就不再做详细介绍了。

remove方法

public boolean remove(Object key, Object value) {
	return removeNode(hash(key), key, value, true, true) != null;
}

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;
        //如果当前 key 映射到的桶不为空
        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;
            //如果当前 key 映射到的桶不为空
            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);
                }
            }
            //比对找到的 key 的 value 跟要删除的是否匹配
            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;
    }

2.7 hash方法

在 get 方法和 put 方法中都需要先计算 key 映射到哪个桶上,然后才进行之后的操作, 计算的主要代码如下:(n - 1) & hash

上面代码中的 n 指的是哈希表的大小,hash 指的是 key 的哈希值,hash 是通过下面 这个方法计算出来的,采用了二次哈希的方式,其中 key 的 hashCode 方法是一个 native 方法:

static final int hash(Object key) {
	int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个 hash 方法先通过 key 的 hashCode 方法获取一个哈希值,再拿这个哈希值与它 的高 16 位的哈希值做一个异或操作来得到最后的哈希值,计算过程可以参考下图。 为啥要这样做呢?注释中是这样解释的:如果当 n 很小,假设为 64 的话,那么 n-1 即为 63(0x111111),这样的值跟 hashCode()直接做与操作,实际上只使用了哈希 值的后 6 位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成冲 突了,所以这里把高低位都利用起来,从而解决了这个问题.

在这里插入图片描述

正是因为与的这个操作,决定了 HashMap 的大小只能是 2 的幂次方,想一想,如果 不是 2 的幂次方,会发生什么事情?即使你在创建 HashMap 的时候指定了初始大小, HashMap 在构建的时候也会调用下面这个方法来调整大小:

// 调整大小方法:返回给定目标容量的2次方大小。返回给定目标容量的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;
}

这个方法的作用看起来可能不是很直观,它的实际作用就是把 cap 变成第一个大于 等于 2 的幂次方的数。例如,16 还是 16,13 就会调整为 16,17 就会调整为 32。

2.8 resize 方法

HashMap 在进行扩容时,使用的 rehash 方式非常巧妙,因为每次扩容都是翻倍,与 原来计算(n-1)&hash 的结果相比,只是多了一个 bit 位,所以节点要么就在原来 的位置,要么就被分配到“原位置+旧容量”这个位置。

例如,原来的容量为 32,那么应该拿 hash 跟 31(0x11111)做与操作;在扩容扩到 了 64 的容量之后,应该拿 hash 跟 63(0x111111)做与操作。新容量跟原来相比只 是多了一个 bit 位,假设原来的位置在 23,那么当新增的那个 bit 位的计算结果为 0 时,那么该节点还是在 23;相反,计算结果为 1 时,则该节点会被分配到 23+31 的 桶上。

正是因为这样巧妙的 rehash 方式,保证了 rehash 之后每个桶上的节点数必定小于等 于原来桶上的节点数,即保证了 rehash 之后不会出现更严重的冲突。

 	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);
        }
        //新的 resize 阈值
        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 { //如果采用链式处理冲突
                        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;
    }

值得注意的是,这里是节点的键key的个数超过阈值,则进行扩容。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMapJava中常用的一种数据结构,它底层采用的是哈希表的实现方式。下面是HashMap底层源码分析: 1. HashMap数据结构 HashMap数据结构是一个数组,数组中的每个元素是一个链表,链表中存放了哈希值相同的键值对。当发生哈希冲突时,新的键值对将会添加到链表的末尾。在查找键值对时,首先根据键的哈希值在数组中定位到对应的链表,然后再在链表中查找键值对。这种实现方式的时间复杂度为O(1),但是在发生哈希冲突时,链表的长度会变长,查找效率也会降低。 2. HashMap的put方法 当向HashMap中添加键值对时,首先会计算键的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置为空,则直接将键值对添加到该位置;否则,需要遍历链表,查找是否已经存在相同的键,如果存在,则将旧的值替换为新的值;如果不存在,则将新的键值对添加到链表的末尾。 3. HashMap的get方法 当从HashMap中获取键值对时,首先计算键的哈希值,然后根据哈希值在数组中定位到对应的链表。接着遍历链表,查找是否存在相同的键,如果存在,则返回对应的值;如果不存在,则返回null。 4. HashMap的扩容机制 当HashMap中的元素个数超过数组长度的75%时,会自动扩容。扩容时,会将数组长度扩大一倍,并将原来的键值对重新分配到新的数组中。重新分配时,需要重新计算键的哈希值和在新数组中的位置。这个过程比较耗时,但是可以避免链表过长导致的查找效率降低。 5. HashMap的线程安全性 HashMap是非线程安全的,因为在多线程环境下,可能会存在多个线程同时对同一个链表进行操作的情况,从而导致数据不一致。如果需要在多线程环境下使用HashMap,可以使用ConcurrentHashMap,它是线程安全的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值