目录
3.3.1 afterNodeRemoval(Node e)方法,v>
3.3.2 afterNodeInsertion(boolean evict)方法
3.3.3 afterNodeAccess(Node e)方法,v>
一、介绍
1.1 LinkedHashMap简介
LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。能保证元素按插入的顺序访问,也能以访问顺序访问,在一些场景下,该特性很有用,比如实现LRU缓存策略。
在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。LinkedHashMap可以看成是 LinkedList + HashMap。
当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap了。下文是基于JDK1.6的实现,实际上JDK1.8对其进行了改动,但实际上只是HashMap在jdk1.8版本有了较大变动,LinkedHashMap的实现原理还是没有太大变化的。
1.2 继承关系
方法原型:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
LinkedHashMap继承自HashMap,它的多种操作都是建立在HashMap操作的基础上的。同HashMap不同的是,LinkedHashMap维护了一个Entry的双向链表,保证了插入的Entry中的顺序。这也是Linked的含义。继承关系图如下:
LinkedHashMap继承HashMap,拥有HashMap的所有特性,并且额外增加了按一定顺序访问的特性。
二、数据结构
2.1 成员属性
/**
* 双向链表头节点,旧数据存在头节点。
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 双向链表尾节点,新数据存在尾节点。
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 是否按访问顺序排序。默认为false按插入顺序存储元素,如果是true则按访问顺序存储元素(按照访问顺序迭代,支持实现LRU算法时)。
* LinkedHashMap可以维持插入的顺序,但是这个顺序并不是不变的,它可以被get操作打乱。
* 该变量控制了是否在get操作后将刚刚get到的节点重新移动到链表的尾部
*/
final boolean accessOrder;
其他的成员属性就是直接继承HashMap中的。
2.2 内部类
// 位于LinkedHashMap中的内部类。存储数据的节点,继承自HashMap的Node类,next用于单链表结构维护,是继承自HashMap.Node的,before和after用于双向链表存储所有元素,是LinkedHashMap新加入的。
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);
}
}
我们来看一下HashMap中的Node内部类,LinkedHashMap中的内部类Entry就是继承自它。
// 位于HashMap中的Node类。HashMap中的Node类是继承的Map.Entry<K, V>
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
}
在LinkedHashMap中,存储数据的节点就是Entry类(LinkedHashMap.Entry<K, V>),这个类继承了HashMap的Node类,在此基础上又加了before和after这两个指针用来维护双向链表结构。
- before、after用于维护Entry插入的先后顺序
- next用于维护HashMap各个桶中Entry的连接顺序
2.3 存储结构
我们知道HashMap使用(数组 + 单链表 + 红黑树)的存储结构,那LinkedHashMap是怎么存储的呢?
通过上面的继承体系,我们知道它继承了HashMap,所以它的内部也有这三种结构,但是它还额外添加了一种“双向链表”的结构存储所有元素的顺序。
添加删除元素的时候需要同时维护在HashMap中的存储,也要维护在LinkedList中的存储,所以性能上来说会比HashMap稍慢。
注意:JDK1.8中引入了红黑树,如下:
上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。
通过上面的图我们可以知道,LinkedHashMap首先内部数据都是完全按照HashMap的组织方式来存储的(数组 + 单链表 + 红黑树),这是因为LinkedHashMap继承了HashMap,然后LinkedHashMap在此基础上,将存储数据的每个节点又用一个双向链表串联了起来,这样就可以按照顺序来遍历每一个节点了。所以LinkedHashMap存储数据的节点中,既需要有维护挂在数组上的单链表结构的next指针,又需要有将所有节点按照一定顺序串连起来构成双链表的after、before两个指针。
LinkedHashMap和HashMap的Entry结构图:
三、源码分析
3.1 构造方法
相比于Hashmap,LinkedHashMap并没有增加构造方法。
// 传入的参数为初始容量,加载因子,调用了父类的构造方法,按照插入顺序
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
// 传入的参数的初始容量,调用父类的构造方法,取得键值对的顺序是插入顺序
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
// 无参构造,调用父类的构造方法,取得键值对的顺序是插入顺序
public LinkedHashMap() {
super();
accessOrder = false;
}
// 传入的参数是一个Map的集合,调用父类的构造方法,构造一个与指定Map具有相同映射的 LinkedHashMap,取得键值对的顺序是插入顺序
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
// 传入的参数为初始容量,加载因子,有序性标识(键值对保持顺序),调用了父类的构造方法
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
前四个构造方法accessOrder都等于false,说明LinkedHashMap默认是按插入顺序存储元素,构造函数如果不明确传入accessOrder的话,默认都是按插入序的。
最后一个构造方法accessOrder从构造方法参数传入,如果传入true,则就实现了按访问顺序存储元素,这也是实现LRU缓存策略的关键。
3.2 LinkedHashMap的init()方法
由 LinkedHashMap的五种构造方法可知:
- 无论采用何种方式创建LinkedHashMap,其都会调用HashMap相应的构造函数
- 不管调用HashMap的哪个构造函数,HashMap的构造函数都会在最后调用一个init()方法进行初始化
- init()方法在HashMap中是一个空实现,而在LinkedHashMap中重写了它,用于初始化它所维护的双向链表
// Hashmap
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
// LinkedHashmap
@Override
void init() {
// header初始化
// hash为-1,其他的参数均为null
// 也就是说这个header不在数组中
// 只是用来标志开始元素和标志结束元素的
header = new Entry<>(-1, null, null, null);
header.before = header.after = header;
}
3.3 维护链表的操作
维护链表主要使用三个方法。
afterNodeRemoval,afterNodeInsertion,afterNodeAccess。这三个方法的主要作用是,在删除,插入,获取节点之后,对链表进行维护。简单来说,在这三个方法中会执行双向链表的操作。
3.3.1 afterNodeRemoval(Node<K,V> e)方法
在节点被删除之后调用的方法。
//在节点删除后,维护链表,传入删除的节点
void afterNodeRemoval(Node<K,V> e) { // unlink
// p指向待删除元素,b指向前驱,a指向后驱
LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e,
b = p.before,
a = p.after;
// 这里执行双向链表删除p节点操作,很简单。
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
本质就是一个把节点从双向链表中删除的方法。
3.3.2 afterNodeInsertion(boolean evict)方法
这是一个很奇葩的方法,虽然名字是在插入之后进行的维护链表的操作,但是默认实际上这个什么都没做。
该方法在HashMap中的putVal()方法中被调用,可以看到HashMap中这个方法的实现为空。
void afterNodeInsertion(boolean evict) { // evict,驱逐的意思。
LinkedHashMap.Entry<K,V> first;
// removeEldestEntry(first)默认返回false,所以afterNodeInsertion这个方法其实并不会执行
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
(1)如果evict为true,且头节点不为空,且确定移除最老的元素,那么就调用HashMap.removeNode()把头节点移除(这里的头节点是双向链表的头节点,而不是某个桶中的第一个元素);
(2)HashMap.removeNode()从HashMap中把这个节点移除之后,会调用afterNodeRemoval()方法;
(3)afterNodeRemoval()方法在LinkedHashMap中也有实现,用来在移除元素后修改双向链表;
(4)默认removeEldestEntry()方法返回false,也就是不删除元素。
为什么不执行也可以呢,这个要到put操作的时候就能看出来了。
那么removeEldestEntry这个方法有什么用呢,看名字可以知道是删除最久远的节点,也就是head节点,这个方法实际是给我们自己扩展的。默认是没有用的,接下来实现LRU的代码中将可以看到它的作用。
3.3.3 afterNodeAccess(Node<K,V> e)方法
在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾。
// 在节点被访问后根据accessOrder判断是否需要调整链表顺序。传入的是访问的节点
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// 如果accessOrder为true,并且访问的节点不是尾节点
// 如果accessOrder为false,什么都不做
if (accessOrder && (last = tail) != e) {
// p指向待移动元素(被访问的节点),b指向前驱,a指向后驱
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 把p节点从双向链表中移除
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
// 把p节点放到双向链表的末尾
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
// 尾节点等于p
tail = p;
// 增加修改次数,保证并发读安全。
++modCount;
}
}
(1)如果accessOrder为true,并且访问的节点不是尾节点;
(2)从双向链表中移除访问的节点;
(3)把访问的节点加到双向链表的末尾;(末尾为最新访问的元素)
3.4 get操作
LinkedHashMap重写了HashMap的get方法,如下:
/**
* 调用hashmap的getNode方法获取到值之后,维护双向链表
* @param key
* @return
*/
public V get(Object key) {
Node<K,V> e;
// 使用HashMap的getNode方法来获取节点
if ((e = getNode(hash(key), key)) == null)
return null;
// 访问完了数据之后,去根据accessOrder来决定是否调整双向链表
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
如果查找到了元素,且accessOrder为true,则调用afterNodeAccess()方法把访问的节点移到双向链表的末尾。
recordAccess方法:
- 如果链表中元素的排序规则是按照插入的先后顺序排序的话(accessOrder=false),该方法什么也不做
- 如果链表中元素的排序规则是按照访问的先后顺序排序的话(accessOrder=true),则将e移到链表的末尾处
调用LinkedHashMap的get(Object key)方法,返回值是 NULL,有如下两种可能:
- 该 key 对应的值就是 null
- HashMap 中不存在该 key
3.4 put操作
LinkedHashMap没有重写HashMap的put方法,所以执行put操作的时候,还是使用的是HashMap的put方法。那么这样如何保证链表的逻辑呢?
总体来看,LinkedHashMap中put的过程跟HashMap的put类似。当put元素时,不但要把它加入到HashMap中去,还要加入到双向链表中,所以可以看出LinkedHashMap就是HashMap+双向链表。
LinkedHashMap完全继承了HashMap的 put(Key,Value) 方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //数组为null时
inflateTable(threshold); //给数组根据阈值分配内容空间
}
if (key == null) //key为null时
return putForNullKey(value);
int hash = hash(key); //通过key计算hash
int i = indexFor(hash, table.length); //计算在数组中的索引位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//使用的是LinkedHashMap重写的方法
1
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//addEntry调用的是LinkedHashMap重写了的方法
2
addEntry(hash, key, value, i);
return null;
}
- 只是对put(Key,Value)方法所调用的recordAccess方法和addEntry方法进行了重写
- addEntry方法中还调用了removeEldestEntry方法,该方法是用来被重写的,一般如果用LinkedHashmap实现LRU算法,就要重写该方法
- 比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中putEntry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)
注意:下面的这几个覆写的方法在jdk1.8及以后就没有了。
LinkedHashMap覆写了HashMap中的recordAccess和addEntry方法:
void recordAccess(HashMap<K,V> m) { // 执行顺序1
// 将传入的HashMap类型的m强制转换成LinkedHashMap类型的
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
// accessOrder默认的是false,当accessOrder为true时进入
if (lm.accessOrder) {
lm.modCount++;
//移除当前节点
remove();
// 执行顺序3
addBefore(lm.header);
}
}
void addEntry(int hash, K key, V value, int bucketIndex) { // 执行顺序2
// 重写了HashMap中的createEntry方法
createEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after; //还是header自身
if (removeEldestEntry(eldest)) { // 执行顺序4
removeEntryForKey(eldest.key);
} else {
//扩容到原来的2倍
if (size >= threshold) // 执行顺序5
resize(2 * table.length);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { // 执行顺序4
return false;
}
在LinkedHashMap的addEntry方法中,它重写了HashMap中的createEntry方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
// 向哈希表中插入Entry,这点与HashMap中相同
// 创建新的Entry并将其链入到数组对应桶的链表的头结点处,
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
// 在每次向哈希表插入Entry的同时,都会将其插入到双向链表的尾部,
// 这样就按照Entry插入LinkedHashMap的先后顺序来迭代元素
// (LinkedHashMap根据双向链表重写了迭代器)
// 同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,也符合LRU算法的实现
e.addBefore(header);
size++;
}
在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。addBefore方法本质上是一个双向链表的插入操作:
//插入有序不做处理,在访问有序做相应处理:addBefore(将当前节点插到header的前面)
private void addBefore(Entry<K,V> existingEntry) { 3
after = existingEntry; //existingEntry即为header
before = existingEntry.before;
before.after = this; //this即为要插入的节点
after.before = this;
}
3.5 remove操作
和put操作一样,也是直接使用HashMap的方法来完成的:
final Node<K, V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K, V>[] tab;
Node<K, V> p;
int n, index;
//判断table是否为空,该key对应的桶是否为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K, V> node = null, e;
K k;
V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//到这里了其实node就已经指向了要删除的节点了
//matchValue的作用是指现在是否需要值匹配。因为可能没有传入value,所以判断一下
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
//调用维护双向链表的操作
afterNodeRemoval(node);
return node;
}
}
return null;
}
3.6 扩容操作
在HashMap的put方法中,如果发现前元素个数超过了扩容阀值时,会调用resize方法。
LinkedHashMap完全继承了HashMap的resize()方法,只是对它所调用的transfer方法进行了重写。
Map扩容操作的核心在于重哈希。重哈希是指重新计算原HashMap中的元素在新table数组中的位置并进行复制处理的过程,鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对重哈希过程(transfer方法)进行了重写。
void resize(int newCapacity) { // 执行步骤5
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重新哈希到新的数组中
// 执行步骤6
transfer(newTable); //LinkedHashMap对它所调用的transfer方法进行了重写
table = newTable;
threshold = (int)(newCapacity * loadFactor); // 重新设定 threshold
}
LinkedHashMap重写了transfer方法,数据的迁移,它的实现如下:
void transfer(HashMap.Entry[] newTable) { // 执行步骤6
int newCapacity = newTable.length;
// 与HashMap相比,借助于双向链表的特点进行重哈希使得代码更加简洁
for (Entry<K,V> e = header.after; e != header; e = e.after) {
int index = indexFor(e.hash, newCapacity); // 计算每个Entry所在的桶
// 将其链入桶中的链表
e.next = newTable[index];
newTable[index] = e;
}
}
可以看出,LinkedHashMap扩容时,数据的再散列和HashMap是不一样的。
HashMap是先遍历旧table,再遍历旧table中每个元素的单向链表,取得Entry以后,重新计算hash值,然后存放到新table的对应位置。
LinkedHashMap是遍历的双向链表,取得每一个Entry,然后重新计算hash值,然后存放到新table的对应位置。
从遍历的效率来说,遍历双向链表的效率要高于遍历table,因为遍历双向链表是N次(N为元素个数);而遍历table是N+table的空余个数(N为元素个数)。
四、LinkedListMap与LRU(Least recently used,最近最少使用)算法
当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方法,但不做任何操作
4.1 利用LinkedListMap实现LRU
LRU,即最近最少使用,优先淘汰最近最少使用的元素。LRU中保存的数据如果满了,那么就会将最近最少使用的数据删除。
LinkedHashMap通过使accessOrder为true,可以满足这种特性。所以可以使用LinkedHashMap实现LRU算法。
测试链接:146. LRU 缓存 - 力扣(LeetCode)
实现代码:
class LRUCache extends LinkedHashMap {
// 设置容器总容量,即规定容器中有多少个元素是算是满了
private int capacity;
public LRUCache(int capacity) {
// accessOrder为true
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return (int)super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
// 重写removeEldestEntry()方法设置何时移除旧元素
protected boolean removeEldestEntry(Map.Entry eldest) {
// 当元素个数大于了缓存的容量, 就移除元素
return size() > capacity;
}
}
这里重写了removeEldestEntry方法,然后removeEldestEntry方法在afterNodeInsertion中被调用,如果这个方法返回真,那么就会删除head指向的节点。根据每次get的节点都会放到尾部的特性,所以head指向的节点就是最久没有使用到的节点,所以可以删除。由于我们每次put完(HashMap#putVal())都会调用这个afterNodeInsertion方法,所以可以上面的设计可以使put过后如果size超了,将删除最久没有使用的一个节点,从而腾出空间给新的节点。
五、总结
- LinkedHashMap和HashMap是Java Collection Framework 的重要成员,也是Map家族成员;
- LinkedHashMap继承自HashMap,具有HashMap的所有特性;
- LinkedHashMap是HashMap和双向链表的合二为一,即一个将所有Entry节点链入一个双向链表的HashMap(LinkedHashMap = HashMap + 双向链表),将所有节点连成了一个双向链表;
- LinkedHashMap和HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null;
- LinkedHashMap 也是 Map 的一个非同步的实现;
- LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;
- 在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中
- LinkedHashMap的扩容比HashMap来的方便,因为HashMap需要将原来的每个链表的元素分别在新数组进行反向插入链化,而LinkedHashMap的元素都连在一个链表上,可以直接迭代然后插入;
- 在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写
- 在读取操作上,LinkedHashMap中重写了HashMap中的get方法(加入recordAccess方法,重写transfer方法),通过HashMap中的getEntry方法获取Entry对象;
- HashMap是无序的,LinkedHashMap通过维护一个额外的双向链表保证了迭代顺序;
- 迭代顺序可以是插入顺序,也可以是访问顺序(即根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap和保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的)
- 如果accessOrder为false,则可以按插入元素的顺序遍历元素;当主动传入的accessOrder参数为false时, 使用put方法时,新加入元素不仅加入哈希桶中,还被加入双向链表末尾,get方法使用时不会把元素放到双向链表尾部
- 如果accessOrder为true,则可以按访问元素的顺序遍历元素;当主动传入的accessOrder参数为true时,使用put方法新加入的元素,如果遇到了哈希冲突,并且对key值相同的元素进行了替换,就会被放在双向链表的尾部,当元素超过上限且removeEldestEntry方法返回true时,直接删除最早元素以便新元素插入。如果没有冲突直接放入,同样加入到链表尾部。使用get方法时会把get到的元素放入双向链表尾部
- 默认的LinkedHashMap并不会移除旧元素(removeEldestEntry方法默认返回false),如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;
- LinkedHashMap很好的支持LRU算法。LinkedHashMap的removeEldestEntry方法默认返回false,要实现LRU很重要的一点就是集合满时要将最久未访问的元素删除,在LinkedHashMap中这个元素就是头指针指向的元素。实现LRU可以直接实现继承LinkedHashMap并重写removeEldestEntry方法来设置缓存大小。jdk中实现了LRUCache也可以直接使用
一句话总结,LinkedHashMap就是HashMap中将其node维护成了一个双向链表。只要搞懂了HashMap就可以很容易搞懂LinkedHashMap。
相关文章:【Java集合】HashMap系列(一)——底层数据结构分析
【Java集合】HashMap系列(二)——底层源码分析
【Java集合】HashMap系列(三)——TreeNode内部类源码分析
【Java集合】HashMap系列(四)——JDK1.7和JDK1.8中的并发问题的分析
【Java集合】一文快速了解HashMap底层原理