Deep understanding of Java LinkedHashMap

Keywords: Java linked list

In this article, we go deep into the interior of LinkedHashMap, an implementation class of Java Map interface. It is a subclass of HashMap and inherits the core code of the parent class. Therefore, readers should first understand the working principle of HashMap.

LinkedHashMap and HashMap

*LinkedHashMap * is similar to hashMap in most aspects, but LinkedHashMap is based on hash table and linked list structure to enhance hashMap. In addition to the array with the default size of 16, the bottom layer also maintains a two-way linked list to link all items.

In order to maintain the element order, LinkedHashMap modifies the Map.Entry class of HashMap class and adds before and after pointer objects. The source code is as follows:

    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }

We see that the Entry class simply adds two pointers to connect the linked list. In addition, the Entry class is used to implement HashMap.

We are looking at the constructor. The iteration order is defined by default. By default, it is in the insertion order:

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

Insertion order

The following is an example to understand the insertion sequence:

@Test
public void givenLinkedHashMap_whenGetsOrderedKeyset_thenCorrect() {
    LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    Set<Integer> keys = map.keySet();
    Integer[] arr = keys.toArray(new Integer[0]);

    for (int i = 0; i < arr.length; i++) {
        assertEquals(new Integer(i + 1), arr[i]);
    }
}

The above code inserts five entities, then obtains their key sets, and finally iteratively verifies the key order. LinkHashMap can ensure that the order of keys is in the order of insertion, while HashMap cannot guarantee the order of keys.

The entire feature is useful in some scenarios. If the call to API needs to be returned according to the order of the client, LinkHashMap can easily meet it.

Note: if the key is reinserted into the map, the insertion order is not affected.

Access order

In the previous section, we saw that the constructor includes three parameters: initial capacity, loading factor and sorting strategy. The default is the insertion order and can also be set as the access order. This mechanism ensures that the order of iterative elements is from the least recently accessed to the most recently accessed according to the last accessed order of elements:

@Test
public void givenLinkedHashMap_whenAccessOrderWorks_thenCorrect() {
    LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    Set<Integer> keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());
 
    map.get(4);
    assertEquals("[1, 2, 3, 5, 4]", keys.toString());
 
    map.get(1);
    assertEquals("[2, 3, 5, 4, 1]", keys.toString());
 
    map.get(3);
    assertEquals("[2, 5, 4, 1, 3]", keys.toString());
}

This strategy makes it easy to implement the Least Recently Used (LRU) caching algorithm. The access operation to the map specific key will cause the change key to appear at the end of the iteration. From the above example, it is obvious that iterating LinkHashMap will not affect the order, but only the access operations displayed will affect the order.

LinkHashMap also provides a mechanism to maintain the fixed size of map entries. When the number of existing entries is the largest, the oldest element will be deleted when a new element is inserted.

We can override the removeEldestEntry method to force the policy to automatically delete obsolete entries. The following example defines the MyLinkedHashMap class, overriding

removeEldestEntry method:

public class MyLinkedHashMap<K, V> extends LinkedHashMap<K, V> {

    private static final int MAX_ENTRIES = 5;

    public MyLinkedHashMap(
      int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }

}

When the entry exceeds the maximum value, the new entry is inserted at the cost of discarding the oldest entry, that is, the most recently accessed entry takes precedence over other entries:

@Test
public void givenLinkedHashMap_whenRemovesEldestEntry_thenCorrect() {
    LinkedHashMap<Integer, String> map
      = new MyLinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);
    Set<Integer> keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());
 
    map.put(6, null);
    assertEquals("[2, 3, 4, 5, 6]", keys.toString());
 
    map.put(7, null);
    assertEquals("[3, 4, 5, 6, 7]", keys.toString());
 
    map.put(8, null);
    assertEquals("[4, 5, 6, 7, 8]", keys.toString());
}

We see that when a new entry is added, the oldest entry at the beginning of the key set is lost.

other aspects

  • Concurrent

Like HashMap, LinkHashMap implementation does not have synchronization function, so if multiple threads access at the same time, synchronization control needs to be added externally. You can create in this way:

Map m = Collections.synchronizedMap(new LinkedHashMap());

What is different from HashMap is that the operation will be structurally modified. If you access LInkHashMap sequentially, you will have structural modifications only by calling get operation. In addition, the same is true for put and remove.

  • Performance description

Like HashMap, LInkHashMap performs basic Map operations, such as add, remove and contain. The operation time is constant. As long as the hash function has enough dimensions, it also supports storing null keys and values.

However, the constant time of LInkHashMap is slightly longer than that of HashMap, because it requires additional maintenance of two-way linked list. The iterative LInkHashMap set is linear o (n) like HashMap, and the performance of LInkHashMap is better than HashMap. This is because the number of elements in LInkHashMap is independent of capacity. N of HashMap is the sum of capacity and size, that is, O(size+capacity).

The load factor and initialization capacity are the same as those of HashMap, but the impact on LinkHashMap is small, because the iteration is not affected by the capacity.

summary

In this article, we learned LinkHashMap, which is the most important implementation of Map interface. By exploring its characteristics, differences with HashMap, internal working principle and application scenarios.

Posted by Lillefix on Sun, 19 Sep 2021 22:10:10 -0700