原创作品,转载请注明出处。
概述
在了解了Java集合-HashMap源码实现深入解析后,再来看LinkedHashMap就简单的多了。
先来个示例程序:
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("语文", "1");
map.put("数学", "2");
map.put("英语", "3");
map.put("历史", "4");
map.put("政治", "5");
map.put("地理", "6");
map.put("生物", "7");
map.put("化学", "8");
map.put("物理", "9");
for(Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
想想输出的结果是什么?结果顺序怎样?
语文: 1
数学: 2
英语: 3
历史: 4
政治: 5
地理: 6
生物: 7
化学: 8
物理: 9
有没有发现,输出结果的顺序和元素插入的顺序一致?而我们再HashMap中插入的顺序和输出的顺序却不一致。Why?要解释这个问题,必须从其数据存储结构说起。
数据结构
代码调试观察的变量结果:
图:LinkedHashMap调试时数据存储的结构图根据上述调试结果,画出了如下的结构图: 图:数据结构示意图,蓝色箭头为Entry节点的链接next,绿色和棕色箭头为链表节点的after和before
再看看源码中的变量声明:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
/**
* The head of the doubly linked list.
*/
private transient Entry<K,V> header;
private final boolean accessOrder;
// 其他省略
}
源码中只声明了一个header的Entry节点和accessOrder属性:
(1)accessOrder:迭代顺序标识,默认为false,按插入顺序迭代,true则按访问顺序迭代。
(2)header节点:实现链表的关键。是一个Entry节点,该Entry继承自HashMap,采用内部类实现,源码如下:
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;
// 其他省略
}
因此LinkedHashMap内部的Entry节点除了继承HashMap.Entry的key、value、hash、next引用属性外,还添加了before和after引用节点以支持双向链表。这里只见到了header,没有tail,因此header其实也充当了tail的角色,正如所图中所示,所有元素和header一起形成互连的双向链表结构。到此,就拥有了链表的结构。
而LinkedHashMap还拥有HashMap一样的结构,原因在于其继承自HashMap,因此HashMap中用于存储元素的table也被继承过来,于是乎元素的存储也具备和HashMap一样的规则,所有元素存储在table中,table下的每个索引在原HashMap的结构下也是一个单向链表,同时每个元素中额外增加的before和after引用将table中的元素互连起来。正如调试代码中看到的那样。
到此我们就不难理解,为什么默认情况下,我们迭代时的顺序和插入的顺序一致了。因为迭代时,LinkedHashMap总是先从head出发,根据before引用遍历到所有元素,而在链表结构上插入元素总是插入到末尾。
put方法原理
LinkedHashMap没有直接重写HashMap的put方法,而是重写了HashMap中put方法用到的addEntry和createEntry方法,方法源码如下:只是多了一个after和before引用链的操作。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 先调用HashMap的addEntry方法存到table中
super.addEntry(hash, key, value, bucketIndex);
// 设置before,after引用,总是放在header的after引用上
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
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);// 从这行代码可以看出和header建立了关系
size++;
}
其中Entry的addBefore方法实现如下,就是把header的after指向新元素,原来的尾部元素也指向新元素:
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
既然LinkedHashMap继承自HashMap,那么扩容规则也和HashMap一样。
get方法原理
LinkedHashMap重新了get方法:
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);// getEntry用的是父类的方法
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
实现基本和HashMap一致,e.recordAccess(this)
, 这个调用是设置accessOrder为true时有效的。即按访问顺序读取,这个方法会将最近访问的元素移动到链表末尾,方便下次访问,因此如果设置了这个参数,不管插入还是读取,都会改变链表的结构,在使用时需要谨慎评估。
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
总结
1、LinkedHashMap继承于HashMap。
2、数据存储结构结合了Hash表和链表结构,元素存储用Hash表,元素链接通过继承父类Entry节点并增加before和after引用实现。
3、LinkedHashMap的迭代顺序收到accessOrder属性的影响,默认为false,按插入顺序访问,true按上次读取顺序访问:即每次访问元素时会把元素移动到链表末尾方便下次访问,结构会时刻变化。
白天在加班加点的干活,还要抽出时间成为技术知识的分享者,分享不易。
喜欢我的文章请点赞,欢迎打赏,成为我们坚持的动力。
网友评论