LinkedHashMap 原理

xiaoxiao2021-02-28  120

1. 概述

在理解了介绍的HashMap后,我们来学习LinkedHashMap的工作原理及实现。首先还是类似的,我们写一个简单的LinkedHashMap的程序:

LinkedHashMap<String, Integer> lmap = new LinkedHashMap<String, Integer>(); lmap.put("语文", 1); lmap.put("数学", 2); lmap.put("英语", 3); lmap.put("历史", 4); lmap.put("政治", 5); lmap.put("地理", 6); lmap.put("生物", 7); lmap.put("化学", 8); for(Entry<String, Integer> entry : lmap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }

运行结果是:

语文: 1 数学: 2 英语: 3 历史: 4 政治: 5 地理: 6 生物: 7 化学: 8

我们可以观察到,和HashMap的运行结果不同,LinkedHashMap的迭代输出的结果保持了插入顺序。是什么样的结构使得LinkedHashMap具有如此特性呢?我们还是一样的看看LinkedHashMap的内部结构,对它有一个感性的认识:

没错,正如官方文档所说:

Hash tableand  linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a  doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map ( insertion-order ).

LinkedHashMap是Hash表和链表的实现,并且依靠着双向链表保证了迭代顺序是插入的顺序。

这是里面核心的数据结构Entry的实现:

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

        }

    }

    transient LinkedHashMap.Entry<K,V> head;

    /**

     * The tail (youngest) of the doubly linked list.

     */

    transient LinkedHashMap.Entry<K,V> tail;

2. 三个重点实现的函数

在HashMap中提到了下面的定义:

// Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { }

LinkedHashMap继承于HashMap,因此也重新实现了这3个函数,顾名思义这三个函数的作用分别是:节点访问后、节点插入后、节点移除后做一些事情。

afterNodeAccess函数,这里 accessOrder = true才会按照访问顺序调整排序。

void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; // 如果定义了accessOrder,那么就保证最近访问节点放到最后 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; } }

就是说在进行put之后就算是对节点的访问了,那么这个时候就会更新链表,把最近访问的放到最后,保证链表。

afterNodeInsertion函数

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

removeEldestEntry 方法返回为true时,删除root元素,特别适合做LRUCache,例如sdk提供的LRUCahce就是基于这个原理实现的 如果用户定义了 removeEldestEntry 的规则,那么便可以执行相应的移除操作。下面是一个LRU的实现的一个例子:

import java.util.*;

//扩展一下LinkedHashMap这个类,让他实现LRU算法 

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

// 定义缓存的容量

private int capacity;

private static final long serialVersionUID = 1L;

// 带参数的构造器

LRULinkedHashMap(int capacity) {

// 调用LinkedHashMap的构造器,传入以下参数

super(16, 0.75f, true);

// 传入指定的缓存最大容量

this.capacity = capacity;

}

// 实现LRU的关键方法,如果map里面的元素个数大于了缓存最大容量,则删除链表的顶端元素

@Override

public boolean removeEldestEntry(Map.Entry<K, V> eldest) {

System.out.println("the eldest is:" + eldest.getKey() + "=" + eldest.getValue());

return size() > capacity;

}

// test

public static void main(String[] args) throws Exception {

// 指定缓存最大容量为4

Map<Integer, Integer> map = new LRULinkedHashMap<>(4);

map.put(9, 3);

map.put(7, 4);

map.put(5, 9);

map.put(3, 4);

map.put(6, 6);

// 总共put了5个元素,超过了指定的缓存最大容量

// 遍历结果

for (Iterator<Map.Entry<Integer, Integer>> it = map.entrySet().iterator(); it.hasNext();) {

System.out.println(it.next().getKey());

}

}

}

afterNodeRemoval函数

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

这个函数是在移除节点后调用的,就是将节点从双向链表中删除。

我们从上面3个函数看出来,基本上都是为了保证双向链表中的节点次序或者双向链表容量 所做的一些额外的事情,目的就是保持双向链表中节点的顺序要从eldest到youngest。

3. put和get函数

put 函数在LinkedHashMap中未重新实现,只是实现了 afterNodeAccess 和 afterNodeInsertion 两个回调函数。 get 函数则重新实现并加入了 afterNodeAccess 来保证访问顺序,下面是 get 函数的具体实现:

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

值得注意的是,在accessOrder模式下,只要执行get或者put等操作的时候,就会产生 structural modification 。官方文档是这么描述的:

A structural modification is any operation that adds or deletes one or more mappings or, in the case of access-ordered linked hash maps, affects iteration order. In insertion-ordered linked hash maps, merely changing the value associated with a key that is already contained in the map is not a structural modification.  In access-ordered linked hash maps, merely querying the map with get is a structural modification .

不要犯了像 ConcurrentModificationException with LinkedHashMap 类似的问题。

总之,LinkedHashMap不愧是HashMap的儿子,和老子太像了,当然,青出于蓝而胜于蓝,LinkedHashMap的其他的操作也基本上都是为了维护好那个具有访问顺序的双向链表。:-)

参考:http://www.tuicool.com/articles/IB3ANb

转载请注明原文地址: https://www.6miu.com/read-50602.html

最新回复(0)