HashMap 提供的访问,是无序的。而在一些业务场景下,我们希望能够提供有序访问的 HashMap 。那么此时,我们就有两种选择:
TreeMap :按照 key 的顺序。
LinkedHashMap :按照 key 的插入和访问的顺序。
LinkedHashMap ,在 HashMap 的基础之上,提供了顺序访问的特性。而这里的顺序,包括两种:
而LinkedHashMap比HashMap优于以下几点
LinkedHashMap 内部维护了一个双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题
LinkedHashMap 元素的访问顺序也提供了相关支持,也就是我们常说的 LRU(最近最少使用)原则。
实现 Map 接口。
继承 HashMap 类。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, // 前一个节点
after; // 后一个节点
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
before 属性,指向前一个节点。after 属性,指向后一个节点。
通过 before + after 属性,我们就可以形成一个以 Entry 为节点的链表
既然 LinkedHashMap 是 LinkedList + HashMap 的组合
/**
* 头节点。
*
* 越老的节点,放在越前面。所以头节点,指向链表的开头
*
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 尾节点
*
* 越新的节点,放在越后面。所以尾节点,指向链表的结尾
*
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 是否按照访问的顺序。
*
* true :按照 key-value 的访问顺序进行访问。
* false :按照 key-value 的插入顺序进行访问。
*
* The iteration ordering method for this linked hash map: {@code true}
* for access-order, {@code false} for insertion-order.
*
* @serial
*/
final boolean accessOrder;
仔细看下每个属性的注释。
head + tail 属性,形成 LinkedHashMap 的双向链表。而访问的顺序,就是 head => tail 的过程。
accessOrder 属性,决定了 LinkedHashMap 的顺序。也就是说:
true 时,当 Entry 节点被访问时,放置到链表的结尾,被 tail 指向。
false 时,当 Entry 节点被添加时,放置到链表的结尾,被 tail 指向。如果插入的 key 对应的 Entry 节点已经存在,也会被放到结尾
LinkedHashMap 一共有 5 个构造方法,其中四个和 HashMap 相同,只是多初始化 accessOrder = false 。所以,默认使用插入顺序进行访问。
另外一个 #LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) 构造方法,允许自定义 accessOrder 属性
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
值得注意的是 LinkedHashMap 并没有覆写任何关于 HashMap put 方法。所以调用 LinkedHashMap 的 put 方法实际上调用了父类 HashMap 的方法。
在插入 key-value 键值对时,例如说 #put(K key, V value) 方法,如果不存在对应的节点,则会调用 #newNode(int hash, K key, V value, Node
因为 LinkedHashMap 自定义了 Entry 节点,所以必然需要重写该方法。代码如下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// <1> 创建 Entry 节点
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<>(hash, key, value, e);
// <2> 添加到结尾
linkNodeLast(p);
// 返回
return p;
}
<1> 处,创建 Entry 节点。虽然此处传入 e 作为 Entry.next 属性,指向下一个节点。但是实际上,#put(K key, V value) 方法中,传入的 e = null 。
<2> 处,调用 #linkNodeLast(LinkedHashMap.Entryp) 方法,添加到结尾。代码如下:
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
// 记录原尾节点到 last 中
LinkedHashMap.Entry<K,V> last = tail;
// 设置 tail 指向 p ,变更新的尾节点
tail = p;
// 如果原尾节点 last 为空,说明 head 也为空,所以 head 也指向 p
if (last == null)
head = p;
// last <=> p ,相互指向
else {
p.before = last;
last.after = p;
}
}
在 linkindHashMap 的读取、添加、删除时,分别提供了 #afterNodeAccess(Node
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
在 accessOrder 属性为 true 时,当 Entry 节点被访问时,放置到链表的结尾,被 tail 指向。所以 #afterNodeAccess(Node
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// accessOrder 判断必须是满足按访问顺序。
// (last = tail) != e 将 tail 赋值给 last ,并且判断是否 e 已经是队尾。如果是队尾,就不用处理了。
if (accessOrder && (last = tail) != e) {
// 将 e 赋值给 p 【因为要 Node 类型转换成 Entry 类型】
// 同时 b、a 分别是 e 的前后节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 第一步,将 p 从链表中移除
p.after = null;
// 处理 b 的下一个节点指向 a
if (b == null)
head = a;
else
b.after = a;
// 处理 a 的前一个节点指向 b
if (a != null)
a.before = b;
else
last = b;
// 第二步,将 p 添加到链表的尾巴。实际这里的代码,和 linkNodeLast 是一致的。
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
// tail 指向 p ,实际就是 e 。
tail = p;
// 增加修改次数
++modCount;
}
}
链表的操作看起来比较繁琐,实际一共分成两步:1)第一步,将 p 从链表中移除;2)将 p 添加到链表的尾巴。
因为 HashMap 提供的 #get(Object key) 和 #getOrDefault(Object key, V defaultValue) 方法,并未调用 #afterNodeAccess(Node
public V get(Object key) {
// 获得 key 对应的 Node
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果访问到,回调节点被访问
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
public V getOrDefault(Object key, V defaultValue) {
// 获得 key 对应的 Node
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
// 如果访问到,回调节点被访问
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
在开始看 #afterNodeInsertion(boolean evict) 方法之前,我们先来看看如何基于 LinkedHashMap 实现 LRU 算法的缓存。代码如下
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 传递进来最多能缓存多少数据
*
* @param cacheSize 缓存大小
*/
public LRUCache(int cacheSize) {
// true 表示让 LinkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
return size() > CACHE_SIZE;
}
}
我们在 #afterNodeInsertion(boolean evict) 方法中来理解。代码如下:
// LinkedHashMap.java
// evict 翻译为驱逐,表示是否允许移除元素
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// first = head 记录当前头节点。因为移除从头开始,最老
// <1> removeEldestEntry(first) 判断是否满足移除最老节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
// <2> 移除指定节点
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
默认情况下,都不移除最老的节点。所以在上述的 LRU 缓存的示例,重写了该方法,判断 LinkedHashMap 是否超过缓存最大大小。如果是,则移除最老的节点。
如果满足条件,则调用 #removeNode(…) 方法,移除最老的节点。
在节点被移除时,LinkedHashMap 需要将节点也从链表中移除,所以重写 #afterNodeRemoval(Node
void afterNodeRemoval(Node<K,V> e) { // unlink
// 将 e 赋值给 p 【因为要 Node 类型转换成 Entry 类型】
// 同时 b、a 分别是 e 的前后节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 将 p 从链表中移除
p.before = p.after = null;
// 处理 b 的下一个节点指向 a
if (b == null)
head = a;
else
b.after = a;
// 处理 a 的前一个节点指向 b
if (a == null)
tail = b;
else
a.before = b;
}
LinkedHashMap 是 HashMap 的子类,增加了顺序访问的特性。
【默认】当 accessOrder = false 时,按照 key-value 的插入顺序进行访问。
当 accessOrder = true 时,按照 key-value 的读取顺序进行访问。
LinkedHashMap 的顺序特性,通过内部的双向链表实现,所以我们把它看成是 LinkedList + LinkedHashMap 的组合。
LinkedHashMap 通过重写 HashMap 提供的回调方法,从而实现其对顺序的特性的处理。同时,因为 LinkedHashMap 的顺序特性,需要重写 #keysToArray(T[] a) 等遍历相关的方法。
LinkedHashMap 可以方便实现 LRU 算法的缓存,
LinkedHashMap 拥有与 HashMap 相同的底层哈希表结构,即数组 + 单链表 + 红黑树,也拥有相同的扩容机制。
LinkedHashMap 相比 HashMap 的拉链式存储结构,内部额外通过 Entry 维护了一个双向链表。
HashMap 元素的遍历顺序不一定与元素的插入顺序相同,而 LinkedHashMap 则通过遍历双向链表来获取元素,所以遍历顺序在一定条件下等于插入顺序。
LinkedHashMap 可以通过构造参数 accessOrder 来指定双向链表是否在元素被访问后改变其在双向链表中的位置。
LinkedHashMap和HashMap都是线程不安全的。
LinkedHashMap的结构是数组+链表(+红黑树)+双向链表。双向链表是用来维护元素的顺序的。HashMap的元素是无序的,LinkedHashMap的元素是有序的。LinkedHashMap的存取数据的方式还是跟HashMap一致
和HashMap的不同之处有:遍历方式不一样(扩容是用到遍历),HashMap根据按数组顺序来遍历,如果遇到链表,则依次遍历链表中的元素,完了之后继续遍历数组的元素。而LinkedHashMap是按双向列表来遍历的,所以LinkedHashMap遍历的性能比HashMap高一点,因为HashMap中有空元素。
看图就知道为什么要看成LinkedList + LinkedHashMap 的组合。
上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了
黑色表示next指针