简介
LinkedHashMap是一个散列表,储存的元素为键值对(key-value),允许空值和空键,非线程安全,实际上它在HashMap的基础上添加了一个双向链表用于保存遍历顺序,所以LinkedHashMap是一个有序集合。
LinkedHashMap的源码比较简单,其主要功能已由HashMap实现,下面暂时只分析链表部分,红黑树部分不做深入。
以下分析基于corretto-1.8.0_282版本。
继承关系
- 继承自HashMap,包含HashMap的所有功能。
主要内部类
Entry<K,V>
/**
* 元素节点,添加了两个指针用于组成双向链表,遍历时会沿着这个双向链表进行遍历
*/
static class Entry<K,V> extends Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
属性
head
/**
* 双向链表的头指针
*/
transient Entry<K,V> head;
tail
/**
* 双向链表的尾指针
*/
transient Entry<K,V> tail;
accessOrder
/**
* 遍历顺序,true代表使用访问顺序遍历,false代表使用插入顺序遍历
* 插入顺序:指遍历时元素按插入的顺序输出
* 访问顺序:指遍历时元素按访问的顺序输出,最近访问的元素会最后遍历
* 最近访问的元素:指的是调用过get(key)、put(key, value)且key存在的对应的元素
*/
final boolean accessOrder;
构造方法
构造函数与HashMap基本类似,部分方法多了一个accessOrder参数用来设置LinkedHashMap的遍历顺序,accessOrder=true按元素访问顺序遍历,accessOrder=false按元素插入顺序遍历。
/**
* 按给定的初始容量和加载因子实例化,遍历顺序设置为按插入顺序遍历
*/
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
/**
* 按给定的初始容量实例化,加载因子为默认(0.75),遍历顺序设置为按插入顺序遍历
*/
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
/**
* 与HashMap类似,使用默认容量(16)和默认加载因子(0.75)进行
* 实例化,并将遍历顺序设置为按插入顺序遍历
*/
public LinkedHashMap() {
super();
accessOrder = false;
}
/**
* 与HashMap类似,根据给定Map实例化一个存放相同映射的HashMap,使用
* 默认加载因子(0.75),初始容量根据传入的Map计算,遍历顺序设置为按插入顺序遍历
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
/**
* 按给定的初始容量、加载因子、遍历顺序实例化
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
方法
LinkedHashMap大部分操作数据的方法都继承自其父类HashMap,在HashMap的对数据操作的方法中,有一些钩子函数,HashMap中没有做实现,是给LinkedHashMap预留的,例如插入元素的putVal方法,其中包含afterNodeAccess、afterNodeInsertion两个钩子函数,用来调整LinkedHashMap中元素在链表中的位置。
/**
* 添加键值对
* onlyIfAbsent参数决定若键已存在是否对其进行更新,true不进行更新,false进行更新
* 通过源码可知,若此键对应旧值为null,则不管onlyIfAbsent参数,都进行更新
* 若插入前键已存在,则返回其对应的值,不存在则返回null
*/
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;
// 若hash对应的数组位置为空,则直接实例化一个链表节点插入此位置
// 变量p为链表头或红黑树的根节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果链表头或红黑树的根节点的key与传入的key相同,则将其赋给变量e,不需要向下查找了
e = p;
else if (p instanceof TreeNode)
// 若已经树化,则交由红黑树操作
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// 若遍历到链表尾部还没有找到key相同的节点,则直接实例化一个新节点插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 若此时链表元素数量大于等于树化阈值,则进行树化操作
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 若找到了key相同的节点,将其赋给变量e,终止遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 若e不为空,说明已经存在key相同的节点,需要根据条件决定是否更新其value值
if (e != null) {
V oldValue = e.value;
// 如果onlyIfAbsent为false或旧值为空,则对其进行更新
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// LinkedHashMap使用
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果HashMap中的元素数量大于扩容阈值,则进行扩容操作
if (++size > threshold)
resize();
// LinkedHashMap使用
afterNodeInsertion(evict);
return null;
}
对HashMap中存在的方法不再做说明,只介绍LinkedHashMap重写的方法。
newNode(int hash, K key, V value, Node<K,V> e)
/**
* HashMap通过此方法创建新的链表节点,此处对其进行重写
*/
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
Entry<K,V> p =
new Entry<K,V>(hash, key, value, e);
// 将新节点插入链表尾部,保存插入顺序
linkNodeLast(p);
return p;
}
/**
* 节点链入链表尾部
*/
private void linkNodeLast(Entry<K,V> p) {
// 保存尾部指针
Entry<K,V> last = tail;
// 将尾指针指向新节点
tail = p;
if (last == null)
// 这是插入的第一个节点,将链表头指针也指向它
head = p;
else {
// 将旧尾和新尾链接起来
p.before = last;
last.after = p;
}
}
get(Object key)
/**
* 重写了HashMap的对应方法,在原有逻辑上添加了调整遍历顺序的功能
*/
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;
}
getOrDefault(Object key, V defaultValue)
/**
* 若key不存在,则返回defaultValue的值
*/
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;
}
forEach(BiConsumer<? super K, ? super V> action)
/**
* 遍历所有元素
*/
public void forEach(BiConsumer<? super K, ? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
// 重写遍历方法,改为对双向链表进行遍历
for (Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
/**
* 遍历所有元素,根据传入的方法对值进行替换
* @param function
*/
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
if (function == null)
throw new NullPointerException();
int mc = modCount;
// 重写遍历方法,改为对双向链表进行遍历
for (Entry<K,V> e = head; e != null; e = e.after)
e.value = function.apply(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
afterNodeRemoval(Node<K,V> e)
/**
* 当删除元素时,会调用此方法,从双向链表中删除对应节点
*/
void afterNodeRemoval(Node<K,V> e) {
// 保存节点e的前驱、后继节点
Entry<K,V> p =
(Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
// 若前驱b节点为null,说明e是链表头,则将链表头指针指向e的后继节点
head = a;
else
// 不是链表头,需要修改前驱节点b的后继指针
b.after = a;
if (a == null)
// 若后继a节点为null,说明e是链表尾,则将链表尾指针指向e的前驱节点
tail = b;
else
// 不是链表尾,需要修改后继节点a的前驱指针
a.before = b;
}
afterNodeAccess(Node<K,V> e)
/**
* 当访问元素时,会调用此方法,遍历除外
*/
void afterNodeAccess(Node<K,V> e) {
Entry<K,V> last;
// 若遍历顺序为访问顺序且给定节点不是链表尾节点
// 则将此节点移动到链表尾部
if (accessOrder && (last = tail) != e) {
// 保存给定节点e的前驱节点b、后继节点a
Entry<K,V> p =
(Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
// 如果前驱节点b为null,说明e是链表头,需要将链表头指针指向后继节点a
head = a;
else
// e不是链表头,则需要将其前驱节点b的后继指针链接到后继节点a上
b.after = a;
if (a != null)
// e不是链表尾,需要将其后继节点a的前驱指针链接到前驱节点b上
a.before = b;
else
// e是链表尾,将last指向e的前驱节点b
last = b;
if (last == null)
// 此时链表中只有e一个元素,将链表头指向e
head = p;
else {
// 将e链接到链表尾部
p.before = last;
last.after = p;
}
// 链表尾指针指向传入节点e
tail = p;
// 修改次数加一
// 当对LinkedHashMap迭代时,若调用了get(key)等访问方法,会抛出ConcurrentModificationException异常
// 所以当accessOrder为true时,不能使用类似下方的代码来对LinkedHashMap进行遍历
// for (Key key : map.keySet()) {
// map.get(key); // throw ConcurrentModificationException
// }
++modCount;
}
}
afterNodeInsertion(boolean evict)
/**
* 当插入元素后,会调用此方法
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
Entry<K,V> first;
// 若传入参数为true且Map不为空,则根据removeEldestEntry(first)方法的返回值决定是否移除链表的第一个元素
// 当遍历顺序为插入顺序时,链表头节点为最先插入的元素
// 当遍历顺序为访问顺序时,链表头节点为最远访问的元素(最近访问的元素会调整到链表尾部)
// 在这里removeEldestEntry始终返回false,即永远不会删除节点
// 可以通过重写此方法使其变得有意义,例如使用LinkedHashMap实现LRU缓存时,需要重写此方法,当元素数量大于缓存最大容量时,删除最近最少使用的节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
总结
- LinkedHashMap继承自HashMap,在支持HashMap的所有功能的基础上,添加了按顺序遍历元素的功能。
- LinkedHashMap中元素的遍历顺序时通过一个双向链表来保存的,当对HashMap中的元素进行增删改查操作时,会对这个链表中元素的顺序进行相应的调整。
- LinkedHashMap重写了HashMap中的迭代器,使其从原来的遍历数组->链表这种不确定顺序的方式,变成遍历一个有序的链表。保证LinkedHashMap的遍历顺序。
- 通过accessOrder参数可以设置LinkedHashMap的遍历顺序是按插入顺序还是按访问顺序。
- 可以将accessOrder设置为true和重写removeEldestEntry方法来快速实现一个LRU缓存。