一、背景
LinkedHashMap是基层HashMap的一个子类,所以很多方法都是用的HashMap的方法,尚未了结HashMap的同学可以去看下我的上一篇博客,在本篇博客中涉及到HashMap的源码方法就不不粘贴了,同样本篇会讲解下java7.和java8的代码
java7的版本号为1.7.0_04,java8的版本号为1.8.0_162
二、问题
LinkedHashMap的数据结构
三、构造函数
1.java7
//循环链表的头结点
private transient Entry<K,V> header;
// 双向链表中元素排序规则的标志位
// accessOrder为false,表示按插入顺序排序
// accessOrder为true,表示按访问顺序排序
private final boolean accessOrder;
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
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;
}
//继承的entry对象,添加了一个before,after
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
}
2、java8
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);
}
}
// 双向链表的头结点
transient LinkedHashMap.Entry<K,V> head;
// 双向链表的尾结点
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder;
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
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;
}
可以看到无论是java7还是java8,构造函数和HashMap
相比,就是增加了一个accessOrder
参数。用于控制迭代时的节点顺序。然后就是调用hashmap的构造函数
四、put方法
1.java7
在java8中LinkedHashMap
并没有重写任何put方法。但是重写了addEntry方法,该方法在putForNullKey()和put方法中调用,在map的某个下标链表增加一个entry'节点
void addEntry(int hash, K key, V value, int bucketIndex) {
createEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed, else grow capacity if appropriate
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
通过上面的代码可以看到,新增一个enrty时,将其添加到双向链表的尾部,
实现方式是在addBefor中,始终获取header节点,header节点的before节点(也就是原先的尾节点)的after指向当前节点,header节点的before指向当前节点
同时还在存值key则覆盖旧值时,重写了recordAccess方法,
A)recordAccess方法
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
//在双向链表移除当前元素的前后指针指向
remove();
//将当前元素添加到双向链表的尾部
addBefore(lm.header);
}
}
private void remove() {
//将当前节点的后一个元素指向前一个元素的后一个元素
before.after = after;
//将当前节点的前一个元素指向给后一个节点的前一个节点
after.before = before;
}
2.java8
在java8中LinkedHashMap
并没有重写任何put方法。但是其重写了构建新节点的newNode()
方法
newNode()
会在HashMap
的putVal()
方法里被调用,putVal()
方法会在批量插入数据putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
或者插入单个数据public V put(K key, V value)
时被调用。
LinkedHashMap
重写了newNode()
,在每次构建新节点时,通过linkNodeLast(p);
将新节点链接在内部双向链表的尾部。
//在构建新节点时,构建的是`LinkedHashMap.Entry` 不再是`Node`.
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
//将新增的节点,连接在链表的尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
//集合之前是空的
if (last == null)
head = p;
else {//将新节点连接在链表的尾部
p.before = last;
last.after = p;
}
}
在hashmap的put方法中,如果要添加的key存在,则覆盖旧值,然后调用afterNodeAccess 函数,
不存在旧值,则判断是否需要扩容,然后调用afterNodeInsertion函数。
A)afterNodeAccess函数
afterNodeAccess函数会将当前被访问到的节点e,移动至内部的双向链表的尾部。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;//原尾节点
//如果accessOrder 是true ,且原尾节点不等于e
if (accessOrder && (last = tail) != e) {
//节点e强转成双向链表节点p
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//p现在是尾节点, 后置节点一定是null
p.after = null;
//如果p的前置节点是null,则p以前是头结点,所以更新现在的头结点是p的后置节点a
if (b == null)
head = a;
else//否则更新p的前直接点b的后置节点为 a
b.after = a;
//如果p的后置节点不是null,则更新后置节点a的前置节点为b
if (a != null)
a.before = b;
else//如果原本p的后置节点是null,则p就是尾节点。 此时 更新last的引用为 p的前置节点b
last = b;
if (last == null) //原本尾节点是null 则,链表中就一个节点
head = p;
else {//否则 更新 当前节点p的前置节点为 原尾节点last, last的后置节点是p
p.before = last;
last.after = p;
}
//尾节点的引用赋值成p
tail = p;
//修改modCount。
++modCount;
}
}
值得注意的是,afterNodeAccess()
函数中,会修改modCount
,因此当你正在accessOrder=true
的模式下,迭代LinkedHashMap
时,如果同时查询访问数据,也会导致fail-fast
,因为迭代的顺序已经改变。
B)afterNodeInsertion函数
//回调函数,新节点插入之后回调 , 根据evict 和 判断是否需要删除最老插入的节点。如果实现LruCache会用到这个方法。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//LinkedHashMap 默认返回false 则不删除节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回true
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
看到这里我们已经回答前面提到的问题了
LinkedHashMap的数据结构是怎样的
LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构。该结构由数组和链表或红黑树组成(java7除去红黑树)。而LinkedHashMap 在上面结构的基础上维持了一个双向链表。插入节点时,将节点追加到双向链表尾部,从而实现按照插入顺序的有序访问。也可以在初始化LinkedHashMap对象时设定为按照访问顺序排序,此时每当访问一个节点,afternodeaccess方法就会将该节点放到双向链表的尾部,从而实现按照访问顺序的有序遍历访问。其结构可能如下图:
五、remove方法
1.java7
在java7中并没有重写remove方法,但是重写了recordRemoval,该方法在remove方法中调用,用途就是将节点删除时,同步将该节点从双向链表上删除。
void recordRemoval(HashMap<K,V> m) {
remove();
}
private void remove() {
//将当前节点的后一个元素指向前一个元素的后一个元素
before.after = after;
//将当前节点的前一个元素指向给后一个节点的前一个节点
after.before = before;
}
2.java8
在java8中LinkedHashMap
也没有重写remove()
方法,因为它的删除逻辑和HashMap
并无区别。但它重写了afterNodeRemoval()
这个回调方法。该方法就是将节点删除时,同步将该节点从双向链表上删除。
//在删除节点e时,同步将e从双向链表上删除
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//待删除节点 p 的前置后置节点都置空
p.before = p.after = null;
//如果前置节点是null,则现在的头结点应该是后置节点a
if (b == null)
head = a;
else//否则将前置节点b的后置节点指向a
b.after = a;
//同理如果后置节点时null ,则尾节点应是b
if (a == null)
tail = b;
else//否则更新后置节点a的前置节点为b
a.before = b;
}
六、get方法
1、java7
java7重写了get方法,getEntry为hashmap中的方法,
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
获取到元素之后,调用recordAccess方法将当前被访问到的节点e,移动至内部的双向链表的尾部。前面已经讲解
2、java8
LinkedHashMap
重写了get()和getOrDefault()
方法
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;
}
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;
}
对比HashMap
中的实现,LinkedHashMap
只是增加了在成员变量(构造函数时赋值)accessOrder
为true的情况下,要去回调void afterNodeAccess(Node<K,V> e)
函数。该函数会将当前被访问到的节点e,移动至内部的双向链表的尾部。前面已经讲解
七、containsValue方法
1.java7
public boolean containsValue(Object value) {
// Overridden to take advantage of faster iterator
if (value==null) {
for (Entry e = header.after; e != header; e = e.after)
if (e.value==null)
return true;
} else {
for (Entry e = header.after; e != header; e = e.after)
if (value.equals(e.value))
return true;
}
return false;
}
2.java8
public boolean containsValue(Object value) {
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}
java7和java8重写了containsValue方法,原因是在HashMap
的实现中,HashMap查找value时用了两个for循环,即使数组中的某一下标没有对应的链表,也要去查找,而LinkedHashMap查找value时,是通过双向链表来查找的,链表中的每一个节点都是有效地,而不用再去查找整个哈希表,更为高效。
那为什么LinkedHashMap没有重写containsKey(Object)方法呢?
因为HashMap的containsKey(Object)方法已经很高效了,HashMap的containsKey(Object)方法是去key对应的数组链表中去查找,其节点个数可能远远小于双向链表的节点个数,所以LinkedHashMap采用HashMap实现的containsKey(Object)方法就可以了。