LinkedHashMap原理分析
LinkedHashMap被称作“有序的Map”,作为对比HashMap被称作“无序的”。这里说的“有序”,“无序”到底什么意思?怎样实现的,来研究一下。
案例
在学习源码之前,先来看两个小测试案例。
@Test
public void testHashMap() {
Map<String, String> map = new HashMap<>();
map.put("caocao", "孟德");
map.put("liubei", "玄德");
map.put("guanyu", "云长");
map.put("zhaoyun", "子龙");
map.put("zhouyu", "公瑾");
map.put("guojia", "郭嘉");
map.forEach((k, v) -> {
System.out.println("key=" + k + ",value=" + v);
});
}
运行结果:
key=guanyu,value=云长
key=guojia,value=郭嘉
key=liubei,value=玄德
key=caocao,value=孟德
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
@Test
public void testLinkedHashMap() {
Map<String, String> map = new LinkedHashMap<>();
map.put("caocao", "孟德");
map.put("liubei", "玄德");
map.put("guanyu", "云长");
map.put("zhaoyun", "子龙");
map.put("zhouyu", "公瑾");
map.put("guojia", "郭嘉");
map.forEach((k, v) -> {
System.out.println("key=" + k + ",value=" + v);
});
}
运行结果:
key=caocao,value=孟德
key=liubei,value=玄德
key=guanyu,value=云长
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
key=guojia,value=郭嘉
先来解释一下有序,无序的意思,有序,无序通常指的是:遍历时能否按照插入的顺序遍历。对于HashMap来说,是无序的,插入的顺序与遍历的顺序不一致,上面的testHashMap()方法多次执行,结果都是一致的,所以无序并不是随机。LinkedHashMap插入顺序与遍历的顺序一致。
原理分析
我们先来分析HashMap无序的原理。HashMap的源码分析在这里HashMap源码分析
HashMap元素排列分析
画一图解释无序问题,下图为测试HashMap时插入元素的实际排布情况。
对照测试结果,可以看出实际的遍历顺序就是按照数组的索引顺序,以及链表的顺序遍历的。我们来看遍历的源码。
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) { //遍历数组
for (Node<K,V> e = tab[i]; e != null; e = e.next) //如果存在链表结构,遍历链表
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
逻辑很清晰,与测试现象一致。
LinkedHashMap源码分析
再来看LinkedHashMap的实现源码。
LinkedHashMap继承了HashMap,为了实现有序,将每个节点保存了前一个,后一个节点的指针,组成双向链表结构。先来看几个关键字段。
static class Entry<K,V> extends HashMap.Node<K,V> { //静态内部类,继承了HashMap.Node类,这时HashMap的节点类,增加了before, after两个指针
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
transient LinkedHashMap.Entry<K,V> head; //链表头
transient LinkedHashMap.Entry<K,V> tail; //链表尾
final boolean accessOrder; //表示按照哪种顺序遍历,false:插入顺序 true:查询顺序,默认false,可以通过构造行数指定
LinkedHashMap并没有重写put方法,而是直接继承HashMap中的put方法,但是在HashMap方法中重写回调方法,实现类了元素的有序,我们来看HashMap的put源码,以及回调方法,研究具体的实现,put方法调用内部的putVal方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //实例化一个新节点
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //按查询排列回调处理
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict); //按插入顺序排列回调
return null;
}
实例化新元素的方法,在LinkedHashMap中被重写,我们看一下实现逻辑。
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); //创建新节点
linkNodeLast(p); //添加到链表尾部
return p;
}
这里的关键逻辑是将新创建的元素添加到了链表的尾部,通过这里可以看出,LinkedHashMap是通过一个双向链表来保持插入元素的顺序的。这时按照上面的测试案例,LinkedHashMap内部会维护下图的链表结构。
下来看下插入元素后,回调的 afterNodeInsertion(evict); 方法,这个方法在HashMap为空实现,在LinkedHashMap中重写,我们直接看LinkedHashMap中的实现。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) { //removeEldestEntry方法返回false,if永远不会成立
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
来看LinkedHashMap的遍历方法。
public void forEach(BiConsumer<? super K, ? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) //遍历的是链表,所以是按照顺序的
action.accept(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
看到这里应该明白了,LinkedHashMap的有序实现原理,但是还没结束,LinkedHashMap还有另外一种遍历顺序,当accessOrder为true时。我们改造下上面的测试案例代码。
@Test
public void testLinkedHashMap() {
Map<String, String> map1 = new LinkedHashMap<>(16,0.75f,true);
map.put("caocao", "孟德");
map.put("liubei", "玄德");
map.put("guanyu", "云长");
map.put("zhaoyun", "子龙");
map.put("zhouyu", "公瑾");
map.put("guojia", "郭嘉");
map1.get("guanyu");
map.forEach((k, v) -> {
System.out.println("key=" + k + ",value=" + v);
});
}
运行结果:
key=caocao,value=孟德
key=liubei,value=玄德
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
key=guojia,value=郭嘉
key=guanyu,value=云长
我们看到,顺序与插入顺序不一致了。这个测试方法,处理在构造器中将accessOrder设置为true,在第12行获取了一个元素。我们来看get方法的实现逻辑。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
如果accessOrder为true会执行afterNodeAccess(e);
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) { //accessOrder为true才会执行下面的逻辑
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
这段代码实现的就是将查询到的元素移动到双向链表的尾部,这就是按照获取的获取的顺序遍历。在删除,修改中也存在类似的逻辑。
总结
LinkedHashMap是有序的,通过维护一个双向链表结构维护了元素的顺序,支持两种遍历顺序:按插入顺序遍历和按照查询顺序遍历。