LinkedHashMap继承HashMap,与HashMap的不同主要是它重写了父类的Entry这个类,以及addEntry(),recordAccess()等方法。
- HashMap是单向的链表,因为他的保存对象Entry中只有保存Entry next,迭代输出时按默认的插入顺序
- linkedHashMap是双向链表,因为他重写了父类Entry增加了Entry before以及Entry after,迭代输出时可以按插入顺序,也可以按访问顺序
Entry:是链表保存的实体对象,其中包含了key,value,hash,entry等。Entry对象的保存是最终以数组table[]的形式保存在该数组中,并且根据相关的hash值可以确定每 个Entry对应的数组下标index。
分析linkedHashMap要从put方法开始,linkedHsapMap的put方法直接使用的是父类HashMap的put方法,
public V put(K key, V value) {
if (table == EMPTY_TABLE) { // 如果初始数组为空,就初始化一个容量,并且实例化一个数组 table = new Entry[capacity];
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);// 根据产生的相关Hash值确定Entry的下标
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; // 此时返回旧的数据,初次put的时候 不进入,返回为null
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
继续分析,调用的是addEntry的方法,此时需要注意子类LinkedHashMap重写了该方法
这里注意下父类的构造方法中有个inti();方法,该方法并未具体实现逻辑而是交给子类去重写,linkedHashMap的init()方法初始化了一个Header,实际就是实体Entry,并且他的before和after都是自己。
@Override
void init() {
header = new LinkedHashMapEntry<>(-1, null, null, null);
header.before = header.after = header;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// Previous Android releases called removeEldestEntry() before actually
// inserting a value but after increasing the size.
// The RI is documented to call it afterwards.
// **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE ****
// Remove eldest entry if instructed
LinkedHashMapEntry<K,V> eldest = header.after;
if (eldest != header) { //如果链表header后面有数据就进入
boolean removeEldest;
size++;
try {
removeEldest = removeEldestEntry(eldest); //调用此方法判断是否要删除头部数据 也就是最老的数据
} finally {
size--;
}
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex); //调用父类的创建方法
}
接下来父类的addEntry会调用子类的creatEntry的方法:
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++;
}
代码中主要关键的还是addBefore()这个方法:
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
将初始化的header代入赋值,最终会形成双向链表的关系:
linkedHashMap的主要一个作用是它具有HashMap没有访问顺序,按访问顺序迭代,超出数量限制后移除最老的元素是Lru算法的主要原理!
那是如何实现这个访问顺序呢?我们发现在第二次put(而不是初次put操作)或者get的时候,都会调用一个方法
e.recordAccess(
this);
看源码,这个是Entry实体的方法,字面意思是重新将链表排序
/**
* 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(); // 首先执行Remove方法
addBefore(lm.header); // 再调用addbefore方法重新加到链表的尾部去,此时最新的数据将会保留在链表尾部!
}
}
remove方法的作用是将被访问的该实体的before与after两个实体连接起来
/**
* Removes this entry from the linked list.
*/
private void remove() {
before.after = after;
after.before = before;
}
经过这么处理之后,最老的数据也就停留在了链表头部,最新的数据就在链表尾部!记住这层关系!
实现lru算法是我们在每次put元素进入时,都会检查一下removeEldestEntry ,如果返回为true就会触发移除老数据的方法,如下:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
将被移除的数据保留在链表头部,也就是会移除最老的数据!