};
linkedHashMap.put(1, 1);
linkedHashMap.put(2, 2);
//调用get进行排序
linkedHashMap.get(1);
for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {
Log.e(“TAG”, “key->” + a.getKey() + “”);
Log.e(“TAG”, “value->” + a.getValue() + “”);
}
}
}
2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: key->1
2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: value->1
2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: key->2
2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: value->2
还是以上的例子,我们在put完以后调用了get(1)再打印日志发现并没有按我们预想的顺序将1放在最后,因为LinkedHashMap按访问顺序排序默认是关闭的,可通过构造函数实例化的时候打开。比如这样:例 4
public class LinkedHashMapActivity extends AppCompatActivity {
LinkedHashMap<Integer, Integer> linkedHashMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_linked_hash_map);
//主要看最后这个参数,默认是false。
linkedHashMap = new LinkedHashMap<Integer, Integer>(2,0.75f,true) {
@Override
protected boolean removeEldestEntry(Entry eldest) {
return linkedHashMap.size() > 2;
}
};
linkedHashMap.put(1, 1);
linkedHashMap.put(2, 2);
//调用get进行排序
linkedHashMap.get(1);
for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {
Log.e(“TAG”, “key->” + a.getKey() + “”);
Log.e(“TAG”, “value->” + a.getValue() + “”);
}
}
}
2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: key->2
2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: value->2
2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: key->1
2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: value->1
在初始化过程开启访问排序后,在通过日志发现,1已经排在的最后,达到了访问排序的效果。以上的几个例子是LinkedHashMap的基本使用,但是具体为什么能达到这种效果,接下来我们深入源码了解LinkedHashMap的原理。
LinkedHashMap的原理是什么?
怎么实现的?
首先我们了解LinkedHashMap的继承关系图,实线代表继承关系,虚线代表实现接口。
以上的继承关系中可以发现LinkedHashMap继承于HashMap,因此LinkedHashMap底层也是基于链表,如果JDK1.8以上就是链表+红黑树。而LinkedHashMap的不同之处就在于它又多维护了一个双向链表。
LinkedHashMap的双向链表对象都包含什么属性?
LinkedHashMap的双向链表对象是通过LinkedHashMapEntry对象来维护的,具体我们看下源码。
//LinkedHashMap.LinkedHashMapEntry
//继承于 HashMap.Node,获得普通链表的能力,同时通过内部属性 before, after;维护双向链表。
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);
}
}
// HashMap.Node
// 链表元素对象
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
//元素的key
final K key;
//值value
V value;
//下一个元素的数据
Node<K,V> next;
…省略若干代码
}
// HashMap.TreeNode
//红黑树对象,主要看继承关系,如需了解内部逻辑可通过HashMap原理进行了解。
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
以上源码中LinkedHashMapEntry就是LinkedHashMap中的每一个键值对对象,其中通过before, after两个属性维护了当前键值对的一前一后两个对象。以上源码的继承关系如下图所示:
补充一点小知识
HashMap.TreeNode继承LinkedHashMap.LinkedHashMapEntry,而LinkedHashMap.LinkedHashMapEntry又继承HashMap.Node,那为什么 HashMap.TreeNode不直接继承HashMap.Node?绕这一圈为的是什么?其实LinkedHashMapEntry有组成双向链表的能力,继承了它就获得了这个能力,但是TreeNode 就多了两个用不到的引用 (LinkedHashMapEntry<K,V> before, after;);同时内存占用也会比较大,这么做是为啥?官方解释是说TreeNode是Node内存占用的2倍左右,但是TreeNode是在HashMap的桶中的节点足够多的时候才会使用,(JDK1.8 当一个桶中的元素超过8才会转换为红黑树(TreeNode),<6又转回链表(Node)) 如果hash算法不是很差的话TreeNode使用的几率是比较低的,因此浪费那一点空间换取了组成双向链表的能力是值得的。如果TreeNode直接 extends Node 则需要Node extentds LinkedHashMapEntry,Node使用是很频繁的,这样空间浪费的就太多了。
LinkedHashMap对象是怎么维护每一个双向链表对象的?
既然LinkedHashMap内的每一个元素都包含其前后两个元素对象,那必然在put对象的过程中就进行了维护,因此我们先看下LinkedHashMap.put方法。因为LinkedHashMap继承于HashMap,并且LinkedHashMap没有重写put(),因此调用的就是父类HashM
ap的put,我们看下源码。
//HashMap的put()方法,同时也是LinkedHashMap的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//直接看关键部分的代码,看我注释的部分
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;
if ((p = tab[i = (n - 1) & hash]) == null)
//调用了newNode()方法,这里用到了一个面向对象里多态的特性,而LinkedHashMap重写了newNode()方法
//如果是LindedHashMap对象调用,触发的是LinkedHashMap重写的newNode()
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))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//有重复的key,则用待插入值进行覆盖,返回旧值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//HashMap中该方法没做任何操作,LinkedHashMap进行了重写实现,并调用。
//该方法主要将当前节点移动到双向链表尾部,后续进行详细分析
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
//每次put新元素都会调用(如果是更新已有元素则不调用,因为没有新增元素,不会导致size+1)
//但是HashMap中该方法没做任何操作, LinkedHashMap进行了重写实现.
//主要用于移除最早的元素,后续详细解析
afterNodeInsertion(evict);
return null;
}
//HashMap内的newNode
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
//LinkedHashMap重写的newNode
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§;
return p;
}
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
//首先获取当前链表的最后一个元素
LinkedHashMapEntry<K,V> last = tail;
//当前插入的元素定义为最后一个元素
tail = p;
if (last == null)
//如果之前的最后元素是null,说明之前的链表就是空的,所以当前的元素是一个元素。
head = p;
else {
//如果之前的链表不是null
//put前的最后一个元素设置为当前put元素的前一个。
p.before = last;
//当前put元素设置为put前最后一个元素的下一个
last.after = p;
}
}
源码有点长,可以直接看我注释部分,注释部分完全可以描述新增键值对时双向链表的维护细节了。其实以上的大段逻辑做的事很简单。
-
先创建新的元素,通过hash计算位置并存放。
-
每次newNode创建元素时都连带调用linkNodeLast()将新插入元素的双向链表关系维护起来(新插入的放末尾)。
-
每次put新元素均通过LinkedHashMap中重写的afterNodeInsertion()判断是否删除头节点元素。
//这个方法是在HashMap的代码里调用的,在put方法中调用的时候参数evict传的是true
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMapEntry<K,V> first;
//evict是true,(first = head)=true,而removeEldestEntry()方法默认返回的是false,因此if内的逻辑默认是不执行的。
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
//移除链表头部的元素
removeNode(hash(key), key, null, false, true);
}
}
//HashMap中的removeNode方法,主要用于删除某一个元素
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;
…省略若干代码
}
通过以上代码发现,其实每次调用afterNodeInsertion()方法,只是内部的if判断中removeEldestEntry()默认返回false ,因此移除链表头部元素的逻辑没能执行,但是可通过重写removeEldestEntry()方法手动返回true,这样可以做到每添加一个元素都移除头部的元素。如果put的key已存在则用新值覆盖并调用LinkedHashMap重写的afterNodeAccess()将新值移动到链表尾部。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
//accessOrder 是LinkedHashMap实例化的时候的入参,需手动传true该方法的逻辑才会启用。
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//p为当前尾节点,因此p.after = null;
p.after = null;
//p.before==null,说明p以前是头节点,但是现在需要把p放在尾节点,则以前p.after就变成了头节点。
if (b == null)
head = a;
else
//原来的顺序是b <-> p <-> a…现在p需要移动到尾部,则就变成了b <-> a… <->p
b.after = a;
if (a != null)
//原来a.before是p,现在p移动走了,那p.before就变成了a.before
a.before = b;
else
//如果之前p就是尾节点,则将last引用p.before
last = b;
if (last == null)
//如果原来尾节点是空,则说明当前链表只有一个节点,因此head引用p
head = p;
else {
//之前尾节点不为空,因为p移动到了最后,因此p.before引用原尾节点,原尾节点.after引用p
p.before = last;
last.after = p;
}
tail = p;
//计数器自增1
++modCount;
}
}
以上复杂的操作均是处理链表移动的各种异常情况,最终目的是将put的元素移动到链表末端。注意:在afterNodeAccess()函数中,会修改modCount变量,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。**LinkedHashMap为什么调用get()就会触发排序?**以上例 4已经展示了调用get()排序的场景,如不记得例4的逻辑,可上翻查看。老规矩吧,点看源码一顿瞅…
//LinkedHashMap重写的get方法
public V get(Object key) {
Node<K,V> e;
//getNode()调用是HashMap.getNode()
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
//哈哈哈,看到这我一下子就明白他怎么做的排序了。
afterNodeAccess(e);
return e.value;
}
LinkedHashMap每次调用get方法,在结果返回前就触发了afterNodeAccess(),而afterNodeAccess()作用就是将当前元素移动到尾节点。**LinkedHashMap调用remove()后链表怎么维护?**首先我们猜下,如果是我们自己实现这个逻辑需要怎么操作?比如现在调用remove(),删了了链表中间的一个元素,应该怎么维护?
-
首先我们通过当前元素前后的元素获取前后节点,然后让前后节点做关联,但是需要注意首位节点。
-
如果LinkedHashMap重写了remove()则在重写的方法里实现逻辑,如果调用的是HashMap.remove()则需要有和afterNodeAccess()类似的after…()方法进行重写实现,且after…()方法需在remove()方法中调用。
我们验证下猜想是否正确。
//HashMap.remove()
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
//HashMap.removeNode(),核心的删除方法
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;
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);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
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);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)