LinkedHashMap源码分析及实现LRU算法

PS: 要先了解HashMap的实现原理HashMap源码分析

一、简单介绍

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

LinkedHashMap

可以看到LinkedHashMap继承了HashMap,其实际是在HashMap基础上,把数据节点连成一个双向链表,遍历的时候按链表顺序遍历。

小总结

默认的LinkedHashMap 的遍历会按照插入的顺序遍历出来,HashMap则不保证顺序。

注意上面是默认的情况,LinkedHashMap中还有个accessorder成员标志,默认是false,当为true时,每get一个元素,都会把这个元素放在链表最后,即遍历的时候就变成最后被遍历出来。

二、源码分析

//通过继承HashMap.Node类,新增两个成员,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);
     }
 }

这里继承了父类HashMap的Node类,新增了两个成员before,after,用于实现链表结构。

类成员

//指向链表的头结点
transient LinkedHashMap.Entry<K,V> head;
//指向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;

//为false时,遍历会按插入的顺序遍历
//为true时,每一次get操作,都会把获得的节点放到链表尾
//默认为false
final boolean accessOrder;

接下来就是分析从增删查来读LinkedHashMap源码。

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

这三个方法是父类HashMap留给子类实现的方法,分别在get、put、remove方法完成后调用

1. 新增操作

一开始笔者想找put方法,但发现其并没有重写父类的put方法,转去找Entry类在哪里使用到。找到以下两个方法。

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

//红黑树结构时用到
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;
 }

上面两个方法是重写父类的,父类HashMap的put方法中,如果是一个当前不存在的key,就会根据不同结构分别调用上面两个方法来创建新节点,这样就能把父类HashMap里的节点换成LinkHashMap中的通过“改装”的节点了。

接下来看linkNodeLast()方法操作实现链表

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;//旧的尾节点下一个节点指向新的尾节点
     }
}

就是把新增的节点放在链表最后,如果链表一开始为空,那就赋值给头结点

再看看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()方法总是返回false,所以该方法等于没有做任何操作。

小总结
LinkedHashMap的新增操作通过父类HashMap来完成,它则是通过定以义一个Entry继承父类Node的,然后利用Entry实现了链表的结构。

2. 查操作

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

getNode()方法在父类HashMap中实现了,LinkedHashMap重用其来查询数据,然后根据accessOrder的值来决定是否需要调整链表.

下面看afterNodeAccess()方法的做法

void afterNodeAccess(Node<K,V> e) {
     LinkedHashMap.Entry<K,V> last;
     if (accessOrder && (last = tail) != e) {
//进入条件:accessOrder为true,且尾节点不等于e
         LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, 
	b = p.before, a = p.after;
//p就是节点e,b是节点的前节点,a是e的后节点
         p.after = null;//先把p的节点赋值为null
         if (b == null)//说明p是头结点
             head = a;
         else
             b.after = a;//把p的前节点连上p的后节点
         if (a != null)//这里肯定不为null吧??
             a.before = b;//把p的后节点连上p的前节点
         else
             last = b;
         if (last == null)
             head = p;
         else {
             p.before = last;//把p的前节点指向尾节点
             last.after = p;//把旧尾节点的后节点指向p
         }
         tail = p;//尾节点赋值为p
         ++modCount;
     }
 }

afterNodeAccess()方法做的就是,把传入节点e,移动到链表尾部。

2.2 遍历操作

来看看其遍历如何实现,挑entrySet方法来说,先回顾下遍历写法

	Iterator it = map.entrySet().iterator();
	while(it.hasNext())
		it.next();

追踪其源码如下

public Set<Map.Entry<K,V>> entrySet() {
       Set<Map.Entry<K,V>> es;
       return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
   }

public final Iterator<Map.Entry<K,V>> iterator() {
    return new LinkedEntryIterator();
}

final class LinkedEntryIterator extends LinkedHashIterator
       implements Iterator<Map.Entry<K,V>> {
       public final Map.Entry<K,V> next() { 
			return nextNode(); 
		}
 }

可以看到map.entrySet().iterator()这一句代码返回的是LinkedEntryIterator对象。而其next()方法,则是调用了nextNode()方法,该方法在父类LinkedHashIterator中实现了

LinkedHashIterator() {
       next = head;//next一开始指向链表头结点
       expectedModCount = modCount;
       current = null;
}

final LinkedHashMap.Entry<K,V> nextNode() {
//next一开始是指向head(即指向链表头)
     LinkedHashMap.Entry<K,V> e = next;
     if (modCount != expectedModCount)//快速失败
         throw new ConcurrentModificationException();
     if (e == null)
         throw new NoSuchElementException();
     current = e;
     next = e.after;//next指向下一个节点
     return e;
 }

可以看到遍历如前文所说,是遍历链表。

3. 删除操作

LinkedHashMap没有重写remove方法,而是重写了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的前后指向赋值为null
     p.before = p.after = null;
     if (b == null)//说明是头结点
         head = a;
     else
         b.after = a;//p原来的前节点指向p原来的后节点
     if (a == null)//说明p是尾节点
         tail = b;
     else
         a.before = b;//p原来的后节点指向p原来的前节点
 }

LinkedHashMap重用了HashMap的remove方法,然后在afterNodeRemoval方法中删除链表中相应的节点

三、实现LRU算法(leetcode的一道题)

利用LinkedHashMap的数据结构特性,可以简便地实现LRU算法。

class LRUCache {
    private LinkedHashMap<Integer,Integer> data;
    private int capacity;

    public LRUCache(int capacity) {
        this.capacity = capacity;
		//这里要指定第三个参数为true
        data=new LinkedHashMap<>(capacity,1,true);
    }
    
    public int get(int key) {
		//get()方法会自动调整链表
        Integer o = data.get(key);
        return o!=null?o:-1;
    }
    
    public void put(int key, int value) {
		//这里要调用一次get方法,一来可以检查当前key是否存在
		//二来可以调整其在链表位置的位置
        if(data.get(key)==null&&data.size()==capacity){
            data.remove(data.keySet().iterator().next());
        }
        data.put(key,value);
    }
}

这里只展示了实现插入整数,可改进为泛型使其更加通用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值