LinkedHashMap Source Code Analysis

Keywords: Java Attribute

brief introduction

LinkedHashMap maintains a two-way linked list, which ensures that elements are accessed in the order of insertion, and also in the order of access. It can be used to implement the LRU caching strategy.

LinkedHashMap can be seen as LinkedList + HashMap.

Inheritance System

LinkedHashMap inherits HashMap, has all the features of HashMap, and adds additional features for sequential access.

storage structure

We know that HashMap uses the storage structure (array + single linked list + red-black tree). Through the above inheritance system, we know that LinkedHashMap inherits Map, so it has three internal structures, but it also adds a "two-way linked list" structure to store the order of all elements.

Adding deleted elements requires maintaining both the storage in HashMap and the storage in LinkedList, so performance is slightly slower than that in HashMap.

Source code parsing

attribute


/**
* Header Node of Bidirectional Link List
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* Two-way list tail node
*/
transient LinkedHashMap.Entry<K,V> tail;

/**
* Is it sorted in order of access?
*/
final boolean accessOrder;

1.head
Head node of bidirectional linked list, old data has header node.

2.tail
The tail node of the bidirectional linked list exists in the new data.

3.accessOrder
Do you need to sort in order of access, store elements in order of insertion if false, and store elements in order of access if true?

Internal class


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

// Located in HashMap
static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;
}

Storage nodes, inherited from the Node class of HashMap, next is used to store single linked lists in buckets, before and after are used to store all elements in two-way linked lists.

Construction method

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

public LinkedHashMap() {
    super();
    accessOrder = false;
}

public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

The first four constructions, accessOrder, are equal to false, indicating that the two-way linked list stores elements in insertion order.

The last constructor, accessOrder, is passed in from the constructor parameters. If true is passed in, elements are stored in order of access, which is also the key to the implementation of LRU caching strategy.

After Node Insertion (boolean evict) method

What to do after node insertion is called in the putVal() method in HashMap, and you can see that the implementation of this method in HashMap is empty.

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
  1. If evict is true and the header node is not empty and the oldest element is determined to be removed, then HashMap.removeNode() is called to remove the header node (where the header node is the header node of a bidirectional list, not the first element in a bucket).
  2. When HashMap.removeNode() removes this node from HashMap, the afterNodeRemoval() method is called.
  3. The afterNodeRemoval() method is also implemented in LinkedHashMap to modify the bidirectional list after removing elements, as shown below.
  4. The default removeEldestEntry() method returns false, meaning that the element is not deleted.

After Node Access (Node < K, V > e) method

Called after node access, mainly when put() already exists or get(), if accessOrder is true, call this method to move the visited node to the end of the bidirectional list.

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // If the accessOrder is true, and the accessed node is not the tail node
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // Remove the p node from the bidirectional list
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;

        if (a != null)
            a.before = b;
        else
            last = b;

        // Put the p node at the end of the two-way list
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        // The tail node equals p
        tail = p;
        ++modCount;
    }
}
  1. If the accessOrder is true, and the accessed node is not the tail node;
  2. Remove accessed nodes from the bidirectional list;
  3. Add the visited node to the end of the bidirectional list; (The last element is the latest accessed)

After Node Removal (Node < K, V > e) method

Method called after the node has been deleted.


void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // Delete node p from the bi-directional list.
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

The classical method of deleting nodes from the bi-directional list.

get(Object key) method

Get the element.

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

If the element is found and the accessOrder is true, the afterNodeAccess() method is called to move the accessed node to the end of the bidirectional list.

summary

  1. LinkedHashMap inherits from HashMap and has all the features of HashMap.
  2. LinkedHashMap maintains a two-way linked list to store all elements.
  3. If accessOrder is false, elements can be traversed in the order in which they are inserted.
  4. If accessOrder is true, elements can be traversed in the order in which they are accessed.
  5. The implementation of LinkedHashMap is very exquisite. Many methods are Hook left in HashMap. By directly implementing these Hooks, the corresponding functions can be realized without rewriting put() and other methods.
  6. The default LinkedHashMap does not remove the old elements. If you need to remove the old elements, you need to rewrite the removeEldestEntry() method to set the removal policy.
  7. LinkedHashMap can be used to implement LRU cache elimination strategy.

LinkedHashMap Implementation of LRU Cache Elimination Strategy

LRU, Least Current Used, is the Least Recently Used element, that is to say, priority is given to the Least Recently Used element.

If we use LinkedHashMap, we can almost implement this strategy by setting accessOrder to true:


package com.coolcoding.code;

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUTest {
    public static void main(String[] args) {
        // Create a cache with only five elements
        LRU<Integer, Integer> lru = new LRU<>(5, 0.75f);
        lru.put(1, 1);
        lru.put(2, 2);
        lru.put(3, 3);
        lru.put(4, 4);
        lru.put(5, 5);
        lru.put(6, 6);
        lru.put(7, 7);

        System.out.println(lru.get(4));

        lru.put(6, 666);

        // Output: {3 = 3, 5 = 5, 7 = 7, 4 = 4, 6 = 666}
        // You can see that the oldest element has been deleted
        // And the last four visits were moved to the back.
        System.out.println(lru);
    }
}

class LRU<K, V> extends LinkedHashMap<K, V> {

    // The capacity to save caches
    private int capacity;

    public LRU(int capacity, float loadFactor) {
        super(capacity, loadFactor, true);
        this.capacity = capacity;
    }

    /**
    * Rewrite the removeEldestEntry() method to set when to remove old elements
    * @param eldest
    * @return
    */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // When the number of elements is greater than the cached capacity, the element is removed
        return size() > this.capacity;
    }
}

Posted by trevprellie on Mon, 12 Aug 2019 06:16:22 -0700