Java基础三:HashMap

77 阅读7分钟

以下是对给定关于HashMap底层原理和数据结构描述的详细梳理:

1. HashMap概述

  • 定义:HashMap是一个采用哈希表实现的键值对集合,它继承自AbstractMap并实现了Map接口。
  • 底层结构:HashMap的底层由数组和链表+红黑树组成。

2. Hash原理

  • 目的:通过一次计算大幅度缩小查找范围,尽可能减少冲突。
  • 哈希函数:是一种映射关系,根据数据的关键词(key),通过一定的函数关系,计算出该元素存储位置的函数。

3. 常见Hash函数

  1. 随机数法:通过随机数生成算法为每个关键字生成一个随机数作为哈希值,这种方法简单但哈希冲突难以控制。

  2. 直接定址法:直接以关键字或关键字的某个线性函数值为哈希地址,即H(key) = a*key + b,其中a和b为常数。适用于关键字分布基本连续的情况。

  3. 除留余数法:取关键字被某个不大于哈希表长度m的数p除后所得的余数作为哈希地址,即H(key) = key % p。p一般选择小于或等于m的最大素数。

  4. 数字分析法:分析关键字集合,在关键字中选取分布均匀的若干位或它们的组合作为哈希地址。适用于关键字位数比哈希地址位数多,且关键字中某些位分布较均匀的情况。

4. 冲突处理

  • 当通过hashCode()计算出的地址对应数组下标已有值时,发生哈希冲突。
  • 解决方法:将新key与链表中已有节点的key进行equals比较。

5. 成员变量

  • 初始容量:16(242^4
  • 默认加载因子:0.75
  • 最大容量2302^{30}
  • 键值对数量size
  • 加载因子loadFactor
  • 链表数组Node[] table

6. Map.put(k,v)原理

  1. 将键值对封装到Node对象中。
  2. 计算key的hashCode(),并将其转换为数组下标。
  3. 如果没有值,则在对应位置添加新的Node
  4. 如果已有值,则进行equals比较:
    • 如果相等,则覆盖旧值。
    • 如果不相等,则添加到链表末尾。
  5. 如果单条链表的长度大于8,则转换为红黑树。

7. Map.get(k)原理

  1. 计算key的hashCode(),并将其转换为数组下标。
  2. 如果没有值,则返回null
  3. 如果有链表,则遍历链表,使用equals比较key。
  4. 如果使用红黑树,则通过instanceof和比较操作查找key。
  5. 如果找到匹配的key,则返回对应的value;否则返回null

8. 扩容机制

  • 扩容机制详细解释可参见:链接
  • 当HashMap中的元素数量超过加载因子与当前容量的乘积时,会触发扩容。

9. 安全的HashMap

  • ConcurrentMap:提供了比HashMap更高的并发级别,适合多线程环境。

10. ArrayMap(非Java标准库中的HashMap,可能是特定环境下的实现)

  • 底层结构:使用两个数组。
    • mHashes:保存每一个key的hash值。
    • mArray:大小是mHashes的两倍,分别存储key和value。
  • 特点:这种结构在内存使用上可能比标准的HashMap更加紧凑,但牺牲了部分性能(如插入和查找速度)。

11. 操作原理

HashMap提供了查找、插入和删除操作,这些操作的核心机制依赖于哈希值和数组下标:

  • 查找:对于给定的键,首先计算其哈希值,通过哈希值定位到数组中的“桶”位置(即数组下标)。如果该位置存在链表或红黑树,则进一步遍历链表或搜索红黑树,使用equals方法比较键,找到对应的键值对。
  • 插入:插入新键值对时,同样先计算键的哈希值以定位到数组中的桶位置。如果该位置为空,则直接添加新的键值对。如果已存在链表或红黑树,则进行equals比较,若键已存在则覆盖旧值,否则将新键值对添加到链表末尾。当链表长度超过一定阈值(如JDK 1.8中为8),链表会转换为红黑树以提高搜索效率。
  • 删除:删除操作也是先通过哈希值定位到数组中的桶位置,然后遍历链表或搜索红黑树,找到要删除的键值对并移除。

12. 线程安全

HashMap本身不是线程安全的,即多个线程同时操作同一个HashMap实例时,可能会导致数据不一致的问题。如果需要在多线程环境下使用HashMap,可以考虑以下几种方案:

  • 使用Collections.synchronizedMap()方法包装HashMap,该方法返回一个线程安全的Map,但性能较低,因为每次操作都需要加锁。
  • 直接使用ConcurrentHashMap,它是专为并发环境设计的,提供了比Collections.synchronizedMap()更高的并发级别,无需外部同步。

13. Map.put(k,v)原理

当调用Map.put(k,v)方法时,执行以下步骤:

  • 将键值对封装成Node对象。
  • 计算键k的哈希值hashCode(k),并根据数组长度和哈希值计算出数组下标。
  • 查找该下标位置的元素:
    • 如果为空,则直接在该位置添加新的Node
    • 如果已存在链表或红黑树,则遍历链表或搜索红黑树,使用equals方法比较键:
      • 如果找到相等的键,则覆盖该键对应的值。
      • 如果没有找到相等的键,则将新的Node添加到链表末尾。如果链表长度超过阈值,则转换为红黑树。

14. Map.get(k)原理

当调用Map.get(k)方法时,执行以下步骤:

  • 计算键k的哈希值hashCode(k),并根据数组长度和哈希值计算出数组下标。
  • 查找该下标位置的元素:
    • 如果为空,则返回null
    • 如果存在链表或红黑树,则遍历链表或搜索红黑树,使用equals方法比较键:
      • 如果找到相等的键,则返回对应的值。
      • 如果链表遍历完毕或红黑树搜索完成仍未找到相等的键,则返回null

15为什么底层链表会转红黑树

红黑树是一种自平衡的二叉查找树,它通过一系列的性质(如节点是红色或黑色、根节点是黑色、每个叶子节点(NIL节点,空节点)是黑色、每个红色节点的两个子节点都是黑色、 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点)来维持树的平衡,以确保在最坏情况下基本保持对数时间复杂度执行基本动态集合操作(如搜索、插入、删除等)。

在Java的HashMap实现中,当某个桶(bucket)中的链表长度达到阈值(JDK 1.8之后默认为8)时,HashMap会进行扩容操作,但如果此时HashMap的容量(capacity)已经达到了最大容量(默认为2的30次方),那么HashMap会将这些链表转换为红黑树来优化性能。

为什么是红黑树?

  1. 提高搜索效率:在链表很长的情况下,搜索某个元素的时间复杂度是O(n),其中n是链表的长度。而红黑树的搜索时间复杂度是O(log n),即使在数据量很大的情况下,也能保持较高的搜索效率。

  2. 优化空间利用:虽然红黑树在结构上比链表复杂,需要额外的空间来存储颜色信息和维持树的平衡,但当链表过长时,转换为红黑树可以有效减少因链表过长而带来的空间浪费。同时,由于红黑树的高度相对较低,可以有效减少因扩容操作带来的性能损失。

  3. 平衡性:红黑树通过其自平衡特性,保证了树的高度相对较低,从而避免了在极端情况下因链表过长而导致的性能问题。

为什么不总是使用红黑树?

虽然红黑树在查询效率上优于链表,但其维护成本也相对较高。在链表较短时,使用链表可以节省空间并降低维护成本。此外,HashMap的设计目标是提供高效的键值对映射,而在大多数情况下,通过合理的哈希函数和足够的容量,HashMap中的链表长度都会保持在一个较短的范围内,因此无需总是使用红黑树。

综上所述,HashMap在链表长度超过一定阈值时转换为红黑树,是为了在极端情况下优化性能,同时保持整体实现的简洁和高效。