[关闭]
@kiraSally 2018-03-12T18:35:09.000000Z 字数 7625 阅读 3804

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

JAVA COLLECTIONS 源码 1.7版本


1.LinkedHashMap的定义

2.LinkedHashMap的数据结构

2.1 类定义

  1. /**
  2. * 充分利用 `多态`,和HashMap操作数据的方法完全一样
  3. */
  4. public class LinkedHashMap<K,V>
  5. extends HashMap<K,V>
  6. implements Map<K,V>

2.2 重要全局变量

  1. /**
  2. * The head of the doubly linked list.
  3. * 新增一个双向链表维护所有元素,用于排序(header为链表表头);Entry为LinkedHashMap的私有静态内部类实现
  4. * header相当于一个串联器,header.before为尾元素,header.after为首元素
  5. */
  6. private transient Entry<K,V> header;
  7. /**
  8. * The iteration ordering method for this linked hash map: <tt>true</tt>
  9. * for access-order, <tt>false</tt> for insertion-order.
  10. * 该值为true:访问顺序(最近访问元素移至链表尾部)
  11. * 该值为false:插入顺序 (先入在前,依次往后)
  12. */
  13. private final boolean accessOrder;

2.3 构造器

  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. }

2.4 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. }

2.5 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.LinkedHashMap的迭代排序

在研究之前下先直观感受一下两种排序的区别:

3.1 插入排序

  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. }

3.2 访问顺序

  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. }

4.LinkedHashMap的重要方法

4.1 插入混淆澄清

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

从table的角度看: 新的entry需要插入到对应的bucket里,当有哈希冲突时,采用头插法将新的entry插入到冲突链表的头部

从header的角度看: 新的entry需要插入到双向链表的尾部(3.排序实例使用该角度描述);而使用双向链表主要是服务于快速有序迭代

4.2 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. //LinkedHashMap的Entry重写父类recordAccess方法
  17. e.recordAccess(this);
  18. return oldValue;
  19. }
  20. }
  21. modCount++;
  22. //LinkedHashMap重写父类recordAccess方法
  23. addEntry(hash, key, value, i);
  24. return null;
  25. }

4.3 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. //注意:结构性变动会使得modCount计数+1,用于fail-fast机制
  10. lm.modCount++;
  11. //变更链表的前后引用,保证有序性
  12. remove();
  13. //将最新访问的元素添加到链表表头
  14. addBefore(lm.header);
  15. }
  16. }

4.4 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. //该方法默认返回false,也就意味着LinkedHashMap不会主动删除"过期"元素
  7. if (removeEldestEntry(eldest)) {
  8. //直接调用父类HashMap的removeEntryForKey()
  9. removeEntryForKey(eldest.key);
  10. } else {
  11. //超过阈值执行resize
  12. if (size >= threshold)
  13. //直接调用父类HashMap的resize方法,容量翻倍
  14. resize(2 * table.length);
  15. }
  16. }

4.5 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. }

4.6 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. }

4.7 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. }

4.8 remove方法

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

4.9 clear方法

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

5.LRU实现

5.1 LRU与LinkedHashMap

  • LRU算法:将最近一次使用时间离现在时间最远的数据删除掉
  • 由于LinkedHashMap的removeEldestEntry默认为false,直接使用LinkedHashMap无法直接实现LRU(没真正删除)
  • 但可以通过继承LinkedHashMap的方式,重写removeEldestEntry即可

5.2 removeEldestEntry方法

  1. /**
  2. * 默认返回false,不会直接删除元素
  3. */
  4. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  5. return false;
  6. }

5.2 重写removeEldestEntry方法

  1. /**
  2. * 超过某个常量即可触发删除功能(真正的删除操作JDK已提供实现,只要加判断即可)
  3. * 这里只提供核心伪代码实现
  4. */
  5. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  6. return size() > 给定的缓存最大容量;
  7. }

集合番@LinkedHashMap一文通(1.7版)黄志鹏kira 创作,采用 知识共享 署名-非商业性使用 4.0 国际 许可协议 进行许可。

本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注