HashMap、LinkedHashMap、TreeMap

1、HashMap

  • 底层接口和原理HashMap实现了Map接口,并且其底层是基于哈希表(散列表/数组)的数据结构。它使用键(Key)对象的哈希码来确定键值对(Entry)存储的位置。在Java 1.8及以后,当链表长度大于一定阈值(默认是8)时,链表会被转换为红黑树来提高搜索效率。

  • 性质

    • 查询速度:理想情况下,HashMap可以提供接近O(1)的时间复杂度进行查找、插入和删除操作。但如果出现大量哈希冲突,性能可能会降低至O(n)(在Java 1.8中,由于链表转为红黑树,其性能会提升到O(log n))。
    • 顺序HashMap不保证元素的顺序,当进行插入和删除操作时,元素的顺序也可能发生变化。HashMap是无序的。
    • 并发性HashMap本身不是线程安全的,如果需要多线程并发访问,可以使用Collections.synchronizedMap方法来包装HashMap或者使用ConcurrentHashMap

2、LinkedHashMap

LinkedHashMap继承自HashMap并且实现了Map接口。其内部维护了一个双向链表,在哈希表的基础上保持了元素插入的顺序。因此,迭代访问LinkedHashMap时,元素将按照插入的顺序(或者如果指定了access-order则按照最近最少使用顺序)返回。

每个LinkedHashMap的条目(entry)除了包含哈希表中所必需的元素(键、值和哈希值)外,还包含两个指针,分别指向链表中前一个和后一个条目。这二者组成了一个双向链表。

在构造LinkedHashMap时,可以指定一个特殊的顺序模式,指定为access-order。默认情况下,LinkedHashMap是按照插入的顺序来排序条目,但如果启用了access-order模式,那么LinkedHashMap将会按照访问顺序排序,即最近访问的条目会被放到链表的尾端。

LinkedHashMap 的特性

  • 保序:不像HashMapLinkedHashMap会保持元素的插入顺序。
  • 性能LinkedHashMap在性能上类似于HashMap,因为它使用了相同的桶结构。对于插入、删除和查找操作,其时间性能依赖于哈希函数和初始容量等因素。
  • 内存开销:由于其内部维护了额外的双向链表来记录顺序,LinkedHashMapHashMap有稍微高一点的内存开销。

使用场景

  • 在Map中保持插入顺序:适用于需要保持顺序的缓存场景,比如记录文件中的键值对顺序。
  • LRU缓存(Least Recently Used, 最近最少使用):通过将LinkedHashMap配置为按访问顺序排序,你可以建立一个简单的LRU缓存。
  • 当需要通过插入顺序或者访问顺序迭代Map时: LinkedHashMap可以比其他Map实现更高效地进行迭代,因为迭代的时间复杂度是O(n),与元素的数量成正比。

3、TreeMap

  • 底层接口和原理TreeMap实现了SortedMap接口,它基于红黑树的数据结构。红黑树是一种自平衡的二叉搜索树,它可以保证在最坏情况下基本操作如搜索、插入、删除的时间复杂度都保持在O(log n)。

  • 性质

    • 查询速度TreeMap中的操作时间复杂度是O(log n),由于是基于红黑树,即使在最坏的情况下也能够保持较好的性能。
    • 顺序TreeMap保证元素以键的自然顺序或者自定义的Comparator进行排序。因此,它是有序的,可以非常方便地从中得到一个区间的子集。
    • 并发性TreeMap也不是线程安全的,同样需要外部同步机制来保障并发访问的安全性。

4、扩容

HashMapLinkedHashMap内部的数组(称为table)达到特定的装载因子(load factor)时,容量扩容流程(称为rehashing)被触发。默认装载因子的值为0.75,即当数组中75%的桶被使用时,就会进行扩容。这个装载因子是一个在创建HashMap时可以设置的参数,用于平衡时间和空间的使用效率。

以下是HashMap扩容流程的大致步骤:

  1. 创建新数组:根据当前数组容量的两倍创建一个新的数组。例如,如果当前数组容量为16,新数组的容量将是32。

  2. 重新计算索引:使用每个键(Key)的哈希值重新计算在新数组中的新索引。因为数组大小改变了,根据哈希值计算得出的索引可能也会改变。

  3. 重新分配元素:遍历旧数组,将每个元素移动到新数组的相应位置。如果冲突发生(即多个键哈希到新数组的同一位置),将保持链表或红黑树的结构不变,即如果旧数组使用链表存储碰撞的元素,则将这些元素作为链表重新链接;如果使用红黑树,则作为红黑树处理。

  4. 更新引用:一旦所有元素都被移动到新数组,将HashMap内部对数组的引用更新为新数组的引用。

在Java 8中,扩容时每个桶位置的元素会分割到两个位置。由于新数组的大小是原来的两倍,一个关键的观察结果是,元素的新位置要么是原位置,要么是原位置加上旧容量的大小。这是因为在Java中,哈希表中的位置是通过哈希值与数组长度减一进行位与操作计算得出的。

以下是在Java 8及以后版本中HashMap扩容时重新分配元素的高效特性:

  1. 不变的元素:对于没有冲突的桶,元素位置保持不变。
  2. 链表拆分:对于链表存储的桶,每个链表会被拆分成两个链表,这一操作可以在O(n)时间内完成。
  3. 红黑树拆分:对于红黑树存储的桶,树被拆分为两棵新树,并保持其平衡性。

这个过程中,不需要再次计算哈希值,因为哈希值存储在节点中,可以直接决定元素移动到原位置还是新位置(原位置加旧容量的大小)。

请注意,扩容(rehashing)是一项计算成本较高的操作,因为需要重新计算数组中每个元素的位置,所以在使用HashMap时应当尽量预估合适的初始容量以避免频繁扩容。

  • 11
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

济南大飞哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值