HashMap

底层实现

  • HashMap内部维护一个节点数组,这是主体结构,数组的每个位置被称为“桶”。
    • 数组:数组是哈希表的基础。当创建一个HashMap时,系统会初始化一个具有一定容量的数组。每个数组元素可以存储一个键值对节点。
    • 链表:当两个或多个不同的Key经过哈希计算后,得到了相同的数组索引时,就会发生“哈希冲突”。为了解决这个问题,HashMap采用了“链地址法”。即将所有哈希冲突的键值对以链表的形式存储在同一个桶中。
    • 红黑树:在早期的Java版本中,即使哈希冲突严重,也只会使用链表。这会导致在极端情况下(所有键都映射到同一个桶),HashMap的性能会从O(1)退化到O(n),相当于一个链表。为了优化这个问题,当一个桶中的链表长度超过一个特定阈值时,并且哈希表的总容量大于等于64时,这个链表就会被转化成一个红黑树。红黑树是一种自平衡的二叉查找树,其查找、插入和删除的时间复杂度都能保持在O(logn),极大地提升了在哈希冲突严重时的性能。

关键工作流程

put(key,value)
  1. 计算哈希值:调用key的hashcode()方法得到一个原始哈希码。为了让键的分布更均匀,减少哈希冲突,HashMap还会对这个哈希码进行二次处理。
  2. 计算数组索引:使用处理后的哈希值与数组的长度减一进行按位与运算,得到该键值对应在数组中的存储位置(桶的索引)。等效于取模运算,但位运算的效率更高。
  3. 处理存储逻辑:
  • 桶为空:如果计算出的索引位置没有任何元素,直接创建一个新的节点并存入该节点。
  • 桶不为空(发生冲突):
    • 键已存在:遍历桶中的链表或红黑树,通过equals()方法检查是否存在一个键与当前要插入的键完全相同。如果找到,就用新的value覆盖旧的value,并且返回旧值。
    • 键不存在:
      • 如果当前桶是链表结构,则将新的键值对节点添加到链表的末尾。在添加后,检查链表长度是否达到“树化”阈值,如果达到,则将链表转换为红黑树。
      • 如果当前桶已经是红黑树结构,则直接将新节点插入到红黑树。
get(key)
  1. 计算哈希和索引:对指定的key执行与put方法完全相同的哈希计算和索引定位过程,找到对应的桶。
  2. 查找目标节点:
  • 如果桶为空,直接返回null。
  • 如果桶不为空,遍历该桶的链表或在红黑树中进行搜索。
  • 在遍历/搜索过程中,首先比较哈希值,如果哈希值相同,再调用key的equals()方法进行精确匹配。
  • 如果找到完全匹配的节点,则返回其对应的value;如果遍历/搜索完整个桶都没有找到,则返回null。
容量与加载因子
  • HashMap 的性能受到两个重要参数的影响:
    • 初始容量:哈希表中桶的数量。
    • 加载因子:一个介于0.0 ~ 1.0的浮点数,表示哈希表在进行扩容之前可以达到的满度。
  • 扩容机制:当 HashMap 中存储的元素数量超过 容量*加载因子 这个阈值时,就会触发扩容操作。
  • 扩容过程
    • 创建新数组:创建一个容量是原数组两倍的新数组。
    • 重新计算和迁移:遍历旧数组中的所有键值对,为每个键值对重新计算其在新数组中的索引位置,然后将其迁移到新数组的对应位置。
    • 替换旧数组:将HashMap内部的table引用指向这个新数组。
  • 扩容是一个相对耗时的操作,以为它需要重新处理所有已存在的元素。因此,如果在创建HashMap时能预估到要存储的元素数量,通过构造函数指定一个合适的初始容量,可以有效减少或避免扩容操作,从而提升性能。
hashcode() 与 equals()的重要性
  1. equals()相等的对象,其hashCode()必须相等:保证在HashMap中能正确找到键的前提。
  2. hashCode()相等的对象,其equals()不一定相等:哈希冲突的根源。
  3. equals()相等的对象,其hashCode()尽可能不相等:有助于让键值对均匀地分布在数组的不同桶中,减少哈希冲突。

HashMap VS HashSet VS 数组

特性 数组 (Array T[]) ArrayList<T> (动态数组) HashSet<E> (集合) HashMap<K, V> (映射)
存储内容 相同类型的元素 相同类型的元素 唯一的元素 (Element) 唯一的键 + 值 (Key-Value)
访问方式 按整数索引 (Index)
myArray[i]
按整数索引 (Index)
myList.get(i)
按元素值 (Element)
mySet.contains(e)
按键 (Key)
myMap.get(key)
核心目的 存储有序数据,索引访问 存储有序数据,索引访问 保证元素唯一性,快速检查存在 建立键值映射,快速按键查找
性能 (平均) 访问: O(1)
查找: O(n)
访问: O(1)
查找: O(n)
末尾添加: O(1)
添加/删除/查找: O(1) 添加/删除/查找: O(1)
是否允许重复 允许 允许 不允许 Key 不允许
Value 允许
是否有序 有序 (按索引排序) 有序 (按插入顺序排序) 无序 (迭代顺序不保证) 无序 (迭代顺序不保证)
大小 固定 (创建时指定) 动态可变 动态可变 动态可变
底层实现 连续的内存空间 内部使用数组 (T[]) 内部使用 HashMap 数组 + 链表 + 红黑树
Null 值 允许多个 null 允许多个 null 只允许一个 null 只允许一个 null 键,多个 null
  • 优先使用数组
    • 需要一个简单地,通过下标(数字,字母也可)来访问或修改元素,保证插入顺序,允许重复元素
  • 优先使用HashSet
    • 保证元素不重复,快速检查某个元素是否存在于集合中,而不关心在哪里,不关心元素的存储顺序。
  • 优先使用HashMap
    • 存储键值对映射关系,根据一个key快速查找对应的value,不关心元素的存储顺序。

本站由 Xylumina 使用 Stellar 1.30.0 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本"页面"访问 次 | 总访问 次 | 总访客