Java 集合类 源码分析学习 ---- LinkedHashMap 与 LRU

Java 集合类 源码分析学习 ---- LinkedHashMap 与 LRU

秋招准备: 加油啊 兄 dei!!!

这部分的面试题并不多,在问到HashMap的时候,可以做个加分项吧!

自己给自己提问题吧~

  1. LinkedHashMap 了解吗 (回答什么 LinkedHashMap );
  2. 对比 HashMap;
  3. 一些细节;
  4. 实现LRU;
  5. 手写LRU(不能继承LinkedHashMap )。

---------------------------------------------------------- 美丽的分割线 ----------------------------------------------------------


1. LinkedHashMap 原理

浏览一遍 LinkedHashMap 注释 和 源码 …

LinkedHashMap 继承于HashMap(站在巨人的肩膀上),自然拥有了HashMap的所有特性。包括 数组 + 链表/红黑树 的数据结构外,还有 一个独有的特性(敲黑板) ---- 按顺序迭代的特性。可以设置为 按插入的顺序 或者 访问的顺序(LRU)。

   Hash table and 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).  Note that insertion order is not affected
 * if a key is re-inserted into the map.  (A key k is
 * reinserted into a map m if m.put(k, v) is invoked when
 * m.containsKey(k) would return true immediately prior to
 * the invocation.

蹩脚翻译一下:
具有 可预测的迭代顺序Map 接口实现,采用 哈希表 和 链表 实现。此实现与 HashMap 的不同之处在于 它维护了一个贯穿其所有条目的双向链表 。此链接列表定义迭代排序,默认(通常)是键插入映射的顺序(插入顺序)。 注意,如果将键重新插入到Map中,则插入顺序不会受到影响。(如果在m.containsKey时调用 m.put(k,v),则将键k 重新插入到map m 中,会在调用之前立即返回true 。)

来看一下 LinkedHashMap 的真面目,上代码:

继承关系:

public class LinkedHashMap  extends HashMap  implements Map

为什么继承了 HashMap 还要实现 Map 接口:这里再次实现Map接口,可能只是强调,LinkedHashMap是Map的一员,如果不实现也没问题。(传送门:https://stackoverflow.com/questions/3854748/why-do-many-collection-classes-in-java-extend-the-abstract-class-and-implement-t)

成员变量:


    /**
     * 双向链表的头结点(最老).
     */
    transient LinkedHashMap.Entry head;

    /**
     * 双向链表的尾结点(最年轻).
     */
    transient LinkedHashMap.Entry tail;

    /**
     * Map 的迭代顺序: 
     *  true for access-order, false for insertion-order.
     */
    final boolean accessOrder;

讲道理,LiekedHashMap 需要维护双向链表,那么就需要重写 HahsMap 中的 crud 的方法,如 put(K k,V v),get(K k) 等等。但是,但是,但是,并没有!!!看一下 LinkedHashMap 的所有方法:
Java 集合类 源码分析学习 ---- LinkedHashMap 与 LRU_第1张图片
LiekedHashMap 只是重写 以上这些 HashMap 的部分方法,那么 LiekedHashMap 是怎么维护这个双向聊表呢?
(1)双向链表的 创建 和 添加:
    还是从 HashMap 的 put 方法说起,HashMap的put 方法中调用的方法里,newNode 、afterNodeAccess、afterNodeInsertion方法被重写了。撸一下代码:

    Node newNode(int hash, K key, V value, Node e) {
        // 新建一个 Entry 节点
        LinkedHashMap.Entry p =
            new LinkedHashMap.Entry(hash, key, value, e);
        // 添加到 双向链表 的尾部
        linkNodeLast(p);
        return p;
    }
    private void linkNodeLast(LinkedHashMap.Entry p) {
       // 拿到 尾节点
        LinkedHashMap.Entry last = tail;
        // 新添加的节点 作为尾节点(并未添加带链表中)
        tail = p;
        // 双向链表为空 -- 当前节点 作为 头结点
        // 双向链表不为空 -- 尾节点 添加到 链表中
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

双向链表的添加,删除等操作 移步其他博客学习吧~(就是懒的画图哈哈哈 ~~~ )。
到这就大概知道了 LinkedHashMap 的双向链表插入过程:在 put 时通过重写的newNode方法新建节点到链表中

再来看看 afterNodeAccess、afterNodeInsertion 方法:

    /**
     * HashMap 插入函数 的 回调
     * @param evict 是否 删除 最老的元素
     */
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry first;
        // removeEldestEntry(first) 判断 是否满足删除条件,默认 false,留给子类重写
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            // 移除头节点
            removeNode(hash(key), key, null, false, true);
        }
    }
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return false;
    }
    void afterNodeAccess(HashMap.Node e) { // move node to last
        LinkedHashMap.Entry last;
        // 判断 是否是 根据 访问 顺序迭代
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry p =
                    (LinkedHashMap.Entry)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;
        }
    }

总结一下 LinkedHashMap 的 put 过程中 如何维护双向链表:
    a. 如果插入节点 K 不存在,需要新建为新节点,那么调用newNode方法,在新建节点的同时,添加本节点到链表的尾部,成为最新的节点
    b. 如果插入节点的 K 已存在,需要覆盖旧值时, 如果 accessOrder = true(也就按照访问顺序遍历),那么 会将当前节点移动到链表的尾部,成为最新的节点

(2)get 方法,被 LinkedHashMap 重写,没什么特别的,只是在查找到 元素时,如果按访问顺序遍历,就调整一下当前节点 到 链表尾部

    public V get(Object key) {
        Node e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
         // 如果是 按访问顺序,调整当前节点的位置
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

(3)remove 方法:同 put 方法一样,只是重写了 removeNode 方法中的回调 afterNodeRemoval 方法:

    void afterNodeRemoval(Node e) { // unlink
        LinkedHashMap.Entry p =
            (LinkedHashMap.Entry)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;
    }
2. LRU 与 LinkedHashMap 实现LRU

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

还记得 LinkedHashMap 中,accessOrder 的注释:

    /**
     * Map 的迭代顺序: 
     *  true for access-order, false for insertion-order.
     */
    final boolean accessOrder;

当accessOrder = true 的时候,LinkedHashMap 将会按照访问顺序排列,在看 afterNodeInsertion 中的这段代码

		// 满足三个条件: 1允许删除 2.链表不为空 3.removeEldestEntry(first)返回 true
		// 才会删除 链表头部的元素,也就是 删除最老的元素
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }

所以,可以通过 设置 accessOrder = true 和 重写 removeEldestEntry 方法 实现LRU 算法

package Linked;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author: Black
 * @description: LruCache TODO
 * @date: 2019/2/27 19:53
 * @version: v 1.0.0
 */
public class LruCache extends LinkedHashMap {

    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    public static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    private int size;

    public LruCache(int size) {
        // 设置 accessOrder = true
        super(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, true);
        this.size = size;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        // 超过最大缓存空间 就 删除链表头部
        return size() > size;
    }

    @Override
    public String toString() {
        StringBuilder str = new StringBuilder();
        Set> entries = this.entrySet();
        for (Map.Entry entry : entries) {
            str.append(entry.getKey()).append(" ").append(entry.getValue()).append("||");
        }
        return str.toString();
    }

    public static void main(String[] args) {
        LruCache lruCache = new LruCache<>(4);
        lruCache.put(1,"1");
        lruCache.put(2,"2");
        lruCache.put(3,"3");
        lruCache.get(1);
        lruCache.put(4,"4");
        lruCache.put(5,"5");
        lruCache.put(6,"6");
        System.out.println(lruCache);
    }

}

输出:
1 1||4 4||5 5||6 6||

  1. 手写LRU算法

万一 让你来一个手写 LRU …那就 来吧~
这部分就是模仿 LinkedHashMap 的双向链表来实现的。
直接上代码,看注释(懒…)

public class MyLruCache {

    /**
     * 头结点 和 尾节点
     */
    private Node head;
    private Node tail;

    /**
     * 缓存 容量
     */
    private int capacity;
    /**
     * 当前缓存大小
     */
    private int size;

    public MyLruCache(int capacity) {
        this.capacity = capacity;
    }

    public T add(T entity) {
        Node newNode = new Node(entity, null, null);
        linkedLast(newNode);
        T old = null;
        if (size > capacity) {
            old = head.data;
            unlink(head);
        }
        return old;
    }

    public T get(T entity) {
        Node node = getNode(entity);
        if (node != null) {
            move2Last(node);
        }
        return node == null ? null : node.data;
    }

    private Node getNode(T entity) {
        T data;
        for (Node node = head; node != null; node = node.nxt) {
            if (entity == null) {
                if (node.data == null) {
                    return node;
                }
            } else {
                if (entity == (data = node.data) || entity.equals(data)) {
                    return node;
                }
            }
        }
        return null;
    }

    private void move2Last(Node node) {
        assert node != null;
        Node pre = node.pre, nxt = node.nxt;
        node.nxt = node.pre = null;
        if (pre == null) {
            head = nxt;
        } else {
            pre.nxt = nxt;
        }
        if (nxt == null) {
            return;
        } else {
            nxt.pre = pre;
        }
        linkedLast(node);
    }

    private void linkedLast(Node node) {
        assert node != null;
        Node last = tail;
        tail = node;
        if (last == null) {
            head = node;
        } else {
            last.nxt = node;
            node.pre = last;
        }
        size++;
    }

    private void unlink(Node node) {
        assert node != null;
        Node b = node.pre, a = node.nxt;
        node.pre = node.nxt = null;

        if (b == null) {
            head = a;
        } else {
            b.nxt = a;
        }
        if (a == null) {
            tail = b;
        } else {
            a.pre = b;
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (Node node = head; node != null; node = node.nxt) {
            sb.append("<<>> ").append(node.data);
        }
        return sb.toString();
    }

    class Node {

        T data;
        Node pre;
        Node nxt;

        public Node(T data, Node pre, Node nxt) {
            this.data = data;
            this.pre = pre;
            this.nxt = nxt;
        }
    }

    public static void main(String[] args) {
        MyLruCache mLru = new MyLruCache<>(7);
        mLru.add(1);
        mLru.add(2);
        mLru.add(3);
        mLru.add(4);
        mLru.add(5);
        mLru.add(6);
        mLru.add(7);
        mLru.add(8);
        mLru.get(3);
        mLru.get(2);
        mLru.get(6);
        System.out.println(mLru);
    }

}

测试结果:
<<>> 4<<>> 5<<>> 7<<>> 8<<>> 3<<>> 2<<>> 6
没毛病…

  1. 总结

如何回答:你知道 LinkedHashMap 吗

// TODO 巴啦巴拉~~~ 明天继续写
5.

你可能感兴趣的:(java数据结构源码分析)