LinkedHashMap source code analysis

Keywords: Java data structure

LinkedHashMap source code analysis

summary

This class inherits HashMap, so it has all the features of HashMap. It maintains data in the form of two-way linked list, so as to maintain the order of internal data, that is, the order of insertion is the order of iteration. When the accessOrder access mode is set, when accessing the elements in the structure, the accessed elements will always be moved to the tail of the linked list. This feature is very suitable for implementing the LRU binding mechanism.

Member variable

// The head node of a two-way linked list is also the first data added to it
transient LinkedHashMapEntry<K,V> head;

// The tail node of the two-way linked list is finally added to the data inside the LinkedHashMap
transient LinkedHashMapEntry<K,V> tail;

// Two access modes are defined: true and false. You will see the difference between them in subsequent code analysis
final boolean accessOrder;

Constructor

// Given the initial capacity and load factor, initialize accessOrder to false
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

// Specify the initial capacity, the load factor will be initialized with the default value of 0.75, and the accessOrder initialization is false
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}
// Initialize with the default capacity of 16 and the default load factor of 0.75. The accessOrder initializes false
public LinkedHashMap() {
    super();
    accessOrder = false;
}

// Initialize with a structure that implements the Map interface, and initialize accessOrder to false
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

// Specify the initial capacity, load factor, and access mode at the same time. There are two modes: access mode and insert mode
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

LinkedHashMap defines five constructors, which are mainly used to initialize three important variables, initial capacity, load factor and access mode. The first two are inherited from HashMap, and the last one is maintained by itself. The access mode defines whether to move the access element when accessing the element. If the access mode is true, access an element, At the same time, the element node will be moved to the tail of the two-way linked list. This is because LinkedHashMap has a feature that the elements added to it will be sorted from the header to the tail according to the addition order. The elements at the tail of the table are always newly added or recently accessed. The accessOrder variable maintains such a feature.

Add element

LinkedHashMap does not re implement the add method. It still calls the put method of the parent class HashMap to add. Here, the put method in the HashMap will be pointed out: for the implementation logic of this method, please refer to: HashMap source code analysis, efficient key value pair storage structure

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)
    tab[i] = newNode(hash, key, value, null);// Note 1 
  else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    else if (p instanceof TreeNode)
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);// Note 2
          if (binCount >= TREEIFY_THRESHOLD - 1)
            treeifyBin(tab, hash);
          break;
        }
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) {
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}

The newNode method is called at both places in the above comments, and LinkedHashMap overloads this method. In its tree adding method, there is also a newTreeNode method overloaded inside LinkedHashMap,

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // For the analysis of LinkedHashMapEntry, please see below
    LinkedHashMapEntry<K,V> p =
        new LinkedHashMapEntry<K,V>(hash, key, value, e);
    linkNodeLast(p);// Later analysis
    return p;
}

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    // The analysis of TreeNode will also appear later
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    linkNodeLast(p);// Later analysis
    return p;
}

The definition of LinkedHashMapEntry is as follows. It inherits the Node class defined in HashMap and defines two variables before and after to specify the previous Node and the latter Node of the current Node. The definition is very simple.

static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
    LinkedHashMapEntry<K,V> before, after;// Specify the previous node and the next node of the current node
    LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

The implementation of TreeNode is also interesting here. Its parent class is LinkedHashMapEntry. In this way, the relationship between HashMap.Node, LinkedHashMapEntry and TreeNode is established, and they are inherited by the latter in turn

// Note the parent class of TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
    TreeNode<K,V> parent; 
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;   
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
}

Looking back at newNode, the linkNodeLast method in the newTreeNode method: this method is used to insert the new element as the tail node of the linked list into the two-way linked list. Calling this method can always ensure that the new element will be at the end of the linked list.

private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;// Make the new element the footer element
    if (last == null)// If the original footer is null, it means that there are no elements in the table. At this time, the header reference should also be null. At this time, the header should also be set as a new element
        head = p;
    else {// If there is data in the original linked list, insert the new element at the end of the linked list
        p.before = last;
        last.after = p;
    }
}

Delete element

LinkedHashMap also does not overload the method of deleting elements. In the implementation of HashMap, there is an empty method afterNodeRemoval, which will be called when the node is finally removed in the removeNode method of HashMap, and its implementation in LinkedHashMap is as follows:

void afterNodeRemoval(Node<K,V> e) {
    // Find the predecessor node and successor node of the current deleted node
    LinkedHashMapEntry<K,V> p =
        (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;// Set the predecessor and successor of the currently deleted node to null,
    if (b == null)// If the predecessor node of the current deleted node is null, it indicates that the deleted node is the head node
        head = a;// Here, the successor node of the deleted node is set as the head node
    else
        b.after = a;// Set the successor of the predecessor of the deleted node as the successor of the deleted node, that is, break the chain of the current node from the linked list and move it out of the linked list
    if (a == null)// If the subsequent node is null, it means that the last node is deleted. Set the tail node as the precursor of the current deleted node
        tail = b;
    else
        a.before = b;//If it is not the last node, its predecessor and successor are connected
}

The function of this method is to remove the deleted node from the linked list, and then connect its predecessor and successor. In this way, the order of the linked list can still be guaranteed after the node is deleted from the linked list.

Get element

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)// The access mode is used here. If the mode is true, the afterNodeAccess method will be called. The function of this method is to move the node to the end of the linked list to indicate that the node has just been accessed.
        afterNodeAccess(e);
    return e.value;
}

// Move the node to the end of the linked list
void afterNodeAccess(Node<K,V> e) { 
    LinkedHashMapEntry<K,V> last;
    if (accessOrder && (last = tail) != e) {// If it is an access mode and the access node is not the tail node, you need to move the node to the tail
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;//Recording precursors and successors
        p.after = null;// The current access node is set to null
        if (b == null)// If the predecessor node is null, it means that the accessed node is the header. At this time, the subsequent node is set as the header node, so that the currently accessed node is removed from the linked list
            head = a;
        else
            b.after = a;// Otherwise, the successor of the predecessor is set as the successor node, and the current access node is also removed from the linked list
        if (a != null)// If the subsequent node is not null, the subsequent precursor is set as the precursor node
            a.before = b;
        else
            last = b;//Otherwise, the deleted node is the tail node, and the tail node is set as the precursor
        // The above code removes the access node from the linked list first
        // ===============================
        // The following code moves the access node to the end of the linked list
        if (last == null)// If the tail node is null at this time, there is no data in the linked list. Set the access node as the header
            head = p;
        else {// If the chain tail is not null, the access node is linked to the chain tail
            p.before = last;
            last.after = p;
        }
        tail = p;// Set the end of the linked list as the access node
        ++modCount;// Operand plus 1 
    }
}

From the implementation of afterNodeAccess, if the access mode is set to true, the current access node will be moved to the tail, indicating that this node has just been accessed.

summary

  1. Have all the features of HashMap
  2. It implements the double linked list mechanism, so it adds an ordered feature to HashMap.
  3. When the accessOrder mode is set to true, the accessed node will move to the end of the linked list.

Scan code to pay attention to official account, browse more articles

Posted by JTapp on Thu, 02 Sep 2021 10:49:01 -0700