面试题22——HashMap的实现原理

本文深入解析HashMap的内部实现,包括其基于数组和链表的结构、存储和读取机制、解决hash冲突的方法,以及扩容策略。强调了HashMap允许使用null键和值的特点,并详细解释了hash算法在提高查询效率中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap概述:

    HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

    在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

       HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

HashMap实现存储和读取:

1)存储

public V put(K key, V value) {
    // HashMap允许存放null键和null值。
    // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
    if (key == null)
        return putForNullKey(value);
    // 根据key的keyCode重新计算hash值。
    int hash = hash(key.hashCode());
    // 搜索指定hash值在对应table中的索引。
    int i = indexFor(hash, table.length);
    // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
    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;
        }
    }
    // 如果i索引处的Entry为null,表明此处还没有Entry。
    modCount++;
    // 将key、value添加到i索引处。
    addEntry(hash, key, value, i);
    return null;
}

       根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

1 static int hash(int h) {
2     h ^= (h >>> 20) ^ (h >>> 12);
3     return h ^ (h >>> 7) ^ (h >>> 4);
4 }

       hash(int h)方法根据key的hashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。

       我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。

       根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

       通过这种方式就可以高效的解决HashMap的冲突问题。

2)读取

 1 public V get(Object key) {
 2     if (key == null)
 3         return getForNullKey();
 4     int hash = hash(key.hashCode());
 5     for (Entry<K,V> e = table[indexFor(hash, table.length)];
 6         e != null;
 7         e = e.next) {
 8         Object k;
 9         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
10             return e.value;
11     }
12     return null;
13 }

       从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

3)总结

       HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

HashMap的resize:

      当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

       那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

总结:

  1. 利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
  4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
### HashMap实现原理面试题解析 #### 一、HashMap 的基本概念 `HashMap` 是基于哈希表的键值对存储结构,在 Java 中实现了 `Map` 接口。它允许存储 `null` 键和多个 `null` 值[^1]。 #### 二、底层数据结构 在 Java 1.8 及之后版本中,`HashMap` 使用了一种混合的数据结构来提高性能: - **数组**:作为主要容器,用于存储节点(Node)。 - **链表**:当发生哈希冲突时,使用链表解决冲突。 - **红黑树**:当某个桶中的链表长度超过阈值(默认为 8),该链表会被转化为一棵平衡的红黑树以优化查找效率[^1]。 这种设计使得 `HashMap` 能够在大多数情况下保持 O(1) 时间复杂度的操作(插入、删除、查询),即使在极端情况下的时间复杂度也能通过红黑树降级到 O(log n)[^1]。 #### 三、核心方法分析 以下是几个重要的操作及其内部机制: ##### 1. 插入元素 (`put(K key, V value)` 方法) - 计算键的哈希码并映射到对应的桶索引位置。 - 如果目标位置为空,则直接创建一个新的节点存入。 - 如果目标位置已有节点,则判断是否存在相同键的情况: - 若存在相同的键,则更新其对应值。 - 否则按顺序追加至链表末尾或插入到红黑树中[^1]。 ```java public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } ``` ##### 2. 查询元素 (`get(Object key)` 方法) - 根据给定键计算出相应的哈希值找到所在槽位。 - 对于单个节点或者短小精悍型列表形式下可以直接定位;而对于较长链条以及转换成树状结构后需进一步深入比较直至匹配成功为止。 ```java public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } ``` ##### 3. 删除元素 (`remove(Object key)` 方法) - 定位指定项所在的分组区域并通过逐层递归的方式移除特定条目同时调整关联关系确保整体连贯性不受破坏。 ```java final Entry<K,V> removeEntryForKey(Object key) { int hash = (key == null) ? 0 : hash(key.hashCode()); for (Entry<K,V>[] tab = table; ; ) { ... } } ``` #### 四、扩容机制 为了维持高效的访问速度,`HashMap` 设定了初始容量与负载因子两个参数控制何时触发扩展动作。一旦实际装载量达到预设界限就会启动重新分配过程——即新建更大规模的新表格并将旧有记录迁移过去完成升级换代工作流程。 #### 五、线程安全性问题 需要注意的是标准版 `HashMap` 并未提供同步保护措施因此不适合应用于多线程环境当中。如果确实临此类需求可以考虑采用其他替代方案比如 `ConcurrentHashMap` 或者借助外部锁机制加以防护处理[^1]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值