JDK source code -- LinkedHashMap class

Posted by voxanBoxer on Tue, 14 Dec 2021 10:34:34 +0100

abstract

This blog will introduce the LinkedHashMap class in detail

LinkedHashMap basic data structure

We know that HashMap is out of order, that is, the order of iterators has nothing to do with the insertion order. On the basis of HashMap, LinkedHashMap adds order: insert order and access order respectively. That is, when traversing LinkedHashMap, the order consistent with the insertion order can be maintained; Or the order consistent with the access order.

How does LinkedHashMap implement these two sequences internally? It is maintained through a double linked list. Therefore, LinkedHashMap can be understood as "double linked list + hash table" or "ordered hash table".

Member variables of LinkedHashMap

/**
 * Head pointer
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * Tail pointer
 */
transient LinkedHashMap.Entry<K,V> tail;

/**
 * LinkedHashMap Iteration order: true is the access order; false is the insertion order.
 */
final boolean accessOrder;

Node class of LinkedHashMap

LinkedHashMap has a nested class Entry, which inherits from the Node class in the HashMap, as follows:

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);
    }
}
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    // ...
}

The Entry class is the Node class in LinkedHashMap. It can be seen that it adds before and after variables on the basis of the Node class. They save the precursor and successor of the Node (which can also be inferred from the literal meaning), so as to maintain the order of LinkedHashMap.

Constructor of LinkedHashMap

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

there super() Method called HashMap A parameterless constructor for. The constructor method constructs a capacity of 16 (default initial capacity) and a load factor of 0.75(Empty for default load factor) LinkedHashMap,The order is the insertion order.

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

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    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;
}

You can see that the above constructors are initialized by calling the constructor method of the parent class (HashMap), which will not be analyzed. The default value of the accessOrder variable of the first four constructors is false; the last one is slightly different, and its accessOrder can be specified during initialization, that is, the order of LinkedHashMap (insertion or access order).

put method of LinkedHashMap

LinkedHashMap itself does not implement the put method. It reads and writes by calling the method of the parent class (HashMap). The put method of HashMap is pasted here:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        // New bin node
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // key already exists
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // Hash collision 
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // Traversal linked list
            for (int binCount = 0; ; ++binCount) {
                // Insert the new node at the end of the linked list
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

Where is this method related to LinkedHashMap? How can I keep the order of LinkedHashMap? Let's look at the newNode() method. Its code in HashMap is as follows:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

However, LinkedHashMap overrides this method: each time a new node is inserted, it is saved to the end of the list. I see. The insertion order of LinkedHashMap is implemented here.

// Create a LinkedHashMap Entry node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // Connect the new node to the end of the list
    linkNodeLast(p);
    return p;
}
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    // list is empty
    if (last == null)
        head = p;
    else {
        // Insert the new node at the end of the list
        p.before = last;
        last.after = p;
    }
}

In addition, when analyzing HashMap above, we mentioned two callback methods: afterNodeAccess and afterNodeInsertion. They are empty in HashMap:

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }

Similarly, LinkedHashMap rewrites them. First, analyze the afterNodeAccess method:

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // accessOrder = true indicates the access order
    if (accessOrder && (last = tail) != e) {
        // p is the visited node, b is its predecessor, and a is its successor
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        // p is the header node
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

get method of LinkedHashMap

LinkedHashMap rewrites the get method of HashMap, mainly to maintain the access order.

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // For access order, move the accessed node to the end of the list
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

The getNode method here is the parent class (HashMap). If accessOrder is true (i.e. specified as access order), the accessed node will be moved to the end of the list.

Overridden afterNodeInsertion method in LinkedHashMap:

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

// The default return value in LinkedHashMap is false, that is, the removeNode method here does not execute
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

The removeNode method is in the parent class HashMap:

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable
) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // table is not empty, and the location of the given hash value is not empty
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // The node corresponding to the given key is the first position in the array
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // The given key is located in a red black tree or linked list
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // Delete node
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            // Action after deleting a node
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

The implementation of afterNodeRemoval method in HashMap is also empty:

void afterNodeRemoval(Node<K,V> p) { }

LinkedHashMap overrides this method:

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

LinkedHashMap actual combat

LinkedHashMap, you can maintain the insertion order:

Map<String, String> map = new LinkedHashMap<>();
map.put("bush", "a");
map.put("obama", "b");
map.put("trump", "c");
map.put("lincoln", "d");
System.out.println(map);

// Output results (insertion order):
// {bush=a, obama=b, trump=c, lincoln=d}
Map<String, String> map = new LinkedHashMap<>(2, 0.75f, true);
map.put("bush", "a");
map.put("obama", "b");
map.put("trump", "c");
map.put("lincoln", "d");
System.out.println(map);

map.get("obama");
System.out.println(map);

// Output results (insertion order):
// {bush=a, obama=b, trump=c, lincoln=d}

// After visiting obama, obama moved to the end
// {bush=a, trump=c, lincoln=d, obama=b}

LinkedHashMap summary

  1. LinkedHashMap inherits from HashMap, and its structure can be understood as ordered [double linked list + hash table];
  2. Two orders can be maintained: insert order or access order;
  3. LRU cache can be easily realized;
  4. Thread unsafe.

Blog reference

JDK source code analysis - LinkedHashMap - Zhihu

Topics: JDK