集合番@LinkedHashMap一文通(1.7版)

JAVA COLLECTIONS 源码

1.什么是LinkedHashMap

  • 底层使用哈希表与双向链表来实现迭代有序,非同步的HashMap
  • 新增双向链表维护所有元素的有序性(因此非遍历操作性能可能低于HashMap)
  • 包含两种排序方式:访问顺序和插入顺序(默认)
  • 访问顺序有利于实现LRU(最近最少访问)
  • 遍历速度只与元素总量(size)有关,与容量(capacity)无关
  • 此版本为JDK1.7,JDK1.8版本正在加班赶工中

2.LinkedHashMap的数据结构

  • 类定义
 
 
  1. /**
  2. * 充分利用 `多态`,和HashMap操作数据的方法完全一样
  3. */
  4. public class LinkedHashMap<K,V>
  5. extends HashMap<K,V>
  6. implements Map<K,V>
  • 重要全局变量
 
 
  1. /**
  2. * The head of the doubly linked list.
  3. * 新增一个双向链表维护所有元素,用于排序(header为链表表头);Entry为LinkedHashMap的私有静态内部类实现
  4. */
  5. private transient Entry<K,V> header;
  6. /**
  7. * The iteration ordering method for this linked hash map: <tt>true</tt>
  8. * for access-order, <tt>false</tt> for insertion-order.
  9. * 该值为true:访问顺序(最近访问元素移至链表尾部)
  10. * 该值为false:插入顺序 (先入在前,依次往后)
  11. */
  12. private final boolean accessOrder;
  • 构造器
 
 
  1. /**
  2. * 1.直接复用HashMap构造器
  3. * 2.排序方式默认插入顺序(先入在前,依次往后)
  4. */
  5. public LinkedHashMap(int initialCapacity, float loadFactor) {
  6. super(initialCapacity, loadFactor);
  7. accessOrder = false;
  8. }
  9. /**
  10. * @Param accessOrder false:插入顺序 ; true:访问顺序(最近访问元素移至链表尾部)
  11. */
  12. public LinkedHashMap(int initialCapacity, float loadFactor,boolean accessOrder) {
  13. super(initialCapacity, loadFactor);
  14. this.accessOrder = accessOrder;
  15. }
  • init方法解析
 
 
  1. /**
  2. * Called by superclass constructors and pseudoconstructors (clone,readObject)
  3. * before any entries are inserted into the map. Initializes the chain.
  4. * 该方法采用模板方法模式,在 HashMap 的实现中并无意义,只是提供给子类实现相关的初始化调用
  5. * LinkedHashMap会对header初始化
  6. */
  7. @Override
  8. void init() {
  9. header = new Entry<>(-1, null, null, null);
  10. header.before = header.after = header;
  11. }
  • 假设header地址为0x00000000,那么init之后的header如图所示:
    《集合番@LinkedHashMap一文通(1.7版)》
  • LinkedHashMap的Entry
 
 
  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. //before指向上一个entry,after指向下一个entry,从而形成双向链表,进而实现有序性
  7. //迭代时直接沿双向链表遍历,因此迭代速度只与元素总量(size)有关,与容量(capacity)无关
  8. Entry<K,V> before, after;
  9. Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
  10. super(hash, key, value, next);
  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. * Inserts this entry before the specified existing entry in the list.
  21. * 新entry插入到双向链表头引用header的前面,这样新entry就成为双向链表中的最后一个元素
  22. */
  23. private void addBefore(Entry<K,V> existingEntry) {
  24. //需要注意的是existingEntry=header
  25. after = existingEntry;
  26. before = existingEntry.before;
  27. before.after = this;
  28. after.before = this;
  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. }

3.迭代排序实例

  • 在研究之前下先直观感受一下两种排序的区别
  • 插入排序
 
 
  1. LinkedHashMap<Integer,String> linkedHashMap = new LinkedHashMap<Integer,String>();
  2. linkedHashMap.put(1,"新恒结衣");
  3. linkedHashMap.put(2,"长泽雅美");
  4. linkedHashMap.put(3,"佐佐木希");
  5. linkedHashMap.put(4,"石原里美");
  6. linkedHashMap.put(5,"堀北真希");
  7. Iterator iterator = linkedHashMap.entrySet().iterator();
  8. while (iterator.hasNext()) {
  9. Map.Entry entry = (Map.Entry) iterator.next();
  10. System.out.println(entry.getKey() + "=" + entry.getValue());
  11. }
  • 如图所示,按照插入顺序(先入在前,依次往后)
    《集合番@LinkedHashMap一文通(1.7版)》
  • 注意:header永远为null,但before和after已发生变更(双向链表生效)
    《集合番@LinkedHashMap一文通(1.7版)》
  • header的before指向链尾元素(结合after从而形成双向环路)
    《集合番@LinkedHashMap一文通(1.7版)》
  • header的after指向链首元素(除去header)
    《集合番@LinkedHashMap一文通(1.7版)》
  • 访问排序
 
 
  1. //注意:访问排序生效需要 accessOrder = true
  2. LinkedHashMap<Integer,String> linkedHashMap = new LinkedHashMap<Integer,String>(16,0.75f,true);
  3. linkedHashMap.put(1,"新恒结衣");
  4. linkedHashMap.put(2,"长泽雅美");
  5. linkedHashMap.put(3,"佐佐木希");
  6. linkedHashMap.put(4,"石原里美");
  7. linkedHashMap.put(5,"堀北真希");
  8. //访问开始
  9. linkedHashMap.get(3);
  10. linkedHashMap.get(1);
  11. Iterator iterator = linkedHashMap.entrySet().iterator();
  12. while (iterator.hasNext()) {
  13. Map.Entry entry = (Map.Entry) iterator.next();
  14. System.out.println(entry.getKey() + "=" + entry.getValue());
  15. }
  • 如图所示,按照访问顺序(最近访问元素移至链表尾部)
    《集合番@LinkedHashMap一文通(1.7版)》
  • header的before指向链尾元素
    《集合番@LinkedHashMap一文通(1.7版)》
  • header的after指向链首元素
    《集合番@LinkedHashMap一文通(1.7版)》

4.LinkedHashMap的存储

  • 在分析之前有一个重要且易混淆的点需要强调(有助于更好的理解LinkedList和HashMap结合的意义)

    • 从table的角度看,新的entry需要插入到对应的bucket里,当有哈希冲突时,采用头插法将新的entry插入到冲突链表的头部
    • 从header的角度看,新的entry需要插入到双向链表的尾部(3.排序实例使用该角度描述);而使用双向链表主要是服务于快速有序迭代
  • put方法解析

 
 
  1. /**
  2. * LinkedHashMap并未重写父类`HashMap`的put方法(因此不要惊讶找不到LinkedHashMap的put方法)
  3. * 但重写了父类 HashMap 的 put 方法调用的子方法,以增加双向链表的实现
  4. * 重写以下子方法 : recordAccess() 、addEntry()、createEntry()
  5. */
  6. public V put(K key, V value) {
  7. if (key == null)
  8. return putForNullKey(value);
  9. int hash = hash(key);
  10. int i = indexFor(hash, table.length);
  11. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  12. Object k;
  13. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  14. V oldValue = e.value;
  15. e.value = value;
  16. e.recordAccess(this);//LinkedHashMap的Entry重写父类recordAccess方法
  17. return oldValue;
  18. }
  19. }
  20. modCount++;
  21. addEntry(hash, key, value, i);//LinkedHashMap重写父类recordAccess方法
  22. return null;
  23. }
  • recordAccess方法解析
 
 
  1. /**
  2. * 记录访问顺序
  3. */
  4. void recordAccess(HashMap<K,V> m) {
  5. LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  6. // 如果定义了LinkedHashMap的迭代顺序为访问顺序,
  7. // 则删除以前位置上的元素,并将最新访问的元素添加到链表表头
  8. if (lm.accessOrder) {
  9. lm.modCount++;//注意:结构性变动会使得modCount计数+1,用于fail-fast机制
  10. remove();//变更链表的前后引用,保证有序性
  11. addBefore(lm.header);//将最新访问的元素添加到链表表头
  12. }
  13. }
  • addEntry方法解析
 
 
  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. // 调用create方法,将新元素以双向链表的的形式加入到映射中
  3. createEntry(hash, key, value, bucketIndex);
  4. // 删除最近最少使用元素的策略定义
  5. Entry<K,V> eldest = header.after;
  6. if (removeEldestEntry(eldest)) { //该方法默认返回false,也就意味着LinkedHashMap不会主动删除"过期"元素
  7. removeEntryForKey(eldest.key);//直接调用父类HashMap的removeEntryForKey()
  8. } else {
  9. if (size >= threshold)//超过阈值执行resize
  10. resize(2 * table.length);//直接调用父类HashMap的resize方法,容量翻倍
  11. }
  12. }
  • createEntry方法解析
 
 
  1. void createEntry(int hash, K key, V value, int bucketIndex) {
  2. HashMap.Entry<K,V> old = table[bucketIndex];
  3. Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
  4. table[bucketIndex] = e;
  5. //将元素加入到哈希、双向链表
  6. e.addBefore(header);
  7. size++;
  8. }
  • get方法解析
 
 
  1. /**
  2. * 记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除
  3. * 链表的增加、删除操作是常量级
  4. */
  5. public V get(Object key) {
  6. // 调用父类HashMap的getEntry()方法,取得要查找的元素
  7. Entry<K,V> e = (Entry<K,V>)getEntry(key);
  8. if (e == null)
  9. return null;
  10. // 记录访问顺序
  11. e.recordAccess(this);
  12. return e.value;
  13. }

5.LinkedHashMap的删除

  • removeEntryForKey方法解析
 
 
  1. /**
  2. * LinkedHashMap没有重写removeEntryForKey,而是使用模板方法模式巧妙的实现该方法
  3. * 以下为HashMap的removeEntryForKey方法实现
  4. */
  5. final Entry<K,V> removeEntryForKey(Object key) {
  6. int hash = (key == null) ? 0 : hash(key);
  7. int i = indexFor(hash, table.length);// hash&(table.length-1)
  8. Entry<K,V> prev = table[i];// 得到冲突链表
  9. Entry<K,V> e = prev;
  10. while (e != null) {// 遍历冲突链表
  11. Entry<K,V> next = e.next;
  12. Object k;
  13. if (e.hash == hash &&
  14. ((k = e.key) == key || (key != null && key.equals(k)))) {// 找到要删除的entry
  15. modCount++; size--;
  16. // 1. 将e从对应bucket的冲突链表中删除
  17. if (prev == e) table[i] = next;
  18. else prev.next = next;
  19. // 2. 将e从双向链表中删除 HashMap中的实际实现为 e.recordRemoval(this)
  20. //等用于 before.after = after; after.before = before;
  21. e.recordRemoval(this);
  22. return e;
  23. }
  24. prev = e; e = next;
  25. }
  26. return e;
  27. }
  28. /**
  29. * LinkedHashMap的删除实现
  30. */
  31. void recordRemoval(HashMap<K,V> m) {
  32. remove();
  33. }
  34. /**
  35. * entry内部执行remove操作时,通过变更链表前后指向删除以前位置上的元素,保证迭代有序性
  36. */
  37. private void remove() {
  38. before.after = after;
  39. after.before = before;
  40. }
  • remove方法解析

    • 从table的角度看,需要将该entry从对应的bucket里删除,如果对应的冲突链表不空,需要修改冲突链表的相应引用
    • 从header的角度来看,需要将该entry从双向链表中删除,同时修改链表中前面以及后面元素的相应引用
  • clear方法解析

 
 
  1. public void clear() {
  2. super.clear();
  3. //header的所有引用重新指向自己,等同于位置reset
  4. header.before = header.after = header;
  5. }

6.LRU实现

  • LRU算法:将最近一次使用时间离现在时间最远的数据删除掉
  • 由于LinkedHashMap的removeEldestEntry默认为false,直接使用LinkedHashMap无法直接实现LRU(没真正删除)
  • 但可以通过继承LinkedHashMap的方式,重写removeEldestEntry即可
  • removeEldestEntry方法解析
 
 
  1. /**
  2. * 默认返回false,不会直接删除元素
  3. */
  4. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  5. return false;
  6. }
  • 重写removeEldestEntry方法
 
 
  1. /**
  2. * 超过某个常量即可触发删除功能(真正的删除操作JDK已提供实现,只要加判断即可)
  3. * 这里只提供核心伪代码实现
  4. */
  5. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  6. return size() > 给定的缓存最大容量;
  7. }
    原文作者:HashMap源码分析
    原文地址: https://juejin.im/entry/5971d9e2518825591c0886d8
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞