提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
HashMap 不是有序的当需要保持元素的顺序时就可以使用 LinkedHashMap 它支持维护两种顺序,插入顺序和访问顺序(可用于实现 Lru 算法),LinkedHashMap 是 HashMap 的子类所以原理相同只不过 LinkedHashMap 额外维护了一个双向链表来保持顺序,在对 LinkedHashMap 进行增删查改时同时操作这个双向链表
提示:本文基于 Android SDK 28 JDK 1.8
一、源码分析
1. accessOrder
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
accessOrder 通过构造方法传入,为 true 表示元素顺序是访问顺序,为 false 表示元素顺序是插入顺序,默认是 false
2. put
LinkedHashMap 并没有重写 put 方法,HashMap 在 put 元素时会调用 newNode(数组和链表) 和 newTreeNode(红黑树) 创建节点对象,LinkedHashMap 重写了这两个方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<>(hash, key, value, e);
linkNodeLast(p);
return p;
}
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<>(hash, key, value, next);
linkNodeLast(p);
return p;
}
newNode 返回的是 LinkedHashMap.Entry 对象,newTreeNode 返回的是 TreeNode 它们的继承关系是 java.util.HashMap.TreeNode 继承自 java.util.LinkedHashMap.Entry 继承自java.util.HashMap.Node 实现了 java.util.Map.Entry 接口,LinkedHashMap.Entry 的定义如下
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 用于实现双向链表,在上面的 newNode 和 newTreeNode 中都调用了 linkNodeLast 将节点插入到双向链表的表尾
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
// 将双向链表的表尾指向 p
tail = p;
// 如果原来双向链表的表尾为 null 说明双向链表还没有元素把表头也指向 p
if (last == null)
head = p;
else {
// 链接到链表尾部
p.before = last;
last.after = p;
}
}
3.afterNodeXxx
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
HashHap 会在插入删除访问元素时调用这几个方法让 LinkedHashMap 可以维护元素顺序
3.1 afterNodeAccess
LinkedHashMap 重写了 get 和 getOrDefault 来处理访问的情况
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
/**
* {@inheritDoc}
*/
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
如果 accessOrder 为 true 则调用 afterNodeAccess 更新访问顺序,把访问的元素移动到双向链表表尾
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// 如果是访问顺序并且表尾的元素不等于 e
if (accessOrder && (last = tail) != e) {
// 记录前置节点和后置节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 因为要把 p 放到双向链表的尾部所以后置节点为 null
p.after = null;
// 如果 p 的前置节点为 null 说明 p 之前是头节点,将 head 指向 p 的后置接点 a
if (b == null)
head = a;
else
// 将 b 的后置节点指向 p 的后置节点 a 移除 p 的引用
b.after = a;
// 如果 p 的后置节点 a 不为空将 a 的前置节点指向 b
if (a != null)
a.before = b;
else
// p 的后置节点 a 为 null 说明 p 之前是尾节点
// 将 last 指向 p 的前置节点 a
last = b;
// 原本的尾节点为 null 说明之前双向链表还没有元素直接把 head 指向 p
if (last == null)
head = p;
else {
// 把 p 跟之前最后一个节点连接起来 p 变成最后一个节点
p.before = last;
last.after = p;
}
// 把尾节点指向 p
tail = p;
++modCount;
}
}
3.2 afterNodeInsertion
在 put 时如果是插入的新元素会调用 afterNodeInsertion 方法,LinkedHashMap 同样重写了此方法
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);
}
}
在 put 方法中 evict 为 true,如果 removeEldestEntry 返回 true 就会移除最老的元素,LinkedHashMap 中默认返回 false,子类可以重写此方法实现自己的逻辑
3.3 afterNodeRemoval
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
移除元素的时候同样在双向链表中把元素移除,就是把引用断开 p 不再引用别的节点,别的节点也不引用它
4. 遍历
HashMap 有三个遍历相关的方法
- keySet 返回 key 的 set 集合,对应的迭代器是 LinkedKeyIterator
- values 返回 value 集合,对应的迭代器是 LinkedValueIterator
- entrySet 返回 LinkedHashMapEntry 的 set 集合,对应的迭代器是 LinkedEntryIterator
三个迭代器都继承自 LinkedHashIterator
abstract class LinkedHashIterator {
LinkedHashMapEntry<K,V> next;
LinkedHashMapEntry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
// 初始化时 next 指向双向链表的表头
next = head;
// 修改次数,遍历过程中有修改抛出异常
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
// 下一个节点不为空
return next != null;
}
final LinkedHashMapEntry<K,V> nextNode() {
LinkedHashMapEntry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
// 记录当前节点用于删除
current = e;
// 指向后置节点
next = e.after;
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
// 调用 HashMap 的方法删除节点
removeNode(hash(key), key, null, false, false);
// 删除节点后会对 modCount 做加 1 对 expectedModCount 重新赋值
expectedModCount = modCount;
}
}
二、LruCache
LRU 即 Least Recently Used 最近最少使用算法,命中之后移动到表尾超出最大容量后移除表头,LruCache 是 Android 中 LRU 算法实现的数据结构,使用 LinkedHashMap 实现,创建
LinkedHashMap 时 accessOrder 传入 true
1. put
public final V put(@NonNull K key, @NonNull V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
// 记录新元素的大小
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
// 如果存在老的元素减去老的元素的大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
// 删除元素时的回调方法,这里是空实现
entryRemoved(false, key, previous, value);
}
// 如果超出容量则删除最老的元素
trimToSize(maxSize);
return previous;
}
public void trimToSize(int maxSize) {
// 循环遍历元素
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
// 如果当前元素的大小小于允许大小或者 map 为空跳出循环
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
// 移除表头元素
map.remove(key);
// 减去元素大小(可以是元素个数也可以是其他比如元素占用内存)
size -= safeSizeOf(key, value);
evictionCount++;
}
// 删除元素回调(空实现,子类可重写实现自己的逻辑)
entryRemoved(true, key, value, null);
}
}
在 put 元素后会判断如果超出了最大允许的元素个数(或者是其他子类实现的逻辑,比如图片缓存时的内存占用)则移除最早的元素,这里没有通过重写 removeEldestEntry 实现我理解是因为重写 removeEldestEntry 只能满足元素个数的场景,比如最多允许 20 个元素满了移除最早的元素,如果是内存占用可能新加入的元素比较大需要移除多个元素 removeEldestEntry 就满足不了了。另外 Android SDK 28 中的 android.util.LruCache 实现是错的,它移除了表尾的元素,在 Android SDK 30 已经修复了,androidx.collection.LruCache 的实现是对的
2. get
public final V get(@NonNull K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// get 时使用 LinkedHashMap 维护顺序,之后会调用 afterNodeAccess
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
}
get 比较简单直接调用了 LinkedHashMap 的 get 方法让 LinkedHashMap 来维护顺序
3. remove
public final V remove(@NonNull K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
// 调用 LinkedHashMap 的 remove 方法
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
remove 与 get 一样都是直接调用了 LinkedHashMap 的方法
总结
在已经分析过 HashMap 的前提下 LinkedHashMap 总体来说比较简单,只是额外维护了一个双向链表来保证顺序,根据构造方法中传入的 accessOrder 的值确定是插入顺序还是访问顺序,在删除插入访问时在 HashMap 原有逻辑的基础上回调几个方法操作双向链表,把删除的元素的引用断开,把插入和访问(accessOrder 为 true 时)的元素移动到链表尾。LinkedHashMap 可以用来实现 LRU 算法的数据结构,Android 中的 LruCache 就使用它来实现
参考
面试必备:LinkedHashMap源码解析(JDK8)
图解LinkedHashMap原理
深度分析:那些阿里,腾讯面试官都喜欢问的LinkedHashMap源码
LinkedHashMap为何有序?详解源码及底层原理