提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录- 前言
- 一、源码分析
- 1. accessOrder
- 2. put
- 3.afterNodeXxx
- 3.1 afterNodeAccess
- 3.2 afterNodeInsertion
- 3.3 afterNodeRemoval
- 4. 遍历
- 二、LruCache
- 1. put
- 2. get
- 3. remove
- 总结
- 参考
前言
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. putlinkedHashMap 并没有重写 put 方法,HashMap 在 put 元素时会调用 newNode(数组和链表) 和 newTreeNode(红黑树) 创建节点对象,linkedHashMap 重写了这两个方法
NodenewNode(int hash, K key, V value, Node e) { linkedHashMap.Entry p = new linkedHashMap.Entry<>(hash, key, value, e); linkNodeLast(p); return p; } TreeNode newTreeNode(int hash, K key, V value, Node next) { TreeNode 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 Entryextends HashMap.Node { Entry before, after; Entry(int hash, K key, V value, Node next) { super(hash, key, value, next); } }
增加了了两个指针 before 和 after 用于实现双向链表,在上面的 newNode 和 newTreeNode 中都调用了 linkNodeLast 将节点插入到双向链表的表尾
private void linkNodeLast(linkedHashMap.Entry3.afterNodeXxxp) { linkedHashMap.Entry last = tail; // 将双向链表的表尾指向 p tail = p; // 如果原来双向链表的表尾为 null 说明双向链表还没有元素把表头也指向 p if (last == null) head = p; else { // 链接到链表尾部 p.before = last; last.after = p; } }
// Callbacks to allow linkedHashMap post-actions
void afterNodeAccess(Node p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node p) { }
HashHap 会在插入删除访问元素时调用这几个方法让 linkedHashMap 可以维护元素顺序
3.1 afterNodeAccesslinkedHashMap 重写了 get 和 getOrDefault 来处理访问的情况
public V get(Object key) {
Node e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
public V getOrDefault(Object key, V defaultValue) {
Node e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
如果 accessOrder 为 true 则调用 afterNodeAccess 更新访问顺序,把访问的元素移动到双向链表表尾
void afterNodeAccess(Node3.2 afterNodeInsertione) { // move node to last linkedHashMap.Entry last; // 如果是访问顺序并且表尾的元素不等于 e if (accessOrder && (last = tail) != e) { // 记录前置节点和后置节点 linkedHashMap.Entry p = (linkedHashMap.Entry )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; } }
在 put 时如果是插入的新元素会调用 afterNodeInsertion 方法,linkedHashMap 同样重写了此方法
void afterNodeInsertion(boolean evict) { // possibly remove eldest
linkedHashMap.Entry 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 afterNodeRemovalvoid afterNodeRemoval(Nodee) { // unlink linkedHashMap.Entry p = (linkedHashMap.Entry )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 next;
linkedHashMapEntry current;
int expectedModCount;
linkedHashIterator() {
// 初始化时 next 指向双向链表的表头
next = head;
// 修改次数,遍历过程中有修改抛出异常
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
// 下一个节点不为空
return next != null;
}
final linkedHashMapEntry nextNode() {
linkedHashMapEntry 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 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
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 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为何有序?详解源码及底层原理



