开局分析
首先外面来认识下这个类,LinkedHashMap,从继承和实现方面讲,继承自HashMap,且实现了Map接口,且内部未做线程安全处理。那么这里对HashMap不是太熟悉的可以先参考我前面写过的《java HashMap 源码分析和底层实现》,作为一个Map体系的代表性集合,其经典使用场景应该就是LRU-cache的应用了。现在还是按照老龟巨!从源码开始分析
成员及构造方法
成员变量方面
/**
* The head of the doubly linked list.
*/
private transient LinkedHashMapEntry<K,V> header;
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
*/
private final boolean accessOrder;
private static final long serialVersionUID = 3801124242820219131L;
在继承了HashMap的非私有成员变量的基础上,新增了3个变量,这里关注header和accessOrder。accessOrder的注释写得很清楚,该布尔变量决定的是遍历时候所使用的顺序,如果是true就采取访问顺序,false的话就是插入顺序来遍历;而header,又难免让我们想起当时分析HashMap里面的table.这里可以点进去看究竟LinkedHashMapEntry<K,V>是个什么类型。
private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
// These fields comprise the doubly linked list used for iteration.
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) {
super(hash, key, value, next);
}
/**
* Removes this entry from the linked list.
*/
private void remove() {
before.after = after;
after.before = before;
}
/**
* Inserts this entry before the specified existing entry in the list.
*/
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
到这里已经可以知道,相对比HashMap,LinkedHashMap内部不光是使用HashMap中的哈希表来存储Entry对象,还另外维护了一个LinkedHashMapEntry,这些LinkedHashMapEntry内部又保存了前驱跟后继的引用,可以确定这是个双向链表。而这个LinkedHashMapEntry提供了对象的增加删除方法都是去更改节点的前驱后继指向。
其实这次我们分析LinkedHashMap要把前面HashMap的代码拿出来讲,可能更直观。现在我们来看构造方法。LinkedHashMapd的构造方法如下:
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;
}
@Override
void init() {
header = new LinkedHashMapEntry<>(-1, null, null, null);
header.before = header.after = header;
}
很好,结合前面的accessOrder成员分析,我们可以得出结论,LinkedHashMap的遍历,默认是插入排序遍历。而其他的则是使用了HashMap构造方法的逻辑。但是不要忘了,HashMap的构造方法实现结构,这里有个init()方法没有被实现。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
initialCapacity = DEFAULT_INITIAL_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
threshold = initialCapacity;
init();
}
void init() {
}
但是到了LinkedHashMap类里面,不难发现这个init()方法已经被重写,这里对header进行了初始化,其hash值为-1,并暂时将header的前驱后继都指向了自己。
关注put方法
但是我们找半天没有发现LinkedHashMap类内部并不存在一个put方法的重写,但是他干了一件事,那就是在put方法内部执行到addEntry()方法的时候,对这个方法进行了重写。
/**
* HashMap的put方法
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
for (HashMapEntry<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;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
----------------------------------------------------------------
LinkedHashMap所重写的方法
void addEntry(int hash, K key, V value, int bucketIndex) {
LinkedHashMapEntry<K,V> eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
removeEldest = removeEldestEntry(eldest);
} finally {
size--;
}
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
不难发现,LinkedHashMap在table指定位置插入Entry的同时,还会把自己所维护的双向链表的元素进行操作。但是现在我们模拟一遍,那就是说,当我创建好一个LinkedHashMap对象的时候,此时header的前驱后继指向的是自己。当我执行put方法添加元素进来的时候,那么此时肯定不会让上面最外层的判断成立并执行if流程内容,那么这个时候就还是走了原来的逻辑,将元素添加到哈希数组table的指定下标。就这么完事了吗?不,LinkedHashMap还重写了createEntry(),那在父类的addEntry()方法内还会走到重写的这个方法里的逻辑。
这里步骤就相当于:
- 把hash值,key-value还有对应在哈希table的下标作为参数,先获取完table中该下标位置的Entry对象
- 创建一个LinkedHashMapEntry对象,该对象的后继指向原Entry对象,这里的后继并非单项链表的后继,而是LinkedHashMap中所维护的双向链表header的前驱后继关系
- 然后在哈希table中指定位置保存新的LinkedHashMapEntry对象。
- 最后再修改新添加的LinkedHashMapEntry在双向链表中的位置,即链头。下面分析其操作过程
前面第一次插入的时候第4步骤,已经有经过这样的一套操作:
- 将header作为新插入元素的后继
- 将header的前驱(第一次插入的时候,header的前驱还是指向自己)作为新元素的前驱
- 新元素的前驱(现已变成header)的后继指向新元素
- 最后让新元素的后继(现已变成header)指向新元素
如此一来,那么LinkedHashMap的LinkedHashMapEntry就建立了一套前驱后继关系,即元素1-header,header-1,形成一个双向链表;反过来,当我第二次再put进哈希table时候,此时判断header的后继,即链头Entry如果不是header自己的话,说明已经有一个双向链表,且链头Entry是作为了最年长的元素。
这里有个很有意思的地方,如果以该链头年长Entry为参,去执行removeEldestEntry()方法,如果返回的是true,那么会执行一个removeEntryForKey的方法!!!
天呐,这里居然有一个这样的流程,等等,我们继续挖掘一下。
在LinkedHashMap内部没有对removeEntryForKey的重写,直接去HashMap类里面看都做了什么操作
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
HashMapEntry<K,V> prev = table[i];
HashMapEntry<K,V> e = prev;
while (e != null) {
HashMapEntry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
哇塞!这里居然会把这个key给删除掉!!!我的天,还有删除的操作,那是不是我用LinkedHashMap的话,保存东西,会在某种条件成里的情况下,把我链头的最年长元素删除?答案是不可能主动出现的,为什么?请看方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
我丢你个螺母?我追了一阵子,你就告诉我这个是false?那这样有什么意义你告诉我,你还不如直接不写。但是这里就是一个钩子方法,包括LRU-cache机制,也是借助了这个方法才能实现。
那这里问题来了,我们现在知道,我就算是重新写了个类继承自LinkedHashMap并重写了这个方法,设置成true,那我后面添加元素,还是要被删,而且删除的还是我链头最年长的Entry。请问 ,LinkedHashMap是怎样保证一个一个Entry是否是最年长的呢?是不是我最先put进来的就会是最年长的?毕竟前面代码写的内容,就是把最新添加进来的Entry作为header的前驱,header作为新元素的后继。这里我们需要知道LinkedHashMap控制年长的机制
get方法
前面说完put方法,因为要涉及到LinkedHashMap对双向链表的最年长的控制,这里引入get方法方便理解。来看看get方法的源码
public V get(Object key) {
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
---------------------------------------------------
/*
* HashMap#getEntry
*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
我觉得,到这里,估计大家心里就有个底了。在执行完HashMap的get方法之后,这里对get到的元素进行访问记录,在LinkedHashMapEntry的内部的recordAccess方法是这样了这样的操作:
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
这里不难理解,如果我们当前这个LinkedHashMap的accessOrder,是true,那么就是遍历按照访问顺序来的话,就会对双向链表的内容进行排序,也就是把自己在原有的双向链表上面的位置删掉,并在header前面插入。这一下我们就清楚了,为什么我们的LRU-cache可以做到最近最少删除的功能是因为,每次我get一个哈希table里面的元素,就相当于我在双向链表中插入了一个新的元素,这样一来,常被get的元素自然就被不会排到链头,尽管这个元素是最早put进LinkedHashMap的也不怕。而少用到的,自然就会被放置到链头,当我们重写removeEldestEntry方法,自然就可以控制哈希table什么时候去删除内部的元素啦
以上就是我对LinkedHashMap的一番总结,毕竟HashMap的同步问题还有LRU-cache机制的实现这块在我们开发跟面试过程中是常常会碰到的,自己如果不能把来龙去脉搞清楚,心里总是会有些没底气,经过这样一番源码分析,其实自己心里会更有自己的想法跟见解,有什么问题也欢迎大家及时沟通,谢谢