本文主要是介绍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 的特性
- 保序:不像
HashMap
,LinkedHashMap
会保持元素的插入顺序。 - 性能:
LinkedHashMap
在性能上类似于HashMap
,因为它使用了相同的桶结构。对于插入、删除和查找操作,其时间性能依赖于哈希函数和初始容量等因素。 - 内存开销:由于其内部维护了额外的双向链表来记录顺序,
LinkedHashMap
比HashMap
有稍微高一点的内存开销。
使用场景
- 在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、扩容
当HashMap
或LinkedHashMap
内部的数组(称为table
)达到特定的装载因子(load factor)时,容量扩容流程(称为rehashing)被触发。默认装载因子的值为0.75,即当数组中75%的桶被使用时,就会进行扩容。这个装载因子是一个在创建HashMap
时可以设置的参数,用于平衡时间和空间的使用效率。
以下是HashMap
扩容流程的大致步骤:
-
创建新数组:根据当前数组容量的两倍创建一个新的数组。例如,如果当前数组容量为16,新数组的容量将是32。
-
重新计算索引:使用每个键(Key)的哈希值重新计算在新数组中的新索引。因为数组大小改变了,根据哈希值计算得出的索引可能也会改变。
-
重新分配元素:遍历旧数组,将每个元素移动到新数组的相应位置。如果冲突发生(即多个键哈希到新数组的同一位置),将保持链表或红黑树的结构不变,即如果旧数组使用链表存储碰撞的元素,则将这些元素作为链表重新链接;如果使用红黑树,则作为红黑树处理。
-
更新引用:一旦所有元素都被移动到新数组,将
HashMap
内部对数组的引用更新为新数组的引用。
在Java 8中,扩容时每个桶位置的元素会分割到两个位置。由于新数组的大小是原来的两倍,一个关键的观察结果是,元素的新位置要么是原位置,要么是原位置加上旧容量的大小。这是因为在Java中,哈希表中的位置是通过哈希值与数组长度减一进行位与操作计算得出的。
以下是在Java 8及以后版本中HashMap
扩容时重新分配元素的高效特性:
- 不变的元素:对于没有冲突的桶,元素位置保持不变。
- 链表拆分:对于链表存储的桶,每个链表会被拆分成两个链表,这一操作可以在O(n)时间内完成。
- 红黑树拆分:对于红黑树存储的桶,树被拆分为两棵新树,并保持其平衡性。
这个过程中,不需要再次计算哈希值,因为哈希值存储在节点中,可以直接决定元素移动到原位置还是新位置(原位置加旧容量的大小)。
请注意,扩容(rehashing)是一项计算成本较高的操作,因为需要重新计算数组中每个元素的位置,所以在使用HashMap
时应当尽量预估合适的初始容量以避免频繁扩容。
这篇关于HashMap、LinkedHashMap、TreeMap的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!