深入理解Java集合框架-LinkHashMap

上篇我们分析了HashMap, 知道了遍历HashMap时, 顺序是不能够保证的.如果遍历时需要顺序, 那么应该用LinkedHashMap, 也就是我们这次要来分析的另外一个集合类.

UML

从UML图来看, LinkedHashMap的继承于HashMap, 可见LinkedHashMap是基于HashMap来扩展的. 如果理解了HashMap的话, 那LinkedHashMap应该算是很简单了.

解析

  • LinkedHashMap是底层的数据结构是HashMap双向链表. LinkedHashMap内部维护着一条双向链表, 在遍历LinkedHashMap时, 会遍历内部的链表, 这样就可以保证遍历的顺序是特定的.至于遍历的顺序, 有两种可选: 按照插入的顺序(默认); 按照访问的顺序(结点每次被访问时, 都会把结点移动到链表尾).

  • 由于LinkedHashMap内部维护一条链表, 因此它在性能上要稍微逊色于HashMap.

结点

1
2
3
4
5
6
7
8
9
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
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内部的Entry继承于HashMap.Entry, 并且加了两个成员变量, 用来记录前一个结点和后后一个结点.

1
2
3
4
5
6
7
8
9
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;

head为头结点, tail为尾结点. 分析到这里, 我们可以确认LinkedHashMap内部确实维护一条双向链表.

hock函数

还记得上次分析HashMap时,说过HashMap留给LinkedHashMap的三个hock函数吗? 不记得的话也没关系, 耐心看下面的分析就会想起来了.

1
2
3
4
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { } //结点被访问后, LinkedHashMap回调的函数
void afterNodeInsertion(boolean evict) { } //结点插入时, LinkedHashMap回调的函数
void afterNodeRemoval(Node<K,V> p) { } //结点被移除时, LinkedHashMap回调的函数

上面三个hock函数定义在HashMap内部, 并且为空的函数.这三个函数是预留给LinkedHashMap用的. 当结点被访问, 插入和被移除时,LinkedHashMap回调的函数.

put(K, V)和afterNodeInsertion(boolean evict)

LinkedHashMap内部没有重写put(K, V)函数, 意味着它的话复用了HashMapput(K, V)方法, 那…我们暂且回调HashMap吧.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
}

put(K, V)内部会调用afterNodeInsertion(evict). 回到LinkedHashMap看看这个函数的实现.

1
2
3
4
5
6
7
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}

LinkedHashMap这个函数的实现是为了移除比较的元素(越的元素会在链表的越前面).但是这个函数的默认实现总是不会移除的元素. 因为

1
2
3
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}

这个方法一直返回false. 重写这个方法, 我们可以控制当LinkedHashMap内部的元素数量达到一定数量时, 移除比较老的元素.

前面说LinkedHashMap是可以根据插入的顺序进行遍历, 那么内部肯定在put(K, V)的时候, 会构造链表.那么构造的操作在哪里?

1
2
3
4
5
6
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重写了newNode(int, K, V, Node), 在其中写了构造链表的逻辑. 具体实现linkNodeLast

1
2
3
4
5
6
7
8
9
10
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null) //如果为尾结点为空,证明链表为空, 直接将头结点和为结点指点新插入的结点
head = p;
else { //插入新结点
p.before = last;
last.after = p;
}
}

上面的逻辑为向双向链表中插入结点.

get(K)和afterNodeAccess(Node e)

1
2
3
4
5
6
7
8
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;
}

前面我们说过, LinkedHashMap支持按照访问的顺序来遍历.而get(K)操作正是访问操作. 默认情况下, LinkedHashMap是按照插入顺序来遍历的, 因为字段accessOrder字段默认为false, 如果要支持按照访问顺序的话, 需要显示调用这个构造方法, 将accessOrder指定为true

1
2
3
4
5
public LinkedHashMap(int initialCapacity, float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

我们接着看看afterNodeAccess这个回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
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;
}
}

函数的逻辑为: 将访问的结点移动到链表的尾端.

至于remove(K)函数和afterNodeRemoval(Node<K,V> e)的关系和前面将的两个回调函数的思想一样. remove(K)内部会回调afterNodeRemoval(Node<K, V> e)函数, afterNodeRemoval(Node<K, V> e)会将结点从双向链表中移除.

迭代器

HashMap一样, 内部有提供了三个视图(KeySet, ValueSet, set), 因此, LinkedHashMap内部也有三个迭代器. 不同的是, LinkedHashMap迭代器的遍历顺序是有的. 下面我只分析LinkedValueIterator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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;
}
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;
}
}

LinkedHashIteratorLinkedHashMap内部迭代器的基类, 在构造函数将链表的头结点复制给 next, 然后nextNode()函数内, 顺序的访问之前构造好的链表.

1
2
3
4
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}

接着LinkedValueIterator继承了LinkedHashIterator, 提供了一个next()的方法.

总结

  • LinkedHashMap是基于HashMap来实现的, 它和HashMap不同之处是: 遍历元素的时候的是有序的. 因为它内部维护了一个双向链表. 遍历的顺序可以有两种: 按照结点的插入顺序; 按照元素被访问的顺序.默认是按结点被插入的顺序.

  • LinkedHashMap可以用来实现LRU缓存.

  • 由于LinkedHashMap内部维护了一个双向链表, 因此, 在性能上要稍微逊色于HashMap.

  • 如果对遍历元素的顺序是无要求的话, 应该使用HashMap, 反之应该使用LinkedHashMap.

分享到