LinkedHashMap源码分析
LinkedLinkedHashMap继承自HashMap,与HashMap不同的是,LinkedLinkedHashMap实现了HashMap所不具备的排序功能。所使用的方式是将HashMap的节点连接成双向链表结构,从而实现排序功能。
LinkedHashMap具有两种排序方式:按照插入顺序排序和按照最近使用顺序排序。而最近使用顺序正好是LRU算法实现的关键,因此可以用LinkedHashMap轻松实现Lru缓存算法。
下面为LinkedHashMap的双链表模型:
LinkedHashMap的定义
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
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);
}
}
....
}
由上面部分可以看出LinkedHashMap正是继承HashMap,并且拓展了HashMap的节点Node。增加了两个属性,分别是before和after。从名字就可以看出,这两个属性正是实现双向链表的关键。
LinkedHashMap并未改变HashMap的排序规则,只是将其数据通过before和after串联成了一个双向链表而已。
但是在android中,为了能够适应早期的Android版本,将HashMap.Node命名成了LinkedHashMapEntry。其余的并没有什么改变。
//截取的部分注释
/**
// BEGIN Android-changed
* LinkedHashMapEntry should not be renamed. Specifically, for
* source compatibility with earlier versions of Android, this
* nested class must not be named "Entry". Otherwise, it would
* hide Map.Entry which would break compilation of code like:
*
* LinkedHashMap.Entry<K, V> entry = map.entrySet().iterator.next()
*
* To compile, that code snippet's "LinkedHashMap.Entry" must
* mean java.util.Map.Entry which is the compile time type of
* entrySet()'s elements.
// END Android-changed
*/
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
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);
}
}
因此在LinkedHashMap中,实际中的存储方式如下图:
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;
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
final boolean accessOrder;
LinkedHashMap定义了两个引用head和tail,分别对应链表的头部和尾部。
head又称为eldest,是当LinkedHashMap按照最近访问次序排序的话,head则对应最近访问次序最少的节点。
accessOrder则对应排列方式,true:按照访问次序排序,false:按照插入次序排序
构造函数
LinkedHashMap一共有5个构造方法,内部对应HashMap的构造方法
//空参数直接调用父类的构造函数,即HashMap的构造方法。
//initialCapacity 默认16,loadFactory默认0.75
//具体含义请看HashMap的源码
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
由上述可以看到,LinkedHashMap认为排序方式为插入顺序排序,只有一种构造方法可以设置其排序方式。putMapEntries为HashMap中的实现,是将Map中的数据存储在HashMap中,详情可查看HashMap的源码。
gat方法的改变
public V get(Object key) {
Node<K,V> e;
//直接调用HashMap的getNode方法获取数据
if ((e = getNode(hash(key), key)) == null)
return null;
//在访问到数据后,将调用afterNodeAccess方法将最近访问的节点移动到链尾tail
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
//带默认值的get
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;
}
当设置了按照访问次序进行排列的时候,每次访问数据都将会调用afterNodeAccess方法,将刚访问到的数据节点移动到链表的尾部tail。
afterNodeAccess 这个方法是HashMap中的方法,默认是空实现,在LinkedHashMap中被重写
void afterNodeAccess(Node<K,V> e) { // move node to last
//last用来记录未移动之前的尾部节点
LinkedHashMap.Entry<K,V> last;
//当按照访问次序排列,并且该访问节点不是尾部节点时开始移动
if (accessOrder && (last = tail) != e) {
//这里定义了3个变量,p代表当前访问节点,b和a对应before和after
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
//前一个节点为空则代表该节点为链首head节点,因此将head节点指向它的后一个节点
//不为空则代表是中间节点,直接让它的前一个节点指向它的后一个节点
if (b == null)
head = a;
else
b.after = a;
//同上,判断他是不是最后一个节点tail
if (a != null)
a.before = b;
else
last = b;
//这种情况是第一次使用时没有节点,tail和head都指向null
if (last == null)
head = p;
else {
//将当前节点连接到最后一个节点
p.before = last;
last.after = p;
}
//并将当前节点设为尾指针tail
tail = p;
++modCount;
}
}
afterNodeAccess方法实现全是节点指针的指向的转换,并未涉及到结构的变换,这里也可以看出,LinkedHashMap是对HashMap的拓展而并非改变。仅是通过增加两个节点指针实现的排序。
在HashMap中,put一个新的数据时,若是不存在该Key,则会调用newNode方法生成一个新的Node进行插入。在LinkedHashMap中,则复写了该方法,是为了将节点转换成带before和after的节点,并且进行双向链表的维护。
newNode
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);
//维护双链表,将该节点移动到链尾tail
linkNodeLast(p);
return p;
}
linkNodeLast 将节点链接到链表尾部
//该方法将节点连接到链表的尾部tail
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
//链表为空的时候,将链首head也指向该节点
if (last == null)
head = p;
//否则就将该节点连在原先链表的尾部
else {
p.before = last;
last.after = p;
}
}
上面说到,在HashMap中,若是put一个新的数据,并且该key不存在map中时,会调用newNode方法生成一个节点,并继而调用方法afterNodeInsertion,该方法在HashMap中是空实现,由LinkedHashMap具体实现。
若是put的key已经在map中存在,则会替换value值,并调用afterNodeAccess方法。该方法在上面已经提过在HashMap中空实现,是由LinkedHashMap实现的,并且在get获取值的时候也会被调用,用于将访问节点移动至链尾tail.
afterNodeInsertion
//该方法在HashMap中添加数据时会被调用,在HashMap中为空实现
//在实际插入时参数才会为true,像clone等回调的该方法参数则为false
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//removeEldestEntry方法用于判断是否将进行移除eldest节点
//属于LinkedHashMap的方法,默认返回false,即不自动移除head(eldest)节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
//移除节点方法,属于HashMap的方法
removeNode(hash(key), key, null, false, true);
}
}
可以看出,afterNodeInsertion方法是用于在map中插入数据的时候调用的,默认为空实现,目的是为了让子类实现判断是否需要删除头结点head(eldest)。
该方法只有在一般情况向HashMap中插入数据的时候参数才会为true。而如clone和带map参数的构造方法等回调出的该方法,参数均为false。而put/putAll等方法回调而来的参数则是true。
而子类LinkedHashMap在实现该方法的后,又将该方法拓展,设置了一个方法叫做removeEldestEntry,并默认返回false表示不自动删除。其子类可以通过重写这个方法来加入自动移除eldest节点的条件。(在Android的LruCache并没有重写该方法来实现自动移除eldest节点)
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
在HashMap的removeNode方法中,移除节点后会进一步调用afterNodeRemoval方法,该方法也是空实现,具体由LinkedHashMap实现的
afterNodeRemoval
//该方法是将移除的节点的引用消除,即清除before/after的引用
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)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;
}
在Java8中,HashMap的实现是数组+链表+红黑树,即在链表超过一定长度的时候,就会将链表转换成红黑树,这里又涉及到节点的转换,因此也要维护双链表的前驱节点和后继节点。
在转换的过程中会涉及到两个方法replacementTreeNode和replacementNode,树化/链表化过程中对节点的转换。这里回调是为了维护LinkedHashMap的双链表结构。
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
LinkedHashMap.Entry<K,V> t =
new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}
这两个方法在这里被复写,最终都会调用transferLinks方法进行链表的维护
//替换二者的before/after引用
private void transferLinks(LinkedHashMap.Entry<K,V> src,
LinkedHashMap.Entry<K,V> dst) {
LinkedHashMap.Entry<K,V> b = dst.before = src.before;
LinkedHashMap.Entry<K,V> a = dst.after = src.after;
if (b == null)
head = dst;
else
b.after = dst;
if (a == null)
tail = dst;
else
a.before = dst;
}
其他方法
//判断是否存在某个value
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;
}
//重新初始化数据,在clone和readObject时调用
void reinitialize() {
super.reinitialize();
head = tail = null;
}
//清空map中的所有数据
public void clear() {
super.clear();
head = tail = null;
}
另外,LinkedHashMap还重写HashMap的entrySet方法,用于实现按顺序遍历Map集合。在HashMap中是按照数组的的顺序->链表的顺序来进行遍历的。
//可以看出,其替换了HashMap的EntrySet类。
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
LinkedEntrySet类
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
...
//主要区别在这个方法,也是替换了iterator
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
...
}
继续追踪到 LinkedEntryIterator
//这里倒是没有什么区别,都是调用LinkedHashIterator的nextNode方法
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
nextNode是在抽象类LinkedHashIterator中实现的
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
//区别在这里
//原HashMap是按照数组顺序遍历
//这里是按照构建的双链表的次序进行遍历
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
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;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
至此,LinkedHashMap的源码基本已经分析完了。
总结
- LinkedHashMap 是继承于HashMap的,内部结构实现也是HashMap的实现(数组+链表+红黑树)。
- LinkedHashMap给HashMap的节点包装了两个属性before/after,用于构建双向链表。它与HashMap中的单链表没有任何关系,只是拓展了两个引用来构建双向链表而已,在HashMap中的单链表的顺序是新插入的数据在链表尾部。
- LinkedHashMap有两种排序方式,默认按照插入顺序排序,另一种是按照最近访问顺序排序
- 内部有两个指针,head(eldest)和tail。Java8中,每次插入新数据都是插入到tail尾部,更新访问的时候也是将节点调整到尾部。
- 在HashMap的增加(put等),删除(remove等),修改(put已经存在的key,链表转换成红黑树等),查询(get等)等方法中都设置或调用回调接口,用于子类实现。LinkedHashMap利用这些方法将内部节点维护成了双链表。
- LinkedHashMap有一个方法removeEldestEntry,默认返回false,可用于设置自动删除head(eldest)节点,返回true则表示需要删除head节点。若是使用LinkedHashMap构建LruCache的话可以通过该方法设置相应的条件来实现。
- Android中给LinkedHashMap的节点改变了名字并且加入了一个方法,叫做public Map.Entry<K, V> eldest() {return head; }。返回的是链表头指针,是为了LruCache的使用方便。