基于源码搞懂LinkedHashMap并通过其实现LRU算法

LinkedHashMap 是通过哈希表和双向链表来实现的,其基于双向链表来保证对哈希表迭代时的有序性。

LinkedHashMap 继承自 HashMap,从而可以直接复用 HashMap 对哈希表的操作逻辑,其只需要额外维护1套双向链表的操作逻辑即可。

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
	......
}

节点

LinkedHashMap 的节点 Entry 继承自 HashMap 的节点 Node。

LinkedHashMap.Entry 在 HashMap.Node 的基础上增加了2个指针 before(前向指针) 和 after(后向指针),以用于实现双向链表。

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

插入元素

LinkedHashMap 没有实现 put() 方法,直接复用 HashMap 的 put() 方法:

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

在这里插入图片描述
这时候,有读者就会问,LinkedHashMap 不是还需要维护1个双向链表吗?HashMap 的 put() 方法里也没有涉及双向链表的操作呀!

其实,LinkedHashMap 是通过覆写 HashMap 的 newNode() 方法来实现双向链表的。

其调用路径是:

HashMap.put()-->HashMap.putVal()-->LinkedHashMap.newNode()

接着看一下 LinkedHashMap 的 newNode() 方法:

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

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
	LinkedHashMap.Entry<K,V> last = tail;
	// 将尾节点指针更新到当前节点
	tail = p;
	// 若当前链表的尾节点为null,说明该链表为空,将头节点指针也更新到当前节点
	if (last == null)
		head = p;
	else {
		// 当前的前向节点为原链表的尾节点
		p.before = last;
		// 原链表尾节点的后向节点为当前节点
		last.after = p;
	}
}

其实就是将当前节点连接到当前链表的尾部,为了同时支持红黑树的节点,LinkedHashMap 也覆写了 newTreeNode() 方法:

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
	TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
	linkNodeLast(p);
	return p;
}

为了能够插入到 LinkedHashMap 的双向链表中,HashMap 的红黑树节点 TreeNode 必然是 LinkedHashMap.Entry 的子类,打开 HashMap 的源码验证一下:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
	TreeNode<K,V> parent;  // red-black tree links
	TreeNode<K,V> left;
	TreeNode<K,V> right;
	TreeNode<K,V> prev;    // needed to unlink next upon deletion
	boolean red;
	TreeNode(int hash, K key, V val, Node<K,V> next) {
		super(hash, key, val, next);
	}
	...
}

哈哈哈,果不其然。LinkedHashMap 继承自 HashMap,而 HashMap.TreeNode 又是继承自 LinkedHashMap.Entry,所以 LinkedHashMap 和 HashMap 是你中有我,我中有你的关系,名副其实的好基友。

除了 newNode() 和 newTreeNode() 方法,HashMap 的 purVal() 方法步骤里还有2个回调方法值得我们关注,就是 afterNodeAccess() 和 afterNodeInsertion() 方法。

顾名思义,afterNodeAccess() 方法是执行节点访问时回调,afterNodeInsertion() 方法是执行节点插入时访问。

看一下这2方法在 HashMap 中的实现:

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

均是空实现,也就是说 HashMap 在查询或插入元素的时候,相当于这俩回调方法不执行任何操作。

但 LinkedHashMap 却覆写了这2方法,以用来实现某些特殊逻辑。

首先看一下 LinkedHashMap.afterNodeAccess():

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

一句话概括就是,当有节点访问操作时,且 accessOrder 设置为 true,会将当前节点移动到双向链表的尾部(若当前节点已经是双向链表的尾部,则无需任何操作)

换句话说,当 accessOrder 设置为 true 时,越是最近访问过的节点越靠近双向链表的尾部,而很久没有访问过的节点越靠近双向链表的头部,可以看出,LinkedHashMap 可以用于实现 LRU 算法。

接着看一下 LinkedHashMap.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);
	}
}

afterNodeInsertion() 可能会删除老元素,但需要满足3个条件:

  • evict 为 true;
  • (first = head) != null,双向链表的头结点不能为 null,换句话说,双向链表中必须有老元素(没有老元素还删个锤锤);
  • removeEldestEntry(first) 方法返回为 true。

LinkedHashMap 在执行 put() 操作时,调用的是 HashMap 的 put() 方法,而HashMap 的 put() 方法传入的 evict 为 true,所以第1个条件满足。

第2个条件只要 LinkedHashMap 中存在老元素,也很容易满足,关键是第3个条件:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
	return false;
}

LinkedHashMap.removeEldestEntry() 方法直接返回 false,即 LinkedHashMap 在执行元素插入时,默认是不删除老元素的。

如果想要在元素插入时,执行删除老元素的操作,就必须覆写 removeEldestEntry() 方法。

我们再回顾一下,上述2个方法在 HashMap.putVal() 方法中的位置:
在这里插入图片描述
虽然 putVal 属于"增"操作,但如果待插入元素的 key 已经在 Map 中存在时,"增"操作就变成了"改"操作,而除了"查"操作会调用 afterNodeAccess(),"改"操作也会调用 afterNodeAccess() 方法。

“增删改查”!!!

afterNodeAccess() 服务于查和改;

afterNodeInsertion() 服务于增。

谁来服务于删呢?

答案就是 afterNodeRemoval() 方法。

同样的,HashMap.afterNodeRemoval() 也是个空实现。

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

删除元素

与 put() 方法类似,LinkedHashMap 删除元素的操作也直接复用的 HashMap 的 remove() 方法:

public V remove(Object key) {
	Node<K,V> e;
	return (e = removeNode(hash(key), key, null, false, true)) == null ?
		null : e.value;
}

在这里插入图片描述
那如何在哈希表删除元素后,同时在双向链表上删除呢?LinkedHashMap 是通过覆写 HashMap 的 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;
}

查询元素

LinkedHashMap 直接覆写了 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;
}

其与 HashMap.get() 方法相比,其实就是多了个 afterNodeAccess() 的逻辑。
在这里插入图片描述

原则上,与增/删元素类似,afterNodeAccess() 方法也可以直接植入到 HashMap 的 get() 方法中去,由于 HashMap 的 afterNodeAccess() 方法为空实现,所以不会影响 HashMap 的固有逻辑,而 LinkedHashMap 也就不用覆写 get() 方法了,至于 LinkedHashMap 为啥没这么实现,暂时不得而知。

基于LinkedHashMap实现LRU算法

LRU,Least Recently Used 的缩写,即最近最少使用算法,是一种内存数据淘汰策略。使用场景是,当内存不足时,需要淘汰掉缓存中最近最少使用的数据。

缓存一般使用 HashMap 类的数据结构,可以以 O(1) 的复杂度基于 Key 取到对应的 Value,如果在此基础上实现 LRU 算法,则需要额外维护一个链表,当某个数据被访问时,则将该数据移动到链表尾部,这样最近访问到的数据必然存在于链表尾部,而头部位置的数据就是很少被访问到数据,当链表长度到达容量阈值时,可以删除头部数据。

哈哈哈,是不是发现 LinkedHashMap 天然满足上述需求,只需要额外配置2项即可实现 LRU 算法。

  • 将 accessOrder 设置为 true,当有节点访问操作时,会将当前节点移动到双向链表的尾部,从而实现最近最少访问的节点处于链表的头部;
  • 覆写 LinkedHashMap 的 removeEldestEntry() 方法,使其到达缓存容量阈值的时候,触发删除操作。
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
    // 定义缓存容量
    private int capacity;

    public LRULinkedHashMap(int capacity) {
        // 必须将 accessOrder 参数设置为 True,从而将最近访问的元素移动到链表尾部
        super(16, 0.75f, true);
        this.capacity = capacity;
    }

    // 实现 LRU 算法的关键,removeEldestEntry() 方法会在添加元素的时候被调用
    // 当 map 中的元素个数大于缓存最大容量,则删除链表的头元素
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        System.out.println("待删除元素: " + eldest.getKey() + "=" + eldest.getValue());
        return size()>capacity;
    }

    public static void main(String[] args) {
        Map<Integer, String> map = new LRULinkedHashMap<>(4);
        map.put(1, "孙悟空");
        map.put(2, "猪八戒");
        map.put(3, "哪吒");
        map.put(4, "白龙马");
        map.put(5, "唐僧");
        for(Iterator<Map.Entry<Integer,String>> it = map.entrySet().iterator(); it.hasNext();){
            Map.Entry<Integer, String> next = it.next();
            System.out.println(next.getKey() + "=" + next.getValue());
        }
        map.get(2);
        map.put(6, "杨戬");
        for(Iterator<Map.Entry<Integer,String>> it = map.entrySet().iterator(); it.hasNext();){
            Map.Entry<Integer, String> next = it.next();
            System.out.println(next.getKey() + "=" + next.getValue());
        }
		// 更新操作也会将更新的节点移动到链表尾部
        map.put(4, "太白金星");
        System.out.println("################################");
        for(Iterator<Map.Entry<Integer,String>> it = map.entrySet().iterator(); it.hasNext();){
            Map.Entry<Integer, String> next = it.next();
            System.out.println(next.getKey() + "=" + next.getValue());
        }
    }
}

程序运行结果如下:

待删除元素: 1=孙悟空
待删除元素: 1=孙悟空
待删除元素: 1=孙悟空
待删除元素: 1=孙悟空
待删除元素: 1=孙悟空
2=猪八戒
3=哪吒
4=白龙马
5=唐僧
待删除元素: 3=哪吒
4=白龙马
5=唐僧
2=猪八戒
6=杨戬
################################
5=唐僧
2=猪八戒
6=杨戬
4=太白金星

一切符合预期,完美收工。

本文到此结束,感谢阅读!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值