介绍

LinkedHashMap继承自HashMap,同时内部用双向链表维护了元素的顺序,使得她的迭代是可以预测的。
类图如下:
image.png

数据结构

LinkedHashMap的内部结构如下:

  1. // 内部的节点,继承自HashMap的node,增加了before,after前后的指针用于实现链表。
  2. static class Entry<K,V> extends HashMap.Node<K,V> {
  3. Entry<K,V> before, after;
  4. Entry(int hash, K key, V value, Node<K,V> next) {
  5. super(hash, key, value, next);
  6. }
  7. }
  8. private static final long serialVersionUID = 3801124242820219131L;
  9. /**
  10. * The head (eldest) of the doubly linked list.
  11. */
  12. // 链表的头节点 / 最久访问的节点
  13. transient LinkedHashMap.Entry<K,V> head;
  14. /**
  15. * The tail (youngest) of the doubly linked list.
  16. */
  17. // 链表的尾节点 / 最近访问的节点
  18. transient LinkedHashMap.Entry<K,V> tail;
  19. /**
  20. * The iteration ordering method for this linked hash map: <tt>true</tt>
  21. * for access-order, <tt>false</tt> for insertion-order.
  22. *
  23. * @serial
  24. */
  25. // 迭代排序的方法,true代表按访问顺序,false代表按插入顺序
  26. final boolean accessOrder;

可以看出元素的顺序是有两种模式可供选择,一种是插入的顺序,即插入时是按什么顺序,迭代就是什么顺序,一种是元素访问的顺序,当访问一个元素时,这个元素会被放到链表的尾部,迭代的顺序就是从最久访问到最近访问的顺序。
为什么HashMap.Node有了next指针,LinkedHashMap.Entry还要增加after指针?

  1. // HashMap的Node 定义
  2. static class Node<K,V> implements Map.Entry<K,V> {
  3. final int hash;
  4. final K key;
  5. V value;
  6. Node<K,V> next;
  7. }

注意这里HashMap的Node里面是有一个next指针,维护的是一个单向的链表,这个链表是解决Hash冲突时用的,所以是和before,after有区别的,LinkedHashMap的after指针是用来控制访问顺序的。

构造方法

  1. // 默认构造,accessOrder=false,按插入时顺序访问
  2. public LinkedHashMap() {
  3. super();
  4. accessOrder = false;
  5. }
  6. // 指定初始容量,accessOrder=false
  7. public LinkedHashMap(int initialCapacity) {
  8. super(initialCapacity);
  9. accessOrder = false;
  10. }
  11. // 指定初始容量和负载因子
  12. public LinkedHashMap(int initialCapacity, float loadFactor) {
  13. super(initialCapacity, loadFactor);
  14. accessOrder = false;
  15. }
  16. // 指定初始容量,负载因子,访问顺序,只有这个能指定访问顺序
  17. public LinkedHashMap(int initialCapacity,
  18. float loadFactor,
  19. boolean accessOrder) {
  20. super(initialCapacity, loadFactor);
  21. this.accessOrder = accessOrder;
  22. }

put方法

直接调用hashmap的put函数,不同的是最后一个参数evict为true,在put完之后会触发LinkedHashMap的钩子函数afterNodeInsertion

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. // 在putVal最后触发
  5. void afterNodeInsertion(boolean evict) { // possibly remove eldest
  6. LinkedHashMap.Entry<K,V> first;
  7. // evict为true,头节点不为null,并且removeEldestEntry返回true,就会移除头节点
  8. if (evict && (first = head) != null && removeEldestEntry(first)) {
  9. K key = first.key;
  10. // 移除头节点
  11. removeNode(hash(key), key, null, false, true);
  12. }
  13. }

这里涉及到一个removeEldestEntry函数,默认是直接返回false,也就是说这个判断是永远走不进去,默认是不会删除最久的元素。

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

removeEldestEntry这个函数其实是扩展用,我们可以自定义一个子类,覆盖它的逻辑来实现一个类似LRU的算法,比如限定最多允许100个元素,超过100个就删除最老的元素。
代码如下:

  1. private static final int MAX_ENTRIES = 100;
  2. protected boolean removeEldestEntry(Map.Entry eldest) {
  3. return size() > MAX_ENTRIES;
  4. }

这样的话,put元素超过MAX_ENTRIES时就会移除头节点的元素,再加上accessOrder = true时,移除的就是最久没有被访问的元素,利用这个可以非常简单的实现LRU(最近最少访问)算法。

newNode方法

那么双向链表,before,after是在哪里维护的?
LinkedHashMap覆盖了HashMap的newNode方法来维护双向链表的关系。

  1. // 新增一个节点
  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. linkNodeLast(p);
  6. return p;
  7. }
  8. // 新增的节点链接到链表的尾部
  9. private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
  10. // 临时保存尾节点
  11. LinkedHashMap.Entry<K,V> last = tail;
  12. // 新增节点p变成尾节点
  13. tail = p;
  14. // 之前的尾节点是空,说明之前链表根本没有元素
  15. if (last == null)
  16. // p即是头节点也是尾节点
  17. head = p;
  18. else {
  19. // last不为空,将last和p进行连接,p的before是last,last的after是p
  20. p.before = last;
  21. last.after = p;
  22. }
  23. }

get方法

LinkedHashMap重写了HashMap的get方法,重写是为了添加访问顺序的逻辑,当accessOrder为true时,最后调用afterNodeAccess方法,当前访问的节点会移动到末尾。

  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) { // move node to last
  11. LinkedHashMap.Entry<K,V> last;
  12. // e本身不是尾节点才进行移动
  13. if (accessOrder && (last = tail) != e) {
  14. // e赋值为p,b是p的before节点,a是p的after节点
  15. LinkedHashMap.Entry<K,V> p =
  16. (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
  17. // p要转移到尾节点,所以p的after是null
  18. p.after = null;
  19. // 如果没有before节点,说明p是头节点
  20. if (b == null)
  21. // 头节点是p的after节点
  22. head = a;
  23. else
  24. // p有before节点,b节点的after指向a,跳过p
  25. b.after = a;
  26. // 判断after节点不是null
  27. if (a != null)
  28. // after节点的before指向b,跳过p
  29. a.before = b;
  30. else
  31. // afte是null,last指向b,b有可能是null
  32. last = b;
  33. // last为null,那么p是头节点
  34. if (last == null)
  35. head = p;
  36. else {
  37. // last不为null,把p挂到last的后面
  38. p.before = last;
  39. last.after = p;
  40. }
  41. // p变成了尾节点
  42. tail = p;
  43. ++modCount;
  44. }
  45. }

remove方法

LinkedHashmap没有重写remove方法,直接调用了HashMap的remove,移除完成后最后会调用钩子函数afterNodeRemoval。

  1. // 移除节点后要维护e前后的before和after指针
  2. void afterNodeRemoval(Node<K,V> e) { // unlink
  3. LinkedHashMap.Entry<K,V> p =
  4. (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
  5. p.before = p.after = null;
  6. // e的before为null,head指向e的after
  7. if (b == null)
  8. head = a;
  9. else
  10. // e的before不为null,before的after指向e的after
  11. b.after = a;
  12. // e的after为null,说明e是尾节点,tail指向e的before,这样e就被移除了
  13. if (a == null)
  14. tail = b;
  15. else
  16. // e的after不为null,e的after节点的before指针指向e的before节点,跳过e
  17. a.before = b;
  18. }