前两篇我们讲了List与Set,这篇是集合的最后一篇了,讲讲我们常用的Map
1. 存储结构(基于 JDK 源码)
-
-
HashMap:
- 内部结构组成:在 JDK 源码中,HashMap 主要由数组(
Node<K,V>[] table)、链表(Node<K,V>)和红黑树(TreeNode<K,V>)构成。数组是存储元素的基础结构,每个数组元素可以看作是一个链表的头节点(在 Java 8 之后,当链表长度达到一定阈值(默认为 8)时,链表会转换为红黑树来提高性能)。Node类(用于链表)和TreeNode类(用于红黑树)都实现了Map.Entry<K,V>接口,用于存储键值对信息。 - 存储过程:当添加一个键值对(
put操作)时,首先通过hash函数计算键(K)的哈希值,然后通过(n - 1) & hash运算(n是数组长度)确定在数组中的存储位置(索引)。如果该位置为空,就直接创建一个新的Node(或TreeNode)并放入该位置;如果该位置已经有元素(产生哈希冲突),则会将新节点插入到对应的链表或红黑树中。例如,下面是putVal方法(简化版)中的关键部分:
- 内部结构组成:在 JDK 源码中,HashMap 主要由数组(
-
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 处理冲突情况
}
return null;
}
-
TreeMap:
- 基于红黑树存储:TreeMap 内部是基于红黑树(
Entry<K,V>作为树的节点)实现的。红黑树是一种自平衡二叉搜索树,其节点包含键(K)、值(V)、指向左子树和右子树的指针(left和right),以及用于维护红黑树性质的颜色标记(color)。每个节点的左子树中的键值都小于该节点的键值,右子树中的键值都大于该节点的键值。 - 存储过程:当插入一个键值对(
put操作)时,会根据键的大小关系沿着树的节点路径找到合适的位置插入元素。在put方法中,通过comparator(比较器)或者键的自然顺序(如果键实现了Comparable接口)来比较键的大小,找到插入位置后插入新节点,然后通过一系列旋转和颜色调整操作来保持红黑树的平衡性质。例如,下面是put方法(部分关键代码):
- 基于红黑树存储:TreeMap 内部是基于红黑树(
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// 根据比较结果查找插入位置
Comparator<? super K> cpr = comparator;
if (cpr!= null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t!= null);
} else {
// 使用键的自然顺序查找插入位置
}
// 插入新节点并调整树结构
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
-
LinkedHashMap:
- 继承与扩展:LinkedHashMap 继承自 HashMap,它在 HashMap 的基础上,通过维护一个双向链表(
Entry<K,V>节点包含了用于维护链表顺序的前一个和后一个节点的引用,即before和after)来记录元素的插入顺序或者访问顺序(可以通过构造函数指定)。 - 存储过程:在插入一个新元素时,首先执行和 HashMap 相同的
put操作来确定元素在哈希表中的存储位置。然后,将新元素插入到双向链表的末尾(如果是按照插入顺序)或者根据访问顺序调整其在链表中的位置。例如,在afterNodeInsertion方法(用于在插入节点后维护链表顺序)中有如下代码:
- 继承与扩展:LinkedHashMap 继承自 HashMap,它在 HashMap 的基础上,通过维护一个双向链表(
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head)!= null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
2. 操作特点(基于 JDK 源码)
-
-
HashMap:
- 添加(put)操作:计算键的哈希值来确定存储位置的过程时间复杂度接近 O (1)。但如果发生哈希冲突,需要在链表或红黑树中添加元素,在最坏情况下(例如所有元素都哈希到同一个位置),时间复杂度可能为 O (n)。不过在实际应用中,只要哈希函数设计合理,性能通常较好。
- 获取(get)操作:通过键的哈希值找到存储位置,然后在链表或红黑树中查找对应的值。在最好情况下,时间复杂度为 O (1),如果发生哈希冲突并且链表很长或者红黑树结构复杂,最坏情况下时间复杂度为 O (n)。
- 删除(remove)操作:先找到键对应的存储位置,然后从链表或红黑树中删除相应的节点。时间复杂度在最好情况下为 O (1),最坏情况下为 O (n)。
-
TreeMap:
- 添加(put)操作:根据键的大小关系在红黑树中找到插入位置,插入后要进行平衡调整,时间复杂度为 O (log n)。这个时间复杂度在元素数量较多时,相比 HashMap 在最坏情况下的性能更稳定。
- 获取(get)操作:在红黑树中根据键的大小关系查找对应的键值对,时间复杂度为 O (log n)。因为红黑树的高度是对数级别的,所以查找操作比较高效。
- 删除(remove)操作:在红黑树中找到要删除的键对应的节点,然后进行删除和树的平衡调整,时间复杂度为 O (log n)。
-
LinkedHashMap:
- 添加(put)操作:和 HashMap 类似,先确定哈希位置,然后插入到链表中,时间复杂度接近 O (1)。由于还需要维护链表顺序,在一些极端情况下可能会有额外的小开销,但总体性能和 HashMap 相近。
- 获取(get)操作:和 HashMap 类似,通过哈希位置查找,时间复杂度在最好情况下为 O (1),最坏情况下为 O (n)。如果是按照访问顺序维护的 LinkedHashMap,在获取元素后还会调整元素在链表中的位置。
- 删除(remove)操作:除了在哈希表中删除节点,还需要在链表中维护顺序,时间复杂度在最好情况下为 O (1),最坏情况下为 O (n)。
-
3. 适用场景(基于操作特点和存储结构)
-
-
HashMap:
-
适用于需要快速的添加、获取和删除键值对操作,并且不要求元素按照特定顺序存储的场景。例如,在缓存系统中,将缓存的键(如数据的唯一标识符)和值(如数据内容)存储在 HashMap 中,能够快速地根据键获取缓存的数据。在数据统计场景中,如统计单词出现的频率,将单词作为键,出现频率作为值存储在 HashMap 中,可以高效地更新频率和获取频率信息。
-
对于一些需要临时存储数据,并且主要关注数据的快速访问和更新的场景,如配置参数的存储(不考虑顺序),HashMap 是一个很好的选择。
-
-
TreeMap:
- 当需要按键的顺序(如自然顺序或自定义顺序)存储和访问键值对时,TreeMap 是合适的选择。例如,在存储学生姓名和成绩的场景中,按照姓名排序存储所有学生的信息,方便进行范围查询(如查询某一姓名区间内的学生成绩)。在实现字典功能或者需要按照一定顺序遍历键值对的应用程序中很有用,如实现一个命令行参数解析器,将命令选项作为键,选项对应的参数作为值,按照选项的字母顺序存储在 TreeMap 中,便于用户查看和使用。
- 在需要对键值对进行排序并进行一些基于顺序的操作(如查找排名、范围查找等)的场景下,TreeMap 能够提供很好的支持。
-
LinkedHashMap:
- 适用于需要保留元素插入顺序或者访问顺序的场景。例如,在实现一个简单的历史记录功能时,将用户访问的网页链接作为键,访问时间作为值存储在 LinkedHashMap 中,就可以按照访问顺序查看历史记录。在需要对键值对进行缓存,并且希望能够按照一定顺序(如最近访问顺序)清理缓存的场景下,LinkedHashMap 可以发挥作用。例如,通过设置缓存容量,当超过容量时,根据插入顺序或者访问顺序删除最旧的键值对。
-
好,以上就是Map的全部