目录
摘要:HashMap 和双向链表合二为一即是 LinkedHashMap
友情提示
1、LinkedHashMap 概述
2、LinkedHashMap 在 JDK 中的定义
2.1 类结构定义
2.2 成员变量定义:增加了两个独有属性:双向链表头结点 header 和 迭代顺序标志位accessOrder【true=按访问顺序排序,false=按插入顺序排序(默认)】
2.3 成员方法定义
2.4 基本元素 Entry:重新定义了Entry,增加了两个指针 before 和 after用于维护双向链表
2.5 LinkedHashMap 的构造函数
2.6 LinkedHashMap 的数据结构
2.7 LinkedHashMap 的快速存取
LinkedHashMap 的存储实现 : put(key, vlaue):在 LinkedHashMap 中向哈希表中插入新 Entry 的同时,还会通过 Entry 的 addBefore(head) 方法将其链入到双向链表中。其中,addBefore 方法本质上是一个双向链表的插入操作
LinkedHashMap 的扩容操作 : resize(),扩容为原来的2倍
LinkedHashMap 的读取实现 :get(Object key)
2.8 LinkedHashMap 存取小结
1、LinkedHashMap 的存取过程基本与 HashMap 类似,只是在细节实现上稍有不同,这是由 LinkedHashMap 本身的特性所决定的,因为它要额外维护一个双向链表用于保持迭代顺序。
2、在 put 操作上,虽然 LinkedHashMap 完全继承了 HashMap 的 put 操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap 中向哈希表中插入新 Entry 的同时,还会通过 Entry 的 addBefore 方法将其链入到双向链表中。
3、在扩容操作上,虽然 LinkedHashMap 完全继承了 HashMap 的 resize 操作,但是鉴于性能和 LinkedHashMap 自身特点的考量,LinkedHashMap 对其中的重哈希过程(transfer方法)进行了重写(照着双向链表的顺序来重哈希)。在读取操作上,LinkedHashMap 中重写了HashMap 中的 get 方法(增加了 recordAccess方法,如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做;如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将 e 移到链表的末尾处),通过 HashMap 中的 getEntry 方法获取 Entry 对象,在此基础上,进一步获取指定键对应的值。
3、LinkedHashMap 与 LRU
3.1 put 操作与标志位 accessOrder:recordAccess 提供了 LRU 算法的实现,它将最近使用的 Entry 放到双向循环链表的尾部。也就是说,当 accessOrder 为 true 时,get 方法和 put 方法(如果不存在一样的key是插入链表尾部,若已经存在一样的key,就是更新,更新后会挪到链表的尾部)都会调用 recordAccess 方法使得最近使用的Entry移到双向链表的末尾;当 accessOrder 为默认值false 时,从源码中可以看出 recordAccess 方法什么也不会做。
3.2 get 操作与标志位 accessOrder
3.3 LinkedListMap 与 LRU 小结【访问标志accessOrder是决定put和get时要不要按访问顺序,removeEldestEntry方法是决定何时删除最近最久未访问节点,默认是返回false,即不会删除,若要删除即要实现LRU,你只需要重写这个方法】
1、使用 LinkedHashMap 实现 LRU 的必要前提是将 accessOrder 标志位设为 true 以便开启按访问顺序排序的模式。我们可以看到,无论是 put 方法还是 get 方法,都会导致目标 Entry 成为最近访问的 Entry,因此就把该 Entry 加入到了双向链表的末尾:get 方法通过调用 recordAccess 方法来实现。
2、put 方法在插入新的 Entry 时,通过createEntry 中的 addBefore 方法来实现插入链表尾部;在覆盖已有 key 的情况下,通过 recordAccess 方法来实现将更新的entry放到链表尾部。get操作也通过recordAccess 方法将该entry放到链表尾部。多次操作后,双向链表前面的 Entry 便是最近没有使用的。
3、在每次put插入新的Entry时,都会根据你重写的removeEldestEntry方法来决定是否要删除最近最久未访问元素(默认返回false,你可以重写成当节点个数大于多少时返回true),这样当节点个数大于某个数时,就会删除最前面的 Entry(head后面的那个Entry),因为它就是最近最久未使用的 Entry。
4、使用 LinkedHashMap 实现 LRU 算法
5、LinkedHashMap 有序性原理分析【利用双向链表进行迭代输出】
6、LinkedHashMap 【JDK1.8】
6.1 构造函数【增加了双向链表的head和tail,以及访问标志accessOrder】
二、get函数
三、afterNodeXXXX命名格式的三个函数在HashMap中只是一个空实现,是专门用来让LinkedHashMap重写实现的hook函数
3.1 afterNodeAccess(Node p) { } //处理元素被访问后的情况:其功能为如果accessOrder为true,则将刚刚访问的元素移动到链表末尾,v>
3.2 afterNodeInsertion(boolean evict) { } //处理元素插入后的情况:即是否要删除最久未访问元素【根据你重写的removeEldestEntry()默认返回false,无需删除,如果你重写的返回true,则在元素插入后会删除最近最久未访问元素。】
3.3 afterNodeRemoval(Node p) { } //处理元素被删除后的情况:在HashMap.removeNode()的末尾处调用, 将e从LinkedHashMap的双向链表中删除,v>
7、总结
1、LinkedHashMap 在 HashMap 的数组加链表结构的基础上,将所有节点连成了一个双向链表。
2、put 方法在插入新的 Entry 时,通过createEntry 中的 addBefore 方法来实现插入链表尾部;在覆盖已有 key 的情况下,通过 recordAccess 方法来实现将更新的entry放到链表尾部。get操作也通过recordAccess 方法将该entry放到链表尾部。多次操作后,双向链表前面的 Entry 便是最近没有使用的。在每次put插入新的Entry时,都会根据你重写的removeEldestEntry方法来决定是否要删除最近最久未访问元素(默认返回false,你可以重写成当节点个数大于多少时返回true),这样当节点个数大于某个数时,就会删除最前面的 Entry(head后面的那个Entry),因为它就是最近最久未使用的 Entry。【实现 LRU 可以直接实现继承 LinkedHashMap 并重写removeEldestEntry 方法来设置缓存大小。JDK 中实现了 LRUCache 也可以直接使用。】
3、LinkedHashMap 的扩容比 HashMap 来的方便,因为 HashMap 需要将原来的每个链表的元素分别在新数组进行插入链化,而 LinkedHashMap 的元素都连在一个链表上,可以直接迭代然后插入。
HashMap 和双向链表合二为一即是 LinkedHashMap。所谓LinkedHashMap,其落脚点在 HashMap,因此更准确地说,它是一个将所有 Entry 节点链入一个双向链表的 HashMap。
由于 LinkedHashMap 是 HashMap 的子类,所以 LinkedHashMa p自然会拥有 HashMap 的所有特性。比如:LinkedHashMap 的元素存取过程基本与 HashMap 基本类似,只是在细节实现上稍有不同。当然,这是由 LinkedHashMap 本身的特性所决定的,因为它额外维护了一个双向链表用于保持迭代顺序。
此外,LinkedHashMap 可以很好的支持 LRU 算法,笔者在第七节便在 LinkedHashMap 的基础上实现了一个能够很好支持 LRU 的结构。
本文所有关于 LinkedHashMap 的源码都是基于 JDK 1.6 的,不同 JDK 版本之间也许会有些许差异,但不影响我们对 LinkedHashMap 的数据结构、原理等整体的把握和了解。后面会讲解 JDK1.8 对于 LinkedHashMap 的改动。
由于 LinkedHashMap 是 HashMap 的子类,所以其具有 HashMap 的所有特性,这一点在源码共用上体现的尤为突出。因此,读者在阅读本文之前,最好对 HashMap 有一个较为深入的了解和回顾,否则很可能会导致事倍功半。可以参考我之前关于 HashMap 的文章。
HashMap 是 Java Collection Framework 的重要成员,也是 Map 族(如下图所示)中我们最为常用的一种。不过遗憾的是,HashMap是无序的,也就是说,迭代 HashMap 所得到的元素顺序并不是它们最初放置到 HashMap 的顺序。
HashMap的这一缺点往往会造成诸多不便,因为在有些场景中,我们确需要用到一个可以保持插入顺序的Map。庆幸的是,JDK为我们解决了这个问题,它为 HashMap 提供了一个子类 —— LinkedHashMap。虽然 LinkedHashMap 增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。
特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将 LinkedHashMap 分为:保持插入顺序的 LinkedHashMap 和保持访问顺序的 LinkedHashMap,其中 LinkedHashMap 的默认实现是按插入顺序排序的。
本质上,HashMap 和双向链表合二为一即是 LinkedHashMap。所谓 LinkedHashMap,其落脚点在 HashMap,因此更准确地说,它是一个将所有 Entry 节点链入一个双向链表的 HashMap。
在 LinkedHashMapMap 中,所有 put 进来的 Entry 都保存在如下面第 1 个图所示的哈希表中,但由于它又额外定义了一个以 head 为头结点的双向链表(如下面第 2 个图所示)。因此对于每次 put 进来 Entry,除了将其保存到哈希表中对应的位置上之外,还会将其插入到双向链表的尾部。
更直观地,下图很好地还原了 LinkedHashMap 的原貌:HashMap 和双向链表的密切配合和分工合作造就了LinkedHashMap。特别需要注意的是,next 用于维护 HashMap 各个桶中的 Entry 链,before、after 用于维护LinkedHashMap 的双向链表,虽然它们的作用对象都是Entry,但是各自分离,是两码事儿。
其中,HashMap 与 LinkedHashMap 的 Entry 结构示意图如下图所示:
特别地,由于 LinkedHashMap 是 HashMap 的子类,所以 LinkedHashMap 自然会拥有 HashMap 的所有特性。比如:LinkedHashMap 也最多只允许一条 Entr y的键为Null(多条会覆盖),但允许多条Entry的值为Null。
此外,LinkedHashMap 也是 Map 的一个非同步的实现。此外,LinkedHashMap 还可以用来实现 LRU (Least recently used,最近最少使用)算法,这个问题会在下文的特别谈到。
LinkedHashMap 继承于 HashMap,其在 JDK 中的定义为:
public class LinkedHashMap extends HashMap implements Map {
...
}
与 HashMap 相比,LinkedHashMap 增加了两个属性用于保证迭代顺序,分别是双向链表头结点 header 和 标志位accessOrder (值为 true 时,表示按照访问顺序迭代;值为false时,表示按照插入顺序迭代)。
// 双向链表的表头元素
private transient Entry header;
// true表示按照访问顺序迭代,fasle表示按照插入顺序迭代
private final boolean accessOrder;
从下图我们可以看出,LinkedHashMap 中并增加没有额外方法。也就是说,LinkedHashMap 与 HashMap 在操作上大致相同,只是在实现细节上略有不同罢了。
LinkedHashMap 采用的 hash 算法和 HashMap 相同,但是它重新定义了 Entry。LinkedHashMap 中的 Entry 增加了两个指针 before 和 after,它们分别用于维护双向链表。特别需要注意的是,next 用于维护 HashMap 各个桶中 Entry 的连接顺序,before、after 用于维护 Entry 插入的先后顺序的,源代码如下:
private static class Entry extends HashMap.Entry {
// These fields comprise the doubly linked list used for iteration.
Entry before, after;
Entry(int hash, K key, V value, HashMap.Entry next) {
super(hash, key, value, next);
}
...
}
形象地,HashMap 与 LinkedHashMap 的 Entry 结构示意图如下图所示:
LinkedHashMap 一共提供了五个构造函数,它们都是在HashMap的构造函数的基础上实现的,除了默认空参数构造方法,下面这个构造函数包含了大部分其他构造方法使用的参数,就不一 一列举了。
该构造函数意在构造一个指定初始容量和指定负载因子的具有指定迭代顺序的 LinkedHashMap,其源码如下:
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor); // 调用HashMap对应的构造函数
this.accessOrder = accessOrder; // 迭代顺序的默认值
}
初始容量和负载因子是影响 HashMap 性能的两个重要参数。同样地,它们也是影响 LinkedHashMap 性能的两个重要参数。此外,LinkedHashMap 增加了双向链表头结点 header 和标志位 accessOrder 两个属性用于保证迭代顺序。
从上面的五种构造函数我们可以看出,无论采用何种方式创建 LinkedHashMap,其都会调用 HashMap 相应的构造函数。事实上,不管调用 HashMap 的哪个构造函数,HashMap 的构造函数都会在最后调用一个 init() 方法进行初始化,只不过这个方法在 HashMap 中是一个空实现,而在 LinkedHashMap 中重写了它用于初始化它所维护的双向链表。例如,HashMap的参数为空的构造函数以及 init() 方法的源码如下:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
void init() {
}
在 LinkedHashMap 中,它重写了 init() 方法以便初始化双向列表,源码如下://即只有一个头结点
@Override
void init() {
header = new Entry<>(-1, null, null, null);
header.before = header.after = header;
}
本质上,LinkedHashMap = HashMap + 双向链表,也就是说,HashMap 和双向链表合二为一即是 LinkedHashMap。
也可以这样理解,LinkedHashMap 在不对 HashMap 做任何改变的基础上,给 HashMap 的任意两个节点间加了两条连线 (before 指针和 after 指针),使这些节点形成一个双向链表。
在 LinkedHashMapMap 中,所有 put 进来的 Entry 都保存在 HashMap 中,但由于它又额外定义了一个以 head 为头结点的空的双向链表,因此对于每次 put 进来 Entry 还会将其插入到双向链表的尾部。
我们知道,在 HashMap 中最常用的两个操作就是:put(Key, Value) 和 get(Key)。同样地,在 LinkedHashMap 中最常用的也是这两个操作。
对于 put(Key,Value) 方法而言,LinkedHashMap 完全继承了 HashMap 的 put(Key,Value) 方法,只是对 put(Key,Value)方法所调用的 recordAccess 方法和 addEntry 方法进行了重写;对于 get(Key) 方法而言,LinkedHashMap 则直接对它进行了重写。
下面我们结合 JDK 源码看 LinkedHashMap 的存取实现。
上面谈到,LinkedHashMap 没有对 put(key,vlaue) 方法进行任何直接的修改,完全继承了 HashMap 的 put(Key,Value) 方法,其源码如下:
public V put(K key, V value) {
// 当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置
if (key == null)
return putForNullKey(value);
// 根据key的hashCode计算hash值
int hash = hash(key.hashCode());
// 计算该键值对在数组中的存储位置(哪个桶)
int i = indexFor(hash, table.length);
// 在table的第i个桶上进行迭代,寻找 key 保存的位置
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
// 判断该条链上是否存在hash值相同且key值相等的映射,若存在,则直接覆盖 value,并返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this); // LinkedHashMap重写了Entry中的recordAccess方法--- (1)
return oldValue; // 返回旧值
}
}
modCount++; // 修改次数增加1,快速失败机制
// 原Map中无该映射,将该添加至该链的链头
addEntry(hash, key, value, i); // LinkedHashMap重写了HashMap中的createEntry方法 ---- (2)
return null;
}
上述源码反映了 LinkedHashMap 与 HashMap 保存数据的过程。特别地,在 LinkedHashMap 中,它对 addEntry 方法和 Entry 的 recordAccess 方法进行了重写。下面我们对比地看一下 LinkedHashMap 和 HashMap 的 addEntry 方法的具体实现:
// LinkedHashMap中的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
// 创建新的Entry,并插入到LinkedHashMap中
createEntry(hash, key, value, bucketIndex); // 重写了HashMap中的createEntry方法
// 双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
Entry eldest = header.after;
// 如果有必要,则删除掉该近期最少使用的节点,
// 这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
// 扩容到原来的2倍
if (size >= threshold)
resize(2 * table.length);
}
}
-------------------------------我是分割线------------------------------------
// HashMap中的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
// 获取bucketIndex处的Entry
Entry e = table[bucketIndex];
// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
table[bucketIndex] = new Entry(hash, key, value, e);
// 若HashMap中元素的个数超过极限了,则容量扩大两倍
if (size++ >= threshold)
resize(2 * table.length);
}
由于 LinkedHashMap 本身维护了插入的先后顺序,因此其可以用来做缓存,中间有几步的操作就是用来支持LRU算法的,这里暂时不用去关心它。此外,在 LinkedHashMap 的 addEntry 方法中,它重写了 HashMap 中的 createEntry 方法,我们接着看一下 createEntry 方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
// 向哈希表中插入Entry,这点与HashMap中相同
// 创建新的Entry并将其链入到数组对应桶的链表的头结点处,
HashMap.Entry old = table[bucketIndex];
Entry e = new Entry(hash, key, value, old);
table[bucketIndex] = e;
// 在每次向哈希表插入Entry的同时,都会将其插入到双向链表的尾部,
// 这样就按照Entry插入LinkedHashMap的先后顺序来迭代元素(LinkedHashMap根据双向链表重写了迭代器)
// 同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,也符合LRU算法的实现
e.addBefore(header);
size++;
}
由以上源码我们可以知道,在 LinkedHashMap 中向哈希表中插入新 Entry 的同时,还会通过 Entry 的 addBefore 方法将其链入到双向链表中。其中,addBefore 方法本质上是一个双向链表的插入操作,其源码如下:
// 在双向链表中,将当前的Entry插入到existingEntry(header)的前面
private void addBefore(Entry existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
到此为止,我们分析了在 LinkedHashMap 中 put 一条键值对的完整过程。总的来说,相比 HashMap 而言,LinkedHashMap 在向哈希表添加一个键值对的同时,也会将其链入到它所维护的双向链表中,以便设定迭代顺序。
在 HashMap 中,我们知道随着 HashMap 中元素的数量越来越多,发生碰撞的概率将越来越大,所产生的子链长度就会越来越长,这样势必会影响 HashMap 的存取速度。
为了保证 HashMap 的效率,系统必须要在某个临界点进行扩容处理,该临界点就是 HashMap 中元素的数量在数值上等于 threshold(table数组长度 * 加载因子)。
但是,不得不说,扩容是一个非常耗时的过程,因为它需要重新计算这些元素在新 table 数组中的位置并进行复制处理。所以,如果我们能够提前预知 HashMap 中元素的个数,那么在构造 HashMap 时预设元素的个数能够有效的提高HashMap 的性能。
同样的问题也存在于 LinkedHashMap 中,因为 LinkedHashMap 本来就是一个 HashMap,只是它还将所有 Entry 节点链入到了一个双向链表中。LinkedHashMap 完全继承了 HashMap 的 resize() 方法,只是对它所调用的 transfer 方法进行了重写。我们先看 resize() 方法源码:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 若 oldCapacity 已达到最大值,直接将 threshold 设为 Integer.MAX_VALUE
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return; // 直接返回
}
// 否则,创建一个更大的数组
Entry[] newTable = new Entry[newCapacity];
// 将每条Entry重新哈希到新的数组中
transfer(newTable); // LinkedHashMap对它所调用的transfer方法进行了重写
table = newTable;
threshold = (int)(newCapacity * loadFactor); // 重新设定 threshold
}
从上面代码中我们可以看出,Map 扩容操作的核心在于重哈希。所谓重哈希是指重新计算原 HashMap 中的元素在新table 数组中的位置并进行复制处理的过程。鉴于性能和 LinkedHashMap 自身特点的考量,LinkedHashMap 对重哈希过程(transfer方法)进行了重写,源码如下:
void transfer(HashMap.Entry[] newTable) {
int newCapacity = newTable.length;
// 与HashMap相比,借助于双向链表的特点进行重哈希使得代码更加简洁
for (Entry e = header.after; e != header; e = e.after) {
int index = indexFor(e.hash, newCapacity); // 计算每个Entry所在的桶
// 将其链入桶中的链表
e.next = newTable[index];
newTable[index] = e;
}
}
如上述源码所示,LinkedHashMap 借助于自身维护的双向链表轻松地实现了重哈希操作。
相对于 LinkedHashMap 的存储而言,读取就显得比较简单了。LinkedHashMap 中重写了 HashMap 中的 get 方法,源码如下:
// LinkedHashMap 中的方法
public V get(Object key) {
// 根据key获取对应的Entry,若没有这样的Entry,则返回null
Entry e = (Entry)getEntry(key);
if (e == null) // 若不存在这样的Entry,直接返回
return null;
e.recordAccess(this);
return e.value;
}
// hashMap 中的方法
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
在 LinkedHashMap 的 get 方法中,通过调用 HashMap 中的 getEntry 方法获取 Entry 对象。注意这里的 recordAccess方法,如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做;如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将 e 移到链表的末尾处,笔者会在后文专门阐述这个问题。
另外,同样地,调用 LinkedHashMap 的 get(Object key) 方法后,若返回值是 NULL,则也存在如下两种可能:
该 key 对应的值就是 null 或者 HashMap 中不存在该 key。
LRU:Least Recently Used,最近最少使用算法。
到此为止,我们已经分析完了 LinkedHashMap 的存取实现,这与 HashMap 大体相同。LinkedHashMap 区别于HashMap 最大的一个不同点是,前者是有序的,而后者是无序的。为此,LinkedHashMap 增加了两个属性用于保证顺序,分别是双向链表头结点 header 和标志位 accessOrder。
我们知道,header 是 LinkedHashMap 所维护的双向链表的头结点,而 accessOrder 用于决定具体的迭代顺序。实际上,accessOrder 标志位的作用可不像我们描述的这样简单,我们接下来仔细分析一波~
我们知道,当 accessOrder 标志位为 true 时,表示双向链表中的元素按照访问的先后顺序排列,可以看到,虽然 Entry 插入链表的顺序依然是按照其 put 到 LinkedHashMap 中的顺序,但 put 和 get 方法均有调用 recordAccess 方法(put 方法在 key 相同时会调用)。
recordAccess 方法判断 accessOrder 是否为 true,如果是,则将当前访问的 Entry(put 进来的 Entry 或 get 出来的 Entry)移到双向链表的尾部(key 不相同时,put 新 Entry时,会调用 addEntry,它会调用 createEntry,该方法同样将新插入的元素放入到双向链表的尾部,既符合插入的先后顺序,又符合访问的先后顺序,因为这时该 Entry 也被访问了);
当标志位 accessOrder 的值为 false 时,表示双向链表中的元素按照 Entry 插入到 LinkedHashMap 中的先后顺序排序,即每次 put 到 LinkedHashMap 中的 Entry 都放在双向链表的尾部,这样遍历双向链表时,Entry 的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序。因此,当标志位 accessOrder 的值为 false 时,虽然也会调用 recordAccess 方法,但不做任何操作。
// 将key/value添加到LinkedHashMap中
public V put(K key, V value) {
// 若key为null,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若key不为null,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
// 若key对已经存在,则用新的value取代旧的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若key不存在,则将key/value键值对添加到table中
modCount++;
// 将key/value键值对添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
从上述源码我们可以看到,当要 put 进来的 Entry 的 key 在哈希表中已经在存在时,会调用 Entry 的 recordAccess 方法;当该 key 不存在时,则会调用 addEntry 方法将新的 Entry 插入到对应桶的单链表的头部。我们先来看 recordAccess 方法:
void recordAccess(HashMap m) {
LinkedHashMap lm = (LinkedHashMap)m;
// 如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部,
// 如果是按照插入的先后顺序排序,则不做任何事情。
if (lm.accessOrder) {
lm.modCount++;
// 移除当前访问的Entry
remove();
// 将当前访问的Entry插入到链表的尾部
addBefore(lm.header);
}
}
LinkedHashMap 重写了 HashMap 中的 recordAccess 方法(HashMap中该方法为空),当调用父类的 put 方法时,在发现 key 已经存在时,会调用该方法;当调用自己的 get 方法时,也会调用到该方法。
该方法提供了 LRU 算法的实现,它将最近使用的 Entry 放到双向循环链表的尾部。也就是说,当 accessOrder 为 true 时,get 方法和 put 方法都会调用 recordAccess 方法使得最近使用的Entry移到双向链表的末尾;当 accessOrder 为默认值false 时,从源码中可以看出 recordAccess 方法什么也不会做。我们反过头来,再看一下 addEntry 方法:
// LinkedHashMap中的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
// 创建新的Entry,并插入到LinkedHashMap中
createEntry(hash, key, value, bucketIndex); // 重写了HashMap中的createEntry方法
// 双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
Entry eldest = header.after;
// 如果有必要,则删除掉该近期最少使用的节点,
// 这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
// 扩容到原来的2倍
if (size >= threshold)
resize(2 * table.length);
}
}
// LinkedHashMap中的createEntry方法
void createEntry(int hash, K key, V value, int bucketIndex) {
// 向哈希表中插入Entry,这点与HashMap中相同
// 创建新的Entry并将其链入到数组对应桶的链表的头结点处,
HashMap.Entry old = table[bucketIndex];
Entry e = new Entry(hash, key, value, old);
table[bucketIndex] = e;
// 在每次向哈希表插入Entry的同时,都会将其插入到双向链表的尾部,
// 这样就按照Entry插入LinkedHashMap的先后顺序来迭代元素(LinkedHashMap根据双向链表重写了迭代器)
// 同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,也符合LRU算法的实现
e.addBefore(header);
size++;
}
同样是将新的 Entry 链入到 table 中对应桶中的单链表中,但可以在 createEntry 方法中看出,同时也会把新 put 进来的 Entry 插入到了双向链表的尾部。
从插入顺序的层面来说,新的 Entry 插入到双向链表的尾部可以实现按照插入的先后顺序来迭代 Entry,而从访问顺序的层面来说,新 put 进来的 Entry 又是最近访问的 Entry,也应该将其放在双向链表的尾部。在上面的 addEntry 方法中还调用了 removeEldestEntry 方法,该方法源码如下:
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
该方法是用来被重写的,一般地,如果用 LinkedHashMap 实现 LRU 算法,就要重写该方法。比如可以将该方法覆写为如果设定的内存已满,则返回 true,这样当再次向 LinkedHashMap 中 putEntry 时,在调用的 addEntry 方法中便会将近期最少使用的节点删除掉(header后的那个节点)。在第七节,笔者便重写了该方法并实现了一个名副其实的 LRU 结构。
public V get(Object key) {
// 根据key获取对应的Entry,若没有这样的Entry,则返回null
Entry e = (Entry)getEntry(key);
if (e == null) // 若不存在这样的Entry,直接返回
return null;
e.recordAccess(this);
return e.value;
}
在 LinkedHashMap 中进行读取操作时,一样也会调用 recordAccess 方法。上面笔者已经表述的很清楚了,此不赘述。
如下所示,笔者使用 LinkedHashMap 实现一个符合 LRU 算法的数据结构,该结构最多可以缓存 6 个元素,但元素多于 6 个时,会自动删除最近最久没有被使用的元素,如下所示:
package com.zju.lru;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 模拟LRU算法
*/
public class LRU extends LinkedHashMap implements Map {
private static final long serialVersionUID = 1L;
public LRU(int initialCapacity, float loadFactor, boolean accessOrder){
super(initialCapacity, loadFactor, accessOrder);
}
// 重写LinkdHashMap中的removeEldestEntry方法,当LRU中元素多于6个时,删除最不经常使用的元素
@Override
public boolean removeEldestEntry(Map.Entry eldest){
if(size() > 6){
return true;
}
return false;
}
// 测试
public static void main(String[] args) {
LRU lru = new LRU(16, 0.75f, true);
String str = "abcdefghijkl";
for (int i = 0; i < str.length(); i++) {
lru.put(str.charAt(i), i); //key:str.charAt(i) value:i
}
System.out.println("LRU中key为h的Entry的值为:" + lru.get('h'));
System.out.println("LRU的大小:" + lru.size());
System.out.println("LRU:" + lru);
}
}
如前文所述,LinkedHashMap 增加了双向链表头结点 header 和 标志位 accessOrder 两个属性用于保证迭代顺序。但是要想真正实现其有序性,还差临门一脚,那就是重写 HashMap 的迭代器,其源码实现如下:
private abstract class LinkedHashIterator implements Iterator {
Entry nextEntry = header.after;
Entry lastReturned = null;
int expectedModCount = modCount;
// 根据双向列表判断
public boolean hasNext() {
return nextEntry != header;
}
public void remove() {
if (lastReturned == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
LinkedHashMap.this.remove(lastReturned.key);
lastReturned = null;
expectedModCount = modCount;
}
// 迭代输出双向链表各节点
Entry nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (nextEntry == header)
throw new NoSuchElementException();
Entry e = lastReturned = nextEntry;
nextEntry = e.after;
return e;
}
}
// Key 迭代器,KeySet
private class KeyIterator extends LinkedHashIterator {
public K next() { return nextEntry().getKey(); }
}
// Value 迭代器,Values(Collection)
private class ValueIterator extends LinkedHashIterator {
public V next() { return nextEntry().value; }
}
// Entry 迭代器,EntrySet
private class EntryIterator extends LinkedHashIterator> {
public Map.Entry next() { return nextEntry(); }
}
从上述代码中我们可以知道,LinkedHashMap 重写了 HashMap 的迭代器,它使用其维护的双向链表进行迭代输出。
参考自:http://www.importnew.com/29828.html
原文是基于 JDK1.6 的实现,实际上 JDK1.8 对其进行了改动。 首先它删除了addEntry,createEnrty等方法(事实上是hashMap 的改动影响了它而已)。
LinkedHashMap 同样使用了大部分 HashMap 的增删改查方法。 新版本 LinkedHashMap 主要是通过对 HashMap 内置几个方法重写来实现 LRU 的。
LinkedHashMap继承HashMap并实现了Map接口,同时具有可预测的迭代顺序(按照插入顺序排序)。它与HashMap的不同之处在于,维护了一条贯穿其全部Entry的双向链表(因为额外维护了链表的关系,性能上要略差于HashMap,不过集合视图的遍历时间与元素数量成正比,而HashMap是与buckets数组的长度成正比的),可以认为它是散列表与链表的结合。
你也可以通过构造函数来构造一个迭代顺序为访问顺序(accessOrder设为true)的LinkedHashMap,这个访问顺序指的是按照最近被访问的Entry的顺序进行排序(从最近最少访问到最近最多访问)。基于这点可以简单实现一个采用LRU(Least Recently Used)策略的缓存。
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry tail;
/**
* 迭代顺序模式的标记位,如果为true,采用访问排序,否则,采用插入顺序
* 默认插入顺序(构造函数中默认设置为false)
*/
final boolean accessOrder;
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
LinkedHashMap的Entry实现也继承自HashMap,只不过多了指向前后的两个指针。
static class Entry extends HashMap.Node {
Entry before, after;
Entry(int hash, K key, V value, Node next) {
super(hash, key, value, next);
}
}
LinkedHashMap复用了HashMap的大部分代码,所以它的查找实现是非常简单的,唯一稍微复杂点的操作是保证访问顺序。【因为hashMap的get没有包含afterNodeAccess函数,所以Linked】
public V get(Object key) {
Node e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
还记得这些afterNodeXXXX命名格式的函数吗?我们之前已经在HashMap中见识过了,这些函数在HashMap中只是一个空实现,是专门用来让LinkedHashMap重写实现的hook函数。
void afterNodeAccess(Node p) { } //处理元素被访问后的情况:
void afterNodeInsertion(boolean evict) { } //处理元素插入后的情况:即是否要删除最久未访问元素
void afterNodeRemoval(Node p) { } //处理元素被删除后的情况:
它们在HashMap的put函数中的位置:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// 1、第一次 put 值的时候,resize()初始化数组长度从 null 初始化到默认的 16 或自定义的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、找到具体的数组下标(n - 1) & hash,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 3、数组该位置有数据,判断是红黑树还是链表【插到链表最后面,如果是第9个,链表转为红黑树】
Node e; K k;
// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
/* 为什么get和put先判断p.hash==hash,下面的if条件中去掉hash的比较逻辑也是正确?
因为hash的比较是两个整数的比较,比较的代价相对较小,
key是泛型,对象的比较比整数比较代价大,所以先比较hash,hash相等再比较key
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果该节点是代表红黑树的节点,调用红黑树的插值方法
//如果已经存在该key的TreeNode,则返回该TreeNode,否则返回null
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
// 到这里,说明数组该位置上是一个链表
for (int binCount = 0; ; ++binCount) {
/* 遍历到了链表最后一个元素,接下来执行链表的插入操作,先封装为Node,
再插入p指向的是链表最后一个节点,将待插入的Node置为p.next,就完成了单链表的插入 */
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 说明存在旧值的key与要插入的key"相等"
// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
/* 这个函数的默认实现是“空”,即这个函数默认什么操作都不执行,那为什么要有它呢?
这其实是个hook/钩子函数,主要要在LinkedHashMap(HashMap子类)中使用,
LinkedHashMap重写了这个函数。以后会有讲解LinkedHashMap的文章。*/
afterNodeAccess(e);
return oldValue;
}
}
// 如果是第一次插入key这个键,就会执行到这里
++modCount;
// 4、如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
if (++size > threshold)
resize();
// 这也是一个hook函数,作用和afterNodeAccess一样
afterNodeInsertion(evict);
return null;
}
// HashMap.get()没有调用此函数,所以LinkedHashMap重写了get()
// get()与put()都会调用afterNodeAccess()来保证访问顺序
// 将e移动到tail,代表最近访问到的节点
void afterNodeAccess(Node e) { // move node to last
LinkedHashMap.Entry last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
removeEldestEntry()
默认返回false,无需删除,如果你重写的返回true,则在元素插入后会删除最近最久未访问元素。】
// 在HashMap.putVal()的末尾处调用
// evict是一个模式标记,如果为false代表buckets数组处于创建模式
// HashMap.put()函数对此标记设置为true
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry first;
// LinkedHashMap.removeEldestEntry()永远返回false
// 避免了最年长元素被删除的可能(就像一个普通的Map一样)
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
一个比较合理的实现示例:(LRU)
protected boolean removeEldestEntry(Map.Entry eldest){
return size() > MAX_SIZE;
}
// 在HashMap.removeNode()的末尾处调用
// 将e从LinkedHashMap的双向链表中删除
void afterNodeRemoval(Node e) { // 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;
}
另外 JDK1.8 的 HashMap 在链表长度超过 8 时自动转为红黑树,会按顺序插入链表中的元素,可以自定义比较器来定义节点的插入顺序。
JDK1.8 的 LinkedHashMap 同样会使用这一特性,当变为红黑树以后,节点的先后顺序同样是插入红黑树的顺序,其双向链表的性质没有改变,只是原来 HashMap 的链表变成了红黑树而已,在此不要混淆。
本文从 LinkedHashMap 的数据结构,以及源码分析,到最后的 LRU 缓存实现,比较深入地剖析了 LinkedHashMap 的底层原理。
总结以下几点: