全网最详细的Hashmap源码逐层剖析(一)

一.HashMap的基本结构

HashMap是基于哈希表(Hash Table)实现的数据结构。它通过哈希函数将键映射到数组中的位置,以实现高效的键值对存储和查找。以下是HashMap的主要组成部分:

主要组成部分

  1. 数组(Node<K,V>[] table)
    • 存储键值对的数组,每个元素是一个链表或红黑树的头节点。
    • 数组的每个位置被称为“桶”(bucket),每个桶存储一个或多个键值对。
  2. 链表(Node<K,V>)
    • 当多个键的哈希值相同时,这些键值对会存储在同一个链表中。
    • 链表用于解决哈希冲突,即不同的键映射到相同的数组位置。
  3. 红黑树
    • 自JDK 1.8开始,当链表长度超过一定阈值(默认是8)时,链表会转换为红黑树,以提高查询效率。
    • 当链表长度小于一定值(默认是6)时,红黑树会转换回链表。

Hashmap是如何创建这些部分的?

HashMap 属于 Map 接口的一种实现,其基本实现原理是拉链法

其内部主要包含了两个组成部分:数组table 和 桶(链表)bucket。

当对 HashMap 放入一个<key,value> 键值对时,会先对 key 调用 hashCode() 方法计算出一个哈希值,再通过一种散列函数将哈希值映射到 table 数组中的一个位置 index,随后将<key,value> 添加到 index 处的 bucket 中。

1. 数组(Node<K,V>[] table)

HashMap的主干是一个数组,数组中的每个元素被称为“桶”(bucket),用于存储键值对。数组的每个位置可以存储一个链表或红黑树的头节点。

数组的创建

数组是在HashMap的构造函数中初始化的。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

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

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

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

    // 存储键值对的数组
    transient Node<K,V>[] table;

    // HashMap中的键值对数量
    transient int size;

    // 构造函数
    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() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 加载因子
    }
}

我们对部分源代码进行讲解

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

HashMap类继承自AbstractMap并实现了Map接口,同时也实现了CloneableSerializable接口。这意味着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);
}

这个构造函数允许用户指定初始容量和加载因子。

  • initialCapacity: 初始容量。如果小于0,会抛出IllegalArgumentException。如果大于最大容量,则会被设置为最大容量。
  • loadFactor: 加载因子。如果小于等于0或不是一个数字,会抛出IllegalArgumentException

tableSizeFor这个方法用于计算大于等于给定容量的最小2的幂。它通过一系列的位运算将容量调整为接近的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;
}

键值对的插入

当创建数组完成后,我们向HashMap中插入一个键值对时,会触发一系列操作,包括计算哈希值、找到合适的数组索引、处理冲突等。

1. 计算哈希值

首先,HashMap会计算键的哈希值。HashMap使用一个改进的哈希函数来减少冲突: 

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

这个哈希函数通过将键的哈希码高位和低位混合,减少了哈希冲突的概率。

这是如何计算的呢?

  1. 获取原始哈希码
    h = key.hashCode();
  2. 右移16位
    h >>> 16
    这个操作将哈希码h的高16位移到低16位,使用的是无符号右移操作符>>>,这意味着高位用0填充。
    假设我们有一个键对象,其原始哈希码为h = 0b10101010101010101010101010101010(一个32位的二进制数),那么计算过程如下:
    h = 0b10101010101010101010101010101010

    h >>> 16 = 0b00000000000000001010101010101010

    h ^ (h >>> 16) = 0b10101010101010100000000000000000

通过这种方式,原始哈希码的高位和低位都参与了最终哈希值的计算。这种混合操作有助于减少哈希冲突,因为它使得哈希值的分布更加均匀,避免了由于哈希码的某些位不变而导致的冲突。

2. 定位数组索引

计算完哈希值后,HashMap使用哈希值来确定键值对在数组中的位置。具体来说,它通过对数组长度取模来确定索引: 

int index = (n - 1) & hash;

这里,n是数组的长度,hash是键的哈希值。

3. 处理冲突

如果两个不同的键计算出的数组索引相同,就会发生哈希冲突。HashMap使用链地址法(即链表)来处理冲突。在Java 8及以后,当链表长度超过一定阈值(默认是8)时,会将链表转换为红黑树,以提高查找效率。

4. 插入节点

找到合适的数组索引后,HashMap会将键值对插入到数组的对应位置。如果该位置已经有节点(即发生冲突),则会将新节点添加到链表或红黑树中。

5. 扩容和再哈希

HashMap中的元素数量超过一定阈值(由加载因子决定)时,会触发扩容操作。扩容时,HashMap会创建一个更大的数组,并将所有现有的键值对重新哈希到新数组中。这是一个比较耗时的操作,但它保证了哈希表的性能。

  • 34
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMapJava中的一个常用的集合类,用于存储键值对的数据结构。它的底层数据结构是数组、链表和红黑树。HashMap可以存储null的键和值。下面是对HashMap源码详细分析。 1. HashMap的特点:HashMap是无序的,不保证元素的顺序。它允许使用null作为键和值。HashMap不是线程安全的,如果在多线程环境下使用,需要进行额外的同步。 2. HashMap的构造方法: - HashMap():创建一个空的HashMap实例,默认初始容量为16,负载因子为0.75。 - HashMap(int initialCapacity):创建一个指定初始容量的HashMap实例。 - HashMap(int initialCapacity, float loadFactor):创建一个指定初始容量和负载因子的HashMap实例。 - HashMap(Map<? extends K, ? extends V> m):创建一个包含指定Map中所有键值对的HashMap实例。 3. HashMap的成员方法: - put(K key, V value):向HashMap中添加键值对。 - hash方法:计算键的哈希值。 - 扩容方法resize():当HashMap的大小达到阈值时,会自动扩容。 - 删除方法remove():根据键删除对应的键值对。 - 查找元素方法get():根据键获取对应的值。 4. HashMap的常见问题: - HashMap的容量为什么是2的幂次方?这是为了提高哈希函数的散列性能和数组的索引计算效率。 - 为什么负载因子默认是0.75?这是为了在保持较高的查找效率的同时,尽可能减少哈希冲突和扩容次数。 综上所述,HashMap是一种底层使用数组、链表和红黑树的存储方式的键值对集合类,并提供了一系列的方法来操作和查询数据。它的源码实现细节涉及到数组的扩容、哈希函数的计算、链表和红黑树的操作等。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [最详细Hashmap源码解析](https://blog.csdn.net/qq_45830276/article/details/126768408)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [HashMap源码解析](https://blog.csdn.net/weixin_46129192/article/details/123287837)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值