一、LinkedHashMap 类概述
1、LinkedHashMap 是 HashMap 的子类。
2、在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序。
3、与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。
4、LinkedHashMap 不仅实现了HashMap的所有功能,更是维护了元素的存储顺序。LinkedHashMap维护元素顺序的方式有两种,一种是维护他的存入顺序,另一种则是维护元素的读取顺序。
5、LinkedHashMap的结构是HashMap+双向链表。他通过继承HashMap得到了用hash表存储数据的能力,同时他又维护了一个双向链表实现了对元素的排序功能。
6、 HashMap 是无序的,即迭代器的顺序与插入顺序没什么关系。而 LinkedHashMap 在 HashMap 的基础上增加了顺序:分别为「插入顺序」和「访问顺序」。即遍历 LinkedHashMap 时,可以保持与插入顺序一致的顺序;或者与访问顺序一致的顺序。
二、LinkedHashMap 类结构
1、LinkedHashMap 类继承结构
2、LinkedHashMap 类签名
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>{}
3、LinkedHashMap 方法列表
三、LinkedHashMap 中Entry节点
1、JDK7中节点
1 /**
2 * LinkedHashMap entry.
3 */
4 private static class Entry<K,V> extends HashMap.Entry<K,V> {
5 // These fields comprise the doubly linked list used for iteration.
6 Entry<K,V> before, after;
7
8 Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
9 super(hash, key, value, next);
10 }
11
12 /**
13 * Removes this entry from the linked list.
14 */
15 private void remove() {
16 before.after = after;
17 after.before = before;
18 }
19
20 /**
21 * Inserts this entry before the specified existing entry in the list.
22 */
23 private void addBefore(Entry<K,V> existingEntry) {
24 after = existingEntry;
25 before = existingEntry.before;
26 before.after = this;
27 after.before = this;
28 }
29
30 /**
31 * This method is invoked by the superclass whenever the value
32 * of a pre-existing entry is read by Map.get or modified by Map.set.
33 * If the enclosing Map is access-ordered, it moves the entry
34 * to the end of the list; otherwise, it does nothing.
35 */
36 void recordAccess(HashMap<K,V> m) {
37 LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
38 if (lm.accessOrder) {
39 lm.modCount++;
40 remove();
41 addBefore(lm.header);
42 }
43 }
44
45 void recordRemoval(HashMap<K,V> m) {
46 remove();
47 }
48 }
2、JDK8 中节点
1 /**
2 * HashMap.Node subclass for normal LinkedHashMap entries.
3 */
4 static class Entry<K,V> extends HashMap.Node<K,V> {
5 Entry<K,V> before, after;
6 Entry(int hash, K key, V value, Node<K,V> next) {
7 super(hash, key, value, next);
8 }
9 }
10
11 // 指向eldest元素
12 transient LinkedHashMap.Entry<K,V> head;
13 // 指向youngest元素
14 transient LinkedHashMap.Entry<K,V> tail;
LinkedHashMap 内部有一个嵌套类 Entry,它继承自 HashMap 中的 Node 类,如上。
jdk1.8的链表结构和1.7的差异很大,可以看出来1.8中的实现简化了不是,只维护了两个指针,befor和after。在整个链表中维护了head(头指针)和tail(尾指针)。这两个指针是有讲究的,head所指向的是eldest元素,也就是最老的元素,tail指向youngest元素,也就是最年轻的元素。在这个链表中,都是在队尾添加元素,队头删除元素,这种方式很像队列,但是还是有点区别。
四、LinkedHashMap 成员变量
LinkedHashMap 提供了以下四个成员变量
1 private static final long serialVersionUID = 3801124242820219131L;
2
3 /**
4 * The head (eldest) of the doubly linked list.
5 */
6 transient LinkedHashMap.Entry<K,V> head; //指向 eldest 最老的元素
7
8 /**
9 * The tail (youngest) of the doubly linked list.
10 */
11 transient LinkedHashMap.Entry<K,V> tail; //指向 yongest 最新的元素
12
13 /**
14 * The iteration ordering method for this linked hash map: <tt>true</tt>
15 * for access-order, <tt>false</tt> for insertion-order.
16 *
17 * @serial
18 */
19 final boolean accessOrder; //此链接的哈希映射的迭代排序方法:true 用于访问顺序,false 用于插入顺序。
五、LinkedHashMap 构造器
LinkedHashMap 提供了两类的构造器:
1、无参或指定成员属性的构造器
1 /**
2 * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
3 * with the default initial capacity (16) and load factor (0.75).
4 */
5 public LinkedHashMap() {
6 super();
7 accessOrder = false;
8 }
9
10 public LinkedHashMap(int initialCapacity) {
11 super(initialCapacity);
12 accessOrder = false;
13 }
14
15 public LinkedHashMap(int initialCapacity, float loadFactor) {
16 super(initialCapacity, loadFactor);
17 accessOrder = false;
18 }
19
20 public LinkedHashMap(int initialCapacity,
21 float loadFactor,
22 boolean accessOrder) {
23 super(initialCapacity, loadFactor);
24 this.accessOrder = accessOrder;
25 }
26
27 public LinkedHashMap(Map<? extends K, ? extends V> m) {
28 super();
29 accessOrder = false;
30 putMapEntries(m, false);
31 }
这里的 super() 方法调用了 HashMap 的无参构造器。该构造器方法构造了一个容量为 16(默认初始容量)、负载因子为 0.75(默认负载因子)的空 LinkedHashMap,其顺序为插入顺序。
倒数第二个稍微不一样,它的 accessOrder 可以在初始化时指定,即指定 LinkedHashMap 的顺序(插入或访问顺序)。
LinkedHashMap的创建和HashMap没什么两样,就是这个构造方法中,加入了acessOrder的参数,告诉LinkedHashMap以哪种方式维护顺序。
其中 accessOrder 元素遍历顺序,true维护元素的访问顺序,最新访问的放入队尾,false维护元素的插入顺序,最新插入的在队尾。
2、传入一个Map的构造器
1 public LinkedHashMap(Map<? extends K, ? extends V> m) {
2 super();
3 accessOrder = false;
4 putMapEntries(m, false);
5 }
六、put 元素
LinkedHashMap 本身没有实现 put 方法,它通过调用父类(HashMap)的方法来进行读写操作。这里再贴下 HashMap 的 put 方法:
1 public V put(K key, V value) {
2 return putVal(hash(key), key, value, false, true);
3 }
4
5 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
6 boolean evict) {
7 Node<K,V>[] tab; Node<K,V> p; int n, i;
8 if ((tab = table) == null || (n = tab.length) == 0)
9 n = (tab = resize()).length;
10 if ((p = tab[i = (n - 1) & hash]) == null)
11 // 新的 bin 节点
12 tab[i] = newNode(hash, key, value, null);
13 else {
14 Node<K,V> e; K k;
15 // key 已存在
16 if (p.hash == hash &&
17 ((k = p.key) == key || (key != null && key.equals(k))))
18 e = p;
19 // 散列冲突
20 else if (p instanceof TreeNode)
21 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
22 else {
23 // 遍历链表
24 for (int binCount = 0; ; ++binCount) {
25 // 将新节点插入到链表末尾
26 if ((e = p.next) == null) {
27 p.next = newNode(hash, key, value, null);
28 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
29 treeifyBin(tab, hash);
30 break;
31 }
32 if (e.hash == hash &&
33 ((k = e.key) == key || (key != null && key.equals(k))))
34 break;
35 p = e;
36 }
37 }
38 if (e != null) { // existing mapping for key
39 V oldValue = e.value;
40 if (!onlyIfAbsent || oldValue == null)
41 e.value = value;
42 afterNodeAccess(e);
43 return oldValue;
44 }
45 }
46 ++modCount;
47 if (++size > threshold)
48 resize();
49 afterNodeInsertion(evict);
50 return null;
51 }
这个方法哪个地方跟 LinkedHashMap 有联系呢?如何能保持 LinkedHashMap 的顺序呢?且看其中的 newNode() 方法,它在 HashMap 中的代码如下:
1 Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
2 return new Node<>(hash, key, value, next);
3 }
但是,LinkedHashMap 重写了该方法:
1 // 新建一个 LinkedHashMap.Entry 节点
2 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
3 LinkedHashMap.Entry<K,V> p =
4 new LinkedHashMap.Entry<K,V>(hash, key, value, e);
5 // 将新节点连接到列表末尾
6 linkNodeLast(p);
7 return p;
8 }
1 // link at the end of list
2 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
3 LinkedHashMap.Entry<K,V> last = tail;
4 tail = p;
5 // list 为空
6 if (last == null)
7 head = p;
8 else {
9 // 将新节点插入到 list 末尾
10 p.before = last;
11 last.after = p;
12 }
13 }
可以看到,每次插入新节点时,都会存到列表的末尾。原来如此,LinkedHashMap 的插入顺序就是在这里实现的。
此外,上文分析 HashMap 时提到两个回调方法:afterNodeAccess 和 afterNodeInsertion。它们在 HashMap 中是空的:
HashMap提供了两个回调方法作为他的扩展,LinkedHashMap只需要实现这两个方法即可,从这里也可以学到如何提供代码的扩展性,预先留出回调接口也是个不错的选择哦。
1 // Callbacks to allow LinkedHashMap post-actions
2 void afterNodeAccess(Node<K,V> p) { }
3 void afterNodeInsertion(boolean evict) { }
在HashMap的put方法中,调用了两个回调方法,afterNodeAccess和afterNodeInsertion。下面介绍afterNodeInsertion,这个方法的主要目的就是在map添加元素以后,维护链表的顺序,同时也会控制了对链表头元素的删除与否。
LinkedHashMap 中重写的 afterNodeInsertion 方法:
1 // 在插入元素以后,判断当前容器的元素是否已满,如果是的话,就删除当前最老的元素,也就是队头元素。
2 void afterNodeInsertion(boolean evict) { // possibly remove eldest
3 LinkedHashMap.Entry<K,V> first;
4 if (evict && (first = head) != null && removeEldestEntry(first)) {
5 K key = first.key;
6 removeNode(hash(key), key, null, false, true);
7 }
8 }
9
10 // 这是用户实现的回调方法,判断当前最老的元素是否需要删除,如果为true,就删除链表头元素
11 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
12 return false;
13 }
七、get 获取元素
LinkedHashMap的get方法几乎就是复用了HashMap。唯一的区别就是多了一个accessOrder判断,如果accessOrder==true说明他需要维护元素的访问顺序,而afterNodeAccess是HashMap提供的回调方法,他也会在put元素的时候调用。
afterNodeAccess方法的作用就是将当前访问的元素添加到队尾,因为这个链表都是从头部删除,因此这个元素会在最后才被删除。
同样,LinkedHashMap 对它们进行了重写。下面来分析 afterNodeAccess 方法。
这里的 getNode 方法是父类的(HashMap)。若 accessOrder 为 true(即指定为访问顺序),则将访问的节点移到列表末尾。
LinkedHashMap 重写了 HashMap 的 get 方法,主要是为了维持访问顺序,代码如下:
1 public V get(Object key) {
2 Node<K,V> e;
3 if ((e = getNode(hash(key), key)) == null)
4 return null;
5 if (accessOrder) //若为访问顺序,将访问的节点移到列表末尾
6 afterNodeAccess(e);
7 return e.value;
8 }
9
10 void afterNodeAccess(Node<K,V> e) { // 将访问元素添加到队尾
11 LinkedHashMap.Entry<K,V> last;
12 // accessOrder 为 true 表示访问顺序
13 if (accessOrder && (last = tail) != e) {
14 // p 为访问的节点,b 为其前驱,a 为其后继
15 LinkedHashMap.Entry<K,V> p =
16 (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
17 p.after = null;
18 // p 是头节点
19 // 如果当前元素是头元素,那么就将head指向他的下一个节点
20 if (b == null)
21 head = a;
22 else
23 b.after = a;
24 // 如果当前元素是尾元素,那么就将last指向他的上一个节点
25 if (a != null)
26 a.before = b;
27 else
28 last = b;
29 if (last == null)
30 head = p;
31 else {
32 p.before = last;
33 last.after = p;
34 }
35 tail = p;
36 ++modCount;
37 }
38 }
为了便于分析和理解,这里画出了两个操作示意图:
这里描述了进行该操作前后的两种情况。可以看到,该方法执行后,节点 p 被移到了 list 的末尾。
八、删除元素
removeNode 方法是父类 HashMap 中的。
1 final Node<K,V> removeNode(int hash, Object key, Object value,
2 boolean matchValue, boolean movable
3 ) {
4 Node<K,V>[] tab; Node<K,V> p; int n, index;
5 // table 不为空,且给的的 hash 值所在位置不为空
6 if ((tab = table) != null && (n = tab.length) > 0 &&
7 (p = tab[index = (n - 1) & hash]) != null) {
8 Node<K,V> node = null, e; K k; V v;
9 // 给定 key 对应的节点,在数组中第一个位置
10 if (p.hash == hash &&
11 ((k = p.key) == key || (key != null && key.equals(k))))
12 node = p;
13 // 给定的 key 所在位置为红黑树或链表
14 else if ((e = p.next) != null) {
15 if (p instanceof TreeNode)
16 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
17 else {
18 do {
19 if (e.hash == hash &&
20 ((k = e.key) == key ||
21 (key != null && key.equals(k)))) {
22 node = e;
23 break;
24 }
25 p = e;
26 } while ((e = e.next) != null);
27 }
28 }
29 // 删除节点
30 if (node != null && (!matchValue || (v = node.value) == value ||
31 (value != null && value.equals(v)))) {
32 if (node instanceof TreeNode)
33 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
34 else if (node == p)
35 tab[index] = node.next;
36 else
37 p.next = node.next;
38 ++modCount;
39 --size;
40 // 删除节点后的操作
41 afterNodeRemoval(node);
42 return node;
43 }
44 }
45 return null;
46 }
afterNodeRemoval 方法在 HashMap 中的实现也是空的:
void afterNodeRemoval(Node<K,V> p) { }
在删除元素以后,LinkedHashMap需要维护当前链表的指针,也就是双向链表的head和tail指针的指向问题。
LinkedHashMap 重写了该方法:
1 void afterNodeRemoval(Node<K,V> e) {
2 LinkedHashMap.Entry<K,V> p =
3 (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
4 p.before = p.after = null;
5 // 如果当前元素是头元素,那么head指向他的下一个节点
6 if (b == null)
7 head = a;
8 else
9 b.after = a;
10 // 如果当前元素是尾元素,那么tail指向他的上一个节点
11 if (a == null)
12 tail = b;
13 else
14 a.before = b;
15 }
该方法就是双链表删除一个节点的操作。
九、遍历操作
容器的遍历是一个亘古不变的话题,然而LinkedHashMap的遍历方式有他的特殊性。因为他在hash表的基础之上又维护了一个双向链表,而这个链表维护这元素的遍历顺序,因为LinkedHashMap在遍历的时候,只能遍历这个链表,而不能像HashMap一样遍历hash表。
1 abstract class LinkedHashIterator {
2 LinkedHashMap.Entry<K,V> next;
3 LinkedHashMap.Entry<K,V> current;
4 int expectedModCount;
5 LinkedHashIterator() {
6 // 第一次从头开始遍历
7 next = head;
8 expectedModCount = modCount;
9 current = null;
10 }
11 public final boolean hasNext() {
12 return next != null;
13 }
14 // 对链表从头到尾开始遍历,顺序遍历的方式很简单就是next = e.after
15 final LinkedHashMap.Entry<K,V> nextNode() {
16 LinkedHashMap.Entry<K,V> e = next;
17 if (modCount != expectedModCount)
18 throw new ConcurrentModificationException();
19 if (e == null)
20 throw new NoSuchElementException();
21 current = e;
22 next = e.after;
23 return e;
24 }
25 public final void remove() {
26 Node<K,V> p = current;
27 if (p == null)
28 throw new IllegalStateException();
29 if (modCount != expectedModCount)
30 throw new ConcurrentModificationException();
31 current = null;
32 K key = p.key;
33 removeNode(hash(key), key, null, false, false);
34 expectedModCount = modCount;
35 }
36 }
十、代码实验
1、HashMap 是无序的
1 Map<String, String> map = new HashMap<>();
2 map.put("bush", "a");
3 map.put("obama", "b");
4 map.put("trump", "c");
5 map.put("lincoln", "d");
6 System.out.println(map);
7
8 // 输出结果(无序):
9 // {obama=b, trump=c, lincoln=d, bush=a}
2、 LinkedHashMap,则可以保持插入的顺序
1 Map<String, String> map = new LinkedHashMap<>();
2 map.put("bush", "a");
3 map.put("obama", "b");
4 map.put("trump", "c");
5 map.put("lincoln", "d");
6 System.out.println(map);
7
8 // 输出结果(插入顺序):
9 // {bush=a, obama=b, trump=c, lincoln=d}
3、指定 LinkedHashMap 的顺序为访问顺序:
1 Map<String, String> map = new LinkedHashMap<>(2, 0.75f, true);
2 map.put("bush", "a");
3 map.put("obama", "b");
4 map.put("trump", "c");
5 map.put("lincoln", "d");
6 System.out.println(map);
7
8 map.get("obama");
9 System.out.println(map);
10
11 // 输出结果(插入顺序):
12 // {bush=a, obama=b, trump=c, lincoln=d}
13
14 // 访问 obama 后,obama 移到了末尾
15 // {bush=a, trump=c, lincoln=d, obama=b}
4、实现 LRU 缓存
private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int capacity;
public LRUCache(int capacity) {
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
测试:
1 LRUCache<String, String> lruCache = new LRUCache<>(2);
2 lruCache.put("bush", "a");
3 lruCache.put("obama", "b");
4 lruCache.put("trump", "c");
5 System.out.println(lruCache);
6
7 // 输出结果:
8 // {obama=b, trump=c}
这里定义的 LRUCache 类中,对 removeEldestEntry 方法进行了重写,当缓存中的容量大于 2,时会把最早插入的元素 "bush" 删除。因此只剩下两个值。
十一、总结
1. LinkedHashMap 继承自 HashMap,其结构可以理解为「双链表 + 散列表」;
2. 可以维护两种顺序:插入顺序或访问顺序;
3. 可以方便的实现 LRU 缓存;
4. 线程不安全。