Why is LinkedHashMap the largest data structure in Java? Get to know?

Keywords: Java

Yun Qi Hao: https://yqh.aliyun.com
The first-hand cloud information, the selected cloud enterprise case base of different industries, and the best practices extracted from many successful cases help you to make cloud decision!

There are many Map families, among which HashMap and ConcurrentHashMap are used the most, while LinkedHashMap seems not to be used much, but it has order. There are two kinds, one is adding order, the other is access order.

LinkedHashMap inherits the HashMap. So if it's you, how do you achieve these two orders?

If the order of adding is implemented, we can add a linked list in this class, and each node corresponds to the bucket in the hash table. In this way, when the loop is traversed, it can be traversed according to the linked list. It just increases memory consumption.

If the access sequence is implemented, the linked list can also be used, but each time you read data, you need to update the linked list to put the last read one at the end of the chain. This can be achieved. At this time, you can also follow up this feature to implement LRU (Least Recently Used) caching.

How to use it?

Here is a small demo

LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
for (int i = 0; i < 10; i++) {
  map.put(i, i);
}

for (Map.Entry entry : map.entrySet()) {
  System.out.println(entry.getKey() + ":" + entry.getValue());
}
map.get(3);
System.out.println();
for (Map.Entry entry : map.entrySet()) {
  System.out.println(entry.getKey() + ":" + entry.getValue());
}

Print results:

0:0
1:1
2:2
3:3
4:4
5:5
6:6
7:7
8:8
9:9

0:0
1:1
2:2
4:4
5:5
6:6
7:7
8:8
9:9
3:3

It's interesting to construct the method first, which has one more accessOrder boolean parameter than HashMap. Represents, sorted by access order. The latest access is placed at the end of the list.

If it is the default, it is in the order of adding, that is, accessOrder is false by default.

Source code implementation

If you look at the internal source code of LinkedHashMap, you will find that there is indeed a linked list maintained internally:

/**
 * The head of a two-way linked list, the longest accessed
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * The end of a two-way linked list, most recently accessed
 */
transient LinkedHashMap.Entry<K,V> tail;

The LinkedHashMap.Entry also maintains the necessary elements of the two-way linked list. before, after:

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

When you add elements, they are appended to the tail.

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);
    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;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

When get, the list order will be modified according to the accessOrder property:

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

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        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;
    }
}

Note at the same time: modCount is modified here. Even for read operations, concurrency is not safe.

How to implement LRU cache?

LRU cache: LRU (Least Recently Used) algorithm eliminates data according to the historical access record of data. Its core idea is "if the data has been accessed recently, the probability of being accessed in the future is higher".
LinkedHashMap doesn't help us to implement the concrete, we need to implement it ourselves. The specific implementation method is the removealdestentry method.
Let's see how it works.

First of all, at the end of putVal method, HashMap will call afterNodeInsertion method, which is actually reserved for LinkedHashMap. The specific implementation of LinkedHashMap is to determine whether the head node needs to be deleted according to some conditions.

The source code is as follows:

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 evict parameter indicates whether or not an element needs to be deleted. The if condition is as follows: head cannot be null. Call the removealdestentry method and return true to delete the head. This method returns false by default, waiting for you to override it.

Therefore, the implementation of the removealdestentry method is usually as follows:

public boolean removeEldestEntry(Map.Entry<K, V> eldest){
   return size() > capacity;
}

If the length is larger than the capacity, you need to clear the cache that is not frequently accessed. afterNodeInsertion will call the removeNode method to delete the head node - if accessOrder is true, this node is the least frequently accessed node.

Pick up

LinkedHashMap rewrites some methods of HashMap, such as containsValue method. Let's guess how to rewrite it reasonably?
HashMap uses a double loop, first loop the outer hash table, and then loop the inner entry list. Performance is conceivable.

However, there is a linked list of elements in LinkedHashMap, which can be traversed directly. Relatively speaking, it is much higher.

public boolean containsValue(Object value) {
    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
        V v = e.value;
        if (v == value || (value != null && value.equals(v)))
            return true;
    }
    return false;
}

This is also a space for time strategy.

The get method is also rewritten. Because the linked list needs to be updated according to the accessOrder.

summary

Xuewei's summary:

LinkedHashMap contains a two-way list maintenance order, which supports two kinds of order: adding order and access order.

The default is based on the order of adding. If you want to change the access order, the accessOrder in the construction method needs to be set to true. In this way, every time the get method is called, the element just accessed will be updated to the end of the list.

With regard to LRU, in the mode of accessOrder being true, you can override the removealdestentry method to return size() > capacity, so that the least frequently accessed elements can be deleted.

Original release time: January 9, 2020
Author: Mona Yilu Road
This article is from Alibaba cloud Qihao partner“ Internet architect ”, you can pay attention to“ Internet architect"

Yun Qi Hao: https://yqh.aliyun.com
The first-hand cloud information, the selected cloud enterprise case base of different industries, and the best practices extracted from many successful cases help you to make cloud decision!

Posted by ddemore on Fri, 10 Jan 2020 00:34:32 -0800