LinkedHashMap
是一个实现了哈希表和链表的映射类。其在Java中的类组织结构如下:
LinkedHashMap
是HashMap
的子类,继承了HashMap
所有的可选操作,所以,在了解LinkedHashMap
之前必须先知道HashMap
是怎么一回事。
HashMap底层实现原理(上)
HashMap底层实现原理(下)
阅读HashMap源码时你可能会有这些疑问
LinkedHashMap
与HashMap
的不同之处在于,它维护一个贯穿其所有条目的双链接列表,有可预测的迭代顺序,这个顺序可用是键插入到映射中的顺序(默认),或访问的顺序。
先看一下在LinkedHashMap
中一个普通元素的节点子类:
/**
* HashMap。普通LinkedHashMap条目的节点子类。
*/
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
/**
* 双链表的头(最年长的)。
*/
transient LinkedHashMapEntry<K,V> head;
/**
* 双链表的尾部(最年轻的)。
*/
transient LinkedHashMapEntry<K,V> tail;
可以看到在LinkedHashMap
中除了继承了HashMap
的数组(Node<K,V>[] table
)之外,其节点实现也由单向链表变成了一个双向的链表结构。
- HashMap中的单向链表仅用于拉链法解决哈希冲突,每个节点对应一个链表,不同节点之间的链表没有连接关系。
- LinkedHashMap中的双向链表是不仅要起到HashMap中链表的作用,还要负责连接每一个元素,具元素之间具有完整的连接关系。
LinkedHashMap
提供了一个特殊的构造函数来创建一个链表哈希映射:LinkedHashMap(int,float,boolean)
,当accessOrder
为true
时,其迭代顺序是最后访问它的条目的顺序。这种映射非常适合于构建LRU缓存(Android LruCache实现原理)。
/**
* 构造一个空的LinkedHashMap实例,该实例具有指定的初始容量、负载因子和排序模式。
*
* @param accessOrder 顺序模式为:true为访问顺序,为false为插入顺序
*/
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
插入顺序
LinkedHashMap
并没有实现自己的put
方法,说明他的put
操作还是沿用了HashMap
的实现。不过,LinkedHashMap
却重写了HashMap
在put
操作过程中调用过的newNode(int , K , V , Node)
方法。也就是说,LinkedHashMap
的数据依旧是存储在数组中,且通过hash值计算出其在数组中的位置,同样也是采用了拉链法和红黑树化来处理哈希冲突。不同之处便在其元素节点之间在创建时会构成一个链式的存储结构。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMapEntry<K,V> p =
new LinkedHashMapEntry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
/**
* 链接在列表的末尾
*/
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
LinkedHashMapEntry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
插入的顺序很容易理解,由linkNodeLast(LinkedHashMapEntry<K,V> p)
方法可以得出结论:每插入一条key不存在的数据,该数据将被连接在全部数据元素的链表尾部。
那么如果插入一条key已经存在的数据呢?我们顺着HashMap#putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
方法的最后可以看到如下代码:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
afterNodeAccess
在HashMap
中是一个空方法,注释中明确说明这个回调是给LinkedHashMap
用的。这个方法顾名思义:Node
节点被访问之后。那么我们从LinkedHashMap#get
方法中寻找一下他的踪迹。
访问顺序
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;
}
LinkedHashMap
对get
的实现大体相当于HashMap
,只有当accessOrder
为true
时会通过afterNodeAccess(e)
对链表做些特殊的处理。这个方法的作用是将当前正在访问的节点e移动到链表的尾部。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;//因为p要被移动到链表尾部,所以他向后的指针应指向null
if (b == null)
head = a;//如果p位于链表头部,让p的下一个节点做头部
else
b.after = a;//如果p不位于链表头部,让p的上一个节点(b)向后的指针指向p的下一个节点(a)
if (a != null)
a.before = b;//如果p不位于链表尾部,让p的下一个节点(a)向前的指针指向e的上一个节点(a)
else
last = b;//如果e位于链表尾部,让e的上一个节点做尾部
//至此为止,节点e已经被从链表中单独剥离出来了。
if (last == null)
head = p;//last == null意味着此时链表长度为1,即p既是头也是尾
else {//将p连接到链表尾部
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
到这里关于LinkedHashMap
的顺序模式可以总结为:
- 插入顺序的模式下,
LinkedHashMap
每次插入元素时(不论key是否存在)都按照插入的先后顺序排列。 - 访问顺序的模式下,
LinkedHashMap
每次访问元素时(get或put都视为访问),将访问的元素从原位置移动到链表的尾部。
遍历
最后再来看LinkedHashMap的遍历。其方式通常有如下两种用法:
// forEach遍历
map.forEach((key, value) -> {
...
});
// 迭代器遍历
for (String key : map.keySet()) {
String value = map.get(key);
}
不同于HashMap
,HashMap
在进行遍历时是对数组Node[]
直接进行遍历,所以无法保证插入的顺序和遍历的顺序一致。而LinkedHashMap
是对其内部链表从头至尾进行遍历,进而也就能分别实现按照插入顺序或访问顺序输出遍历结果。
forEach
是java8之后出现的,实现比较简短。
public void forEach(BiConsumer<? super K, ? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
// Android-changed: Detect changes to modCount early.
for (LinkedHashMapEntry<K,V> e = head; modCount == mc && e != null; e = e.after)
action.accept(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}