Java 集合 | LinkedHashMap源码分析(JDK 1.7)

一、基本图示

《Java 集合 | LinkedHashMap源码分析(JDK 1.7)》

二、基本介绍

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> 
结构
  • LinkedHashMap 继承了 HashMap
  • LinkedHashMap 实现了 Map 接口
特性
  • 底层使用拉链法处理冲突的散列表
  • 元素是有序的,即遍历得到的元素顺序和放进去的元素属性一样
  • 是线程不安全的
  • key 和 value 都允许为 null
  • 1 个 key 只能对应 1 个 value,1 个 value 对应多个 key

三、基本结构

// 指向双向链表的头节点 transient LinkedHashMap.Entry<K,V> head; // 判断是否进行重排序 final boolean accessOrder; 

LinkedList 的基本结构是继承 HashMap 的 Entry,因此 hashkeyvaluenext 都是继承自 HashMap 的 Entry 类的,而 beforeafter 则是 LinkedHashMap 特有的

需要注意的是,next 用于指定位置的哈希数组对应的链表,而 beforeafter 则用于循环双向链表中元素的指向

private static class Entry<K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } ... } 

四、构造函数

需要注意这里的 accessOrder 变量,该变量是用来判断是按照插入顺序进行排序(false),还是按照访问顺序进行排序(false),具体的后面会介绍。如果不特别指定该变量为 true,默认都是 false,即元素放进去什么顺序,遍历出来就是什么顺序;如果指定为 true,则情况就不同了

注意这里的构造函数,都是调用父类的构造函数

public LinkedHashMap() { // 调用 HashMap 的 构造函数 super(); accessOrder = false; } public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } // 需要注意这个构造函数,可以自己定义 accessOrder public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; } 

五、增加函数

整体还是继承的 HashMap 的 put 方法,但是里面的 addEntry 方法进行了重写

// HashMap 的 put 方法 public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; // LinkedHashMap 重写的方法 addEntry(hash, key, value, i); return null; } 

LinkedHashMap 中重写了的 addEntry 方法,里面还是调用了父类 HashMap 的 addEntry 方法。在 HashMap 的 addEntry 方法中,createEntry 方法已经被子类重写了,因此这里的 createEntry 方法调用的是子类中重写的方法

// LinkedHashMap 中重写了的方法 void addEntry(int hash, K key, V value, int bucketIndex) { super.addEntry(hash, key, value, bucketIndex); // Remove eldest entry if instructed Entry<K,V> eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } } // 父类 HashMap 的中的方法 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 这里调用的是子类 LinkedHashMap 的重写后的方法 createEntry(hash, key, value, bucketIndex); } 

LinkedHashMap 中重写后的方法,其中前面三步和 HashMap 中该方法的实现一样,唯一的区别在于 LinkedHashMap 中的 addBefore 方法

void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value, old); table[bucketIndex] = e; // 将新的键值对 e,放到 header 节点之前,即在循环双向链表最后一个元素后插入 e.addBefore(header); size++; } 

LinkedHashMap 的 Entry 继承 HashMap 的 Entry 类,因此除了之前的特性,还加上了 before 和 after 的属性,用于指向循环链表中元素

private static class Entry<K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } private void remove() { before.after = after; after.before = before; } // 将新的 Entry 键值对插入到 existingEntry 键值对之前 private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } ... } 

直接看图示就明白了
《Java 集合 | LinkedHashMap源码分析(JDK 1.7)》

接下来再看 LinkedHashMap 中的扩容方法。同样是调用父类 HashMap 中的扩容方法 resize,但是具体的实现就有些不同了

先看 HashMap 中的 resize 方法,其中的 transfer 方法在 LinkedHashMap 中重写了,因此这里调用 LinkedHashMap 的此方法

// 当键值对的个数 ≥ 阀值,并且哈希数组在所放入的位置不为 null,则对哈希表进行扩容 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; // 子类重写了该方法,因此调用子类的这个方法 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } 

HashMap 中的 transfer 方法是先遍历旧的哈希数组 table,然后再遍历 table 中对应位置的单链表,计算每个元素在新的哈希数组中的位置,然后将元素进行转移

LinkedHashMap 中的 transfer 方法,则是直接遍历循环双向链表,计算每个元素在新的哈希数组中的位置,然后将元素进行转移

void transfer(HashMap.Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; // 遍历双向链表,e 是双向链表的第一个元素 for (Entry<K,V> e = header.after; e != header; e = e.after) { if (rehash) e.hash = (e.key == null) ? 0 : hash(e.key); // 计算该元素在新哈希数组中的位置 int index = indexFor(e.hash, newCapacity); e.next = newTable[index]; newTable[index] = e; } } 

在转移前后,改变的是哈希数组中元素的位置,而双向链表中元素的位置是不变的

六、元素的获取

在 LinkedHashMap 中重写了 get 方法,但是在根据 key 找到 Entry 键值对的过程中,是通过调用 HashMap 的 getEntry 方法实现的,而在该方法中,先找到哈希数组的位置,然后在遍历对应位置的单链表。可见,LinkedHashMap 的 get 方法不是遍历双向链表,依然还是遍历哈希表来获取元素的

// LinkedHashMap 中的 get 方法 public V get(Object key) { Entry<K,V> e = (Entry<K,V>)getEntry(key); if (e == null) return null; e.recordAccess(this); return e.value; } // HashMap 中的方法,先找到指定数组下标,然后遍历对应位置的单链表 final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; } 

七、双向链表的重新排序

成员变量中,有这么一个变量 accessOrder,它有两个作用

  • false,按照元素插入的顺序排序
  • true,按照元素访问的顺序排序
private final boolean accessOrder; public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; } 

而在 LinkedHashMap 的 get 和 put 方法中,都调用了同一个方法 recordAccess

public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; // 如果需要覆盖 key 和 hash 相同的元素,则会调用该方法 // this 调用 put 方法的对象,因此 this 就是 linkedList 对象 e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; } public V get(Object key) { Entry<K,V> e = (Entry<K,V>)getEntry(key); if (e == null) return null; // 调用了 recordAccess 方法 e.recordAccess(this); return e.value; } 

该方法的作用,是把访问过的元素,这里的访问,有两层含义:

  • put,根据 key 修改对应元素的 value
  • get,根据 key 获取对应元素的 value

每访问一次,都会通过 recordAccess 方法把访问过的元素放到双向链表的最后一位。该方法在 HashMap 中是空实现,但是在 LinkedHashMap 中,是在 Entry 类中的方法,每次是通过 Entry 对象来调用该方法的

private static class Entry<K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } private void remove() { before.after = after; after.before = before; } private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } void recordAccess(HashMap<K,V> m) { // 获取传入的 HashMap 对象,向下转型为 LinkedHashMap 对象 LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; // 如果 accessOrder 为 true if (lm.accessOrder) { lm.modCount++; // 将访问后的元素删除 remove(); // 将访问后的元素移到最后一位 addBefore(lm.header); } } ... } 

我们可以通过例子和图示来看一下整个过程

@Test public void test3() { LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>(16,0.75f, true); linkedHashMap.put("语文", 1); linkedHashMap.put("英语", 2); linkedHashMap.put("数学", 3); print(linkedHashMap); linkedHashMap.get("语文"); print(linkedHashMap); linkedHashMap.put("英语", 4); print(linkedHashMap); } public static void print(LinkedHashMap<String, Integer> linkedHashMap) { for (Map.Entry entry: linkedHashMap.entrySet()) { System.out.print(entry + " "); } System.out.println(); } 

结果是

语文=1 英语=2 数学=3 英语=2 数学=3 语文=1 数学=3 语文=1 英语=4 

可以看到,每访问一个元素,该元素就被放到了最后一位,通过图示在来看一下把

通过代码,可以看到,recordAccess 方法先调用查询到键值对的 remove 方法将查询到的元素删除
《Java 集合 | LinkedHashMap源码分析(JDK 1.7)》

然后在调用该键值对的 addBefore 方法将删除的元素置于链表的最后一位
《Java 集合 | LinkedHashMap源码分析(JDK 1.7)》
这里我就画了 get 方法调用 recordAccess 的例子,put方法也是一样的。其实就做了两个步骤,将查询到的元素删除,然后将其置于最后一位

需要注意的是,无论是 get 还是 put,都是使用对象 e 来指向查询到的元素的,对象 e 也是 Entry 的对象,因此在调用 recordAccess 方法的时候,其实是用实例过后的 e 来调用的。之后 recordAccess 方法里的 remove 和 addBefore 方法,调用他们的对象都是对象 e,即那个查询到的键值对。了解这一点后,就能看懂了

八、参考

https://www.jianshu.com/p/8f4f58b4b8ab
https://www.cnblogs.com/xrq730/p/5052323.html

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/babycan5/article/details/82937397
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞