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

直接看图示就明白了



接下来再看 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 方法将查询到的元素删除

然后在调用该键值对的 addBefore 方法将删除的元素置于链表的最后一位

这里我就画了 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

猜你喜欢

转载自blog.csdn.net/babycan5/article/details/82937397