JDK1.8LinkedHashMap实现原理及源码分析
首先附上我的几篇其它文章链接感兴趣的可以看看,如果文章有异议的地方欢迎指出,共同进步,顺便点赞谢谢!!!
Android framework 源码分析之Activity启动流程(android 8.0)
Android studio编写第一个NDK工程的过程详解(附Demo下载地址)
面试必备1:HashMap(JDK1.8)原理以及源码分析
Android事件分发机制原理及源码分析
View事件的滑动冲突以及解决方案
Handler机制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源码
Android三级缓存原理及用LruCache、DiskLruCache实现一个三级缓存的ImageLoader
概述
本文对LinkedHashMap的源码分析是基于JDK1.8,因为LinkedHashMap是在HashMap的基础上进行的功能扩展,所以需要掌握HashMap的源码和实现原理,如果不了解请先阅读我的另一篇HashMap的实现原理和源码分析
重点:本文如果有分析的不对的地方请大家留言指正!!!!!
再次强调一下:读此文章之前需要先去了解HashMap的源码请先阅读我的另一篇HashMap的实现原理和源码分析,以便理解。
LinkedHashMap的数据结构
想要知道 LinkedHashMap的实现原理,就必须先去了解它的数据结构(即存储机制),和HashMap一样LinkedHashMap的数据结构也是通过数组和链表组成的散列表,同样线程也是不安全的允许null值null键
,不同的是LinkedHashMap在散列表的基础上内部维持的是一个双向链表在每次增、删、改、 查时增加或删除或调整链表的节点顺序。
- 在默认情况下LinkedHashMap遍历时的顺序是按照插入节点顺序,我们可一再构造器中通过传入
accessOrder=true
参数,使得其遍历顺序按照访问的顺序输出。 - 因继承自HashMap的一些特性LinkedHashMap都有,比如扩容的策略,哈希桶长度一定是2的N次方等等。
- 本质上LinkedHashMap是通过复写父类HashMap的几个抽象方法,去实现有序输出
LinkedHashMap的链表节点LinkedHashMapEntry<K,V>:
LinkedHashMap与HashMap都是有数组和链表组成的散列表,不同的是LinkedHashMap的LinkedHashMapEntry<K,V>
继承HashMap的Node<K,V>
,并在其基础上进行扩展成一个双向链表其源码如下:
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
}
}
此外LinkedHashMap还增加了两个成员变量分别指向链表的头节点和尾节点
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMapEntry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMapEntry<K,V> tail;
增、改put(key,value)方法源码
LinkedHashMap并没有重写HashMap的put、putVal()方法,只是重写了putVal()中调用的以下三个方法
- 构建新节点时调用的
newNode()
方法,去构建LinkedHashMapEntry节点,而不是Node节点 - 节点被访问后
afterNodeAccess(e)
抽象方法 - 节点被插入后
afterNodeInsertion(evict)
抽象方法
public V put(K key, V value) {
//计算hash值,调用putVal方法,与父类相同,再此不做分析
return putVal(hash(key), key, value, false, true);
}
在这里我将从这三个复写的方法源码进行分析,至于put()、putVal()方法的详细分析请阅读上文HashMap的实现原理和源码分析中put方法源码
1:重写了newNode()方法源码
不同的是重写了在putVal()方法中调用的newNode方法,并且在创建新节点时将该节点链接在链表尾部 linkNodeLast(p)
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
//创建的是LinkedHashMapEntry节点
LinkedHashMapEntry<K,V> p =
new LinkedHashMapEntry<K,V>(hash, key, value, e);
//并且在创建新节点时将该节点链接在链表尾部
linkNodeLast(p);
return p;
}
// link at the end of list 将新增的节点,放在在链表的尾部
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
LinkedHashMapEntry<K,V> last = tail;
tail = p;
//集合之前是空的
if (last == null)
head = p;
else {
//将新节点连接在链表的尾部
p.before = last;
last.after = p;
}
}
2:复写了afterNodeAccess(Node e )
当节点被访问后,即put的键已经存在时,调用afterNodeAccess(Node e)
方法进行排序
/**
* 当put(key,value)中key存在的时候,即访问某个存在的节点时
* 如果assessOrder=true,将该节点移动到最后
* @param e
*/
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;//记录尾部节点的临时变量
if (accessOrder && (last = tail) != e) {//如果assessOrder&&该节点不在链表尾部则将其移动到尾部
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;//p记录当前访问的节点 b和a分别记录当前节点的前、后节点
p.after = null;//将当前节点的after置null,因为链表尾部节点没有after节点
//以下是断链和重新连接链表的过程
if (b == null)
//b==null表示,p的前置节点是null,即p以前是头结点,所以更新现在的头结点是p的后置节点a
head = a;
else
//否则直接将当前节点的前后节点相连,移除当前节点
b.after = a;
if (a != null)
//a != null表示p的后置节点不是null,则更新后置节点a的前置节点为b
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;
}
//修改成员变量 为节点为当前节点
tail = p;
//修改modCount
++modCount;
}
}
3:复写了afterNodeInsertion(Node e)
/**
* 当插入新节点后,根据evict和判断是否需要删除最老插入的节点
* @param evict 为false时表示初始化时调用
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMapEntry<K,V> first;//记录链表头节点
//是否初始化&&链表不为null&&是否移除最老的节点,莫认返回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
* @param eldest 移除最老的节点
* @return
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
注意: 这两个方法void afterNodeInsertion(boolean evict)
和boolean removeEldestEntry(Map.Entry<K,V> eldest)
是构建LruCache需要的回调,在LinkedHashMap里可以忽略它们。
查:get(key)和getOrDefault( key, defaultValue)方法源码
和父类HashMap相比复写了get(key)和getOrDefault( key, defaultValue)
两个方法,其查找过程和父类的相同,只是在查找完成后根据构造器中的accessOrder=true
时调用了afterNodeAccess(e)
方法会将当前被访问到的节点e,移动至内部的双向链表的尾部
public V get(Object key) {
Node<K,V> e;
//获取Node节点过程和父类HashMap的相同,在这里不做分析
if ((e = getNode(hash(key), key)) == null)
return null;
//accessOrder,将当前访问节点移动到双向链表的尾部
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
/**
* {@inheritDoc}
*/
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
//获取Node节点过程和父类HashMap的相同,在这里不做分析
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
// //accessOrder,将当前访问节点移动到双向链表的尾部
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
删:remove()方法源码
LinkedHashMap的remove()和HashMap的逻辑相同,故而没有重写removeNode()
方法,在这里不做过多分析,详细过程请阅读HashMap的实现原理和源码分析中的remove()
方法的源码,只是重写了removeNode()
方法中删除节点后调用的afterNodeRemoval()
这个回调方法,其源码如下:
/**
* 在删除节点e时,同步将e从双向链表上删除
* @param e 被删除的节点
*/
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<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;
if (a == null)//同理如果后置节点时null ,则尾节点应是b
tail = b;
else
//否则更新后置节点a的前置节点为b
a.before = b;
}
扩展:复写了containsValue(Object value)
相比HashMap的实现,更为高效。
public boolean containsValue(Object value) {
//一次for循环遍历链表,查找Value相同的
for (LinkedHashMapEntry<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;
}
而HashMap的containsValue
方法,是由两个for循环组成,查询效率相对较低源码如下:
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {//遍历数组
for (Node<K,V> e = tab[i]; e != null; e = e.next) {//遍历链表
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
遍历:entrySet()方法源码
这里需要和上文HashMap的entrySet()
方法源码对比分析,更容易理解
LinkedHashMap的entrySet()
方法源码:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
//直接返回 LinkedEntrySet() 集合
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
遍历主要用的是LinkedEntrySet的Iterator<Map.Entry<K,V>> iterator()
方法,LinkedEntrySet是LinkedHashMap 的内部类,继承成AbstractSet<Map.Entry<K,V>>
集合,其详细源码比较简单不在这里进行详细分析
LinkedHashMap中内部类LinkedEntrySet
的iterator()方法
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
//放回LinkedEntryIterator迭代器
return new LinkedEntryIterator();
}
....省略部分源码...
}
通过上文HashMap的extrySet方法源码和以上源码分析可以知道,当我们HashMap或LinkedHashMap调用entrySet()的Itrrator()方法返回的Iterator不同,分别是new EntryIterator();
和new LinkedEntryIterator()
,通过返回的Iterator对象进行集合的遍历过程,接下来我将对其其源码入手分析集合的遍历过程
LinkedHashMap的迭代器LinkedEntryIterator
源码
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
//迭代器的next方法就是返回通过调用`nextNode()`返回下一个节点
public final Map.Entry<K,V> next() { return nextNode(); }
}
nextNode()
方法是父类LinkedHashIterator
中的方法,LinkedHashMap的本质LinkedHashIterator
是实现集合遍历,其源码分析如下
abstract class LinkedHashIterator {
LinkedHashMapEntry<K,V> next;//下一个节点
LinkedHashMapEntry<K,V> current;//当前操作的节点
int expectedModCount;
/**
* 构造器做初始化动作
* 需要注意的是:expectedModCount的作用:
* 和HashMap一样,LinkedHashMap不是线程安全的,所以在迭代的时候,会将modCount赋值到迭代器的expectedModCount属性中,
* 然后进行迭代,如果在迭代的过程中HashMap被其他线程修改了,modCount的数值就会发生变化,
* 这个时候expectedModCount和ModCount不相等,迭代器就会抛出 ConcurrentModificationException()异常
*/
LinkedHashIterator() {
//初始化的时候next为双向链表的表头
next = head;
//遍历前,先记录modCount值
expectedModCount = modCount;
//当前节点初始化为null
current = null;
}
public final boolean hasNext() {
//判断是否有下一个节点,即判断next是否为null
return next != null;
}
/**
* nextNode()的方法,就是Iterator中next方法中调用的,
* 其遍历LinkedHashMap过程就是,就是从内部维护的双向链表的表头开始循环输出。
*/
final LinkedHashMapEntry<K,V> nextNode() {
//e用于记录返回的节点
LinkedHashMapEntry<K,V> e = next;
//线程安全判断处理
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//current =next
current = e;
//next指向next的下一个节点,遍历链表
next = e.after;
return e;
}
/**
* Iterator删除方法 本质上还是调用了HashMap的removeNode方法
* 只是在调用之前,通过modCount != expectedModCount时抛出并发修改异常,处理线程不安全问题,
* 如果相等则调用HashMap的removeNode方法移除节点
*
*/
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
//调用HashMap的,removeNode方法
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
HashMap的遍历本质上是通过Iterator的next()
方法实现,而next()
就是调用nextNode()
方法,从nextNode
的源码可以看出:迭代LinkedHashMap
,就是从内部维护的双链表的表头开始循环输出。而双链表节点的顺序在LinkedHashMap的增、删、改、查时都会更新。以满足按照插入顺序输出,还是访问顺序输出。
总结
本文是在上文HashMap的实现原理和源码分析基础上做的分析
- LinkedHashMap的数据结构就是在HashMap的散列表的基础上维持了一个双向链表,在每次增、删、改、 查时增加或删除或调整链表的节点顺序。
- LinkedHashMap就是复写了HashMap提供的几个抽象方法,在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序以改变它迭代遍历时的顺序。