linkedhashmap遍历_【源码面试】轻松解读LinkedHashMap如何实现有序性和LRU

面试题

  1. LinkedHashMap如何实现有序的
  2. 如何用LinkedHashMap实现LRU
f05afef25b9bca96f89758de8cd77444.png

源码解析

LinkedHashMap在Map的基础上进行了扩展,提供了按序访问的能力。这个顺序通过accessOrder控制,可以是结点的插入顺序,也可以是结点的访问时间顺序。

LinkedHashMap还提供了removeEldestEntry方法,可以用来删除最老访问结点。

通过accessOrder和removeEldestEntry可以用来实现LRU缓存。

03aad83718ad6885fd3cfa3b3ce26d38.png

如图所示,LinkedHashMap实现顺序访问的方法比较简单,在HashMap实现之外,还维护了一个双向链表。每当插入结点时,不仅要在Map中维护,还需要在链表中进行维护。HashMap中的put, get等方法都提供了一些钩子方法,如afterNodeAccess、afterNodeInsertion和afterNodeRemoval等。通过这些方法,LinkedHashMap可以对这些结点进行一些特性化的维护。

当遍历LinkedHashMap时通过遍历链表代替遍历Map中的各个槽,从而实现按序访问。

底层数据结构

    /**     * LinkedHashMap普通的链表结点,继承了HashMap的Node,在此基础上     * 对每个Node添加了before和after指针。LinkedHashMap在HashMap的     * 基础上,还维护了一个双向链表,链表中的结点就是Map中的每个结点,     * 通过此链表,LinkedHashMap就实现了维护结点顺序的目的     */    static class Entry extends HashMap.Node {        Entry before, after;        Entry(int hash, K key, V value, Node next) {            super(hash, key, value, next);        }    }    /**     * 双向链表的头结点     */    transient LinkedHashMap.Entry head;    /**     * 双向链表的尾结点     */    transient LinkedHashMap.Entry tail;    /**     * true-按访问顺序(最早操作过的结点靠前)     * false-按插入顺序遍历(最早插入的结点靠前)     *     * @serial     */    final boolean accessOrder;

Node结点

    Node newNode(int hash, K key, V value, Node e) {        LinkedHashMap.Entry p =                new LinkedHashMap.Entry(hash, key, value, e);        // 创建一个key-value对时,不仅要放入map中,还有放入LinkedHashMap        // 内置的双向链表中,用来维护插入顺序        linkNodeLast(p);        return p;    }    Node replacementNode(Node p, Node next) {        LinkedHashMap.Entry q = (LinkedHashMap.Entry) p;        LinkedHashMap.Entry t =                new LinkedHashMap.Entry(q.hash, q.key, q.value, next);        // 用t结点代替q结点在双向链表中的位置        transferLinks(q, t);        return t;    }    TreeNode newTreeNode(int hash, K key, V value, Node next) {        TreeNode p = new TreeNode(hash, key, value, next);        linkNodeLast(p);        return p;    }    TreeNode replacementTreeNode(Node p, Node next) {        LinkedHashMap.Entry q = (LinkedHashMap.Entry) p;        TreeNode t = new TreeNode(q.hash, q.key, q.value, next);        transferLinks(q, t);        return t;    }

工具方法

    // 在双向链表尾部添加结点    private void linkNodeLast(LinkedHashMap.Entry p) {        LinkedHashMap.Entry last = tail;        tail = p;        if (last == null)            head = p;        else {            p.before = last;            last.after = p;        }    }    // 使用dst结点覆盖src结点在双向链表中的位置    private void transferLinks(LinkedHashMap.Entry src,                               LinkedHashMap.Entry dst) {        LinkedHashMap.Entry b = dst.before = src.before;        LinkedHashMap.Entry a = dst.after = src.after;        if (b == null)            head = dst;        else            b.after = dst;        if (a == null)            tail = dst;        else            a.before = dst;    }        /**     * 每次插入新Node时,是否需要删除最老的结点。     *     * @return true-删除最老结点,false-不删除     */    protected boolean removeEldestEntry(Map.Entry eldest) {        return false;    }

构造方法

    public LinkedHashMap(int initialCapacity, float loadFactor) {        super(initialCapacity, loadFactor);        accessOrder = false;    }    public LinkedHashMap(int initialCapacity) {        super(initialCapacity);        accessOrder = false;    }    public LinkedHashMap() {        super();        accessOrder = false;    }    public LinkedHashMap(Map extends K, ? extends V> m) {        super();        accessOrder = false;        putMapEntries(m, false);    }    /**     * 可以指定遍历结点的顺序     *     * @param accessOrder true-按访问顺序(最早操作过的结点靠前)     *                    false-按插入顺序遍历(最早插入的结点靠前)     */    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {        super(initialCapacity, loadFactor);        this.accessOrder = accessOrder;    }

钩子方法

    // 重写HashMap中提供给LinkedHashMap的钩子方法    /**     * HashMap 调用remove方法后,会调用这个钩子方法,e为删除的结点     */    void afterNodeRemoval(Node e) {        //  p = e; b = p.before; a = p.after;        LinkedHashMap.Entry p =                (LinkedHashMap.Entry) e, b = p.before, a = p.after;        // 从双向链表中删除p结点        p.before = p.after = null;        if (b == null)            head = a;        else            b.after = a;        if (a == null)            tail = b;        else            a.before = b;    }    /**     * HashMap 调用put等方法后,会调用这个钩子方法     *     * @param evict false-table处于创建模式(即通过构造方法调用)     */    void afterNodeInsertion(boolean evict) {        LinkedHashMap.Entry first;        // 如果map中存在元素,且需要删除eldest元素,则从链表和Map中        // 删除双向链表头结点。removeEldestEntry在LinkedHashMap默认返回        // false。该方法可以用来实现LRU缓存        if (evict && (first = head) != null && removeEldestEntry(first)) {            K key = first.key;            removeNode(hash(key), key, null, false, true);        }    }    /**     * HashMap 调用put, get等方法后,会调用这个钩子方法,更改最新访问时间。     * 可以用来实现LRU缓存     *     * @param e 最近操作过的结点     */    void afterNodeAccess(Node e) {        LinkedHashMap.Entry last;        // 如果accessOrder为true,代表按最新遍历时间维护链表        // 则将e移至链表尾部        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;        }    }

其他

    public boolean containsValue(Object value) {        // 因为LinkedHashMap中使用双向链表维护了所有Node,所以只需要遍历        // 双向链表即可遍历所有Node。而不用遍历Map。        for (LinkedHashMap.Entry e = head; e != null; e = e.after) {            V v = e.value;            if (v == value || (value != null && value.equals(v)))                return true;        }        return false;    }    public V get(Object key) {        Node e;        // 寻找key对应结点        if ((e = getNode(hash(key), key)) == null)            return null;        // 如果需要按访问时间排序,则更新结点在双向链表中的位置        if (accessOrder)            afterNodeAccess(e);        return e.value;    }    public V getOrDefault(Object key, V defaultValue) {        Node e;        if ((e = getNode(hash(key), key)) == null)            return defaultValue;        if (accessOrder)            afterNodeAccess(e);        return e.value;    }    public void clear() {        super.clear();        head = tail = null;    }    public void forEach(BiConsumer super K, ? super V> action) {        if (action == null)            throw new NullPointerException();        int mc = modCount;        // 覆写了遍历方法,用遍历双向链表代替遍历map,从而实现了按序遍历。        for (LinkedHashMap.Entry e = head; e != null; e = e.after)            action.accept(e.key, e.value);        if (modCount != mc)            throw new ConcurrentModificationException();    }

迭代器

    abstract class LinkedHashIterator {        // 下一个要遍历的结点        LinkedHashMap.Entry next;        // 上一个遍历过的结点        LinkedHashMap.Entry current;        // 版本号        int expectedModCount;        LinkedHashIterator() {            next = head;            expectedModCount = modCount;            current = null;        }        public final boolean hasNext() {            return next != null;        }        final LinkedHashMap.Entry nextNode() {            LinkedHashMap.Entry e = next;            if (modCount != expectedModCount)                throw new ConcurrentModificationException();            if (e == null)                throw new NoSuchElementException();            current = e;            // 遍历双向链表的下一个结点            next = e.after;            return e;        }        public final void remove() {            Node p = current;            if (p == null)                throw new IllegalStateException();            if (modCount != expectedModCount)                throw new ConcurrentModificationException();            current = null;            K key = p.key;            removeNode(hash(key), key, null, false, false);            expectedModCount = modCount;        }    }

面试题解答

LinkedHashMap如何实现有序的 LinkedHashMap在HashMap的基础上,还将每个key-value对应的Node维护在了一个额外的双向链表中。

LinkedHashMap通过accessOrder可以支持按插入的顺序访问,或者按遍历的顺序访问 accessOrder false: 按插入顺序排序,map中每插入一个结点时,将这个结点同时放置在双向链表的结尾 true: 按访问顺序排序,当操作map中的一个结点时,通过HashMap提供的钩子方法(afterNodeAccess、afterNodeInsertion和afterNodeRemoval)找到这个结点在链表中的位置,并移动到链表结尾。

这样链表的头结点就是链表最久没有访问过的结点 遍历的时候,通过便利双向链表代替遍历map的每个槽,来实现顺序访问。

如何用LinkedHashMap实现LRU 首先分析LRU算法有哪些特性 新数据插入到链表尾部(代表最新访问); 每当缓存命中(即缓存数据被访问)则将数据移到链表尾部(代表最新访问);

当链表满的时候,将链表头部的数据丢弃(删除最久未访问结点); 在LinkedHashMap保证结点有序的情况下,通过设置accessOrder为true,采用按遍历顺序维护结点。

put方法将结点插入到双向链表尾部实现LRU特性 1; 钩子方法afterNodeAccess实现LRU特性 2; 实现removeEldestEntry方法,删除最久未访问结点。实现LRU特性 3;

作者:LLLZH
链接:https://juejin.im/post/5eae84daf265da7bf7328e25

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值