Java集合之LinkedHashMap

1. 简介

      LinkedHashMap内部维护了一个双向链表,能保证元素按插入的顺序访问,也能以访问顺序访问所以可以用来实现LRU缓存策略

2. 继承关系

在这里插入图片描述
      通过继承关系,我们可以看到继承了 HashMap,所以其拥有 HashMap的所有特性,并且额外增加了按顺序访问的特性。

3. 存储结构

在这里插入图片描述
      添加删除元素的时候需要同时维护在HashMap中的存储,也要维护在LinkedList中的存储,所以性能上来说会比HashMap稍慢。

4. 深入源码

4.1 属性

    /**
     * 指向双向链表的头节点指针
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * 指向双向链表的尾节点指针
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /**
     * true:双向链表按访问顺序排序
     * false:双向链表按插入顺序访问
     */
    final boolean accessOrder;

      (1)head

      双向链表的头节点,旧数据存在头节点

      (2)tail

      双向链表的尾节点,新数据存在尾节点

      (3)accessOrder

      是否需要按访问顺序排序,如果为false则按插入顺序存储元素,如果是true则按访问顺序存储元素。

4.2 内部类

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

	static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
   }

      可以看到 LinkedHashMap 不仅仅采用了 数组 + 链表 + 红黑树的结构,节点和节点之间还通过两个指针链接成了双向链表。

4.3 构造方法

	// 构造一个空的按插入顺序排序的 LinkedHashMap 实例,其默认初始容量(16)和负载因子(0.75)。
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

	// 构造一个空的按插入顺序排序的 LinkedHashMap 实例,初始容量为 initialCapacity 的二次幂取整的值 和负载因子(0.75)。
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

	// 指定 initialCapacity 和 加载因子
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
    
	// 该构造方法accessOrder从构造方法参数传入,如果传入true,则就实现了按访问顺序存储元素,这也是实现LRU缓存策略的关键。
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

      前四个构造方法accessOrder都等于false,说明双向链表是按插入顺序存储元素。

      最后一个构造方法accessOrder从构造方法参数传入,如果传入true,则就实现了按访问顺序存储元素,这也是实现LRU缓存策略的关键。

在这里插入图片描述

4.4 添加及删除元素

      通过观察LinkedHashMap,可以看到其并没有putremove方法,说明其调用的是父类 HashMap的方法,那么其是怎么实现按插入顺序或访问顺序访问的特性的呢

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
       // 代码省略
           
        afterNodeAccess(e);
   		// 代码省略
        afterNodeInsertion(evict);
        return null;
    }
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        // 代码省略
        afterNodeRemoval(node);
        return null;
    }
	// 该方法位于 LinkedHashMap 中
    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;
    }

      我们直到在 afterNodeInsertionafterNodeAccessafterNodeRemoval 这三个方法中为空实现,而 LinkedHashMap中却实现了该方法,所以这便是LinkedHashMap 特性的决定方法。

4.4.1 afterNodeInsertion

      该方法在节点被插入之后调用该方法

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        // 如果evict为true,且头节点不为空,且确定移除最老的元素,那么就调用HashMap.removeNode()把双向链表的头节点移除
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

(1)如果evicttrue,且头节点不为空,且确定移除最老的元素,那么就调用HashMap.removeNode()把头节点移除(这里的头节点是双向链表的头节点,而不是某个桶中的第一个元素);

(2)HashMap.removeNode()HashMap中把这个节点移除之后,会调用afterNodeRemoval()方法;

(3)afterNodeRemoval()方法在LinkedHashMap中也有实现,用来在移除元素后修改双向链表,见下文;

(4)默认removeEldestEntry()方法返回false,也就是不删除元素。

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

	// 这里调用的是 HashMap 中的方法删除,已在 HashMap 中分析过
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                // 调用删除节点后的方法
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

4.4.2 afterNodeRemoval

      该方法在节点被删除之后调用该方法。

	// e 为被删除的节点
	// 实现便是在双向链表中删除一个节点的实现
    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;
    }

      经典的把节点从双向链表中删除的方法。

4.4.3 afterNodeAccess

      该方法在节点被访问之后调用该方法。

      在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,如果accessOrdertrue,调用这个方法把访问到的节点移动到双向链表的末尾。

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 如果accessOrder为true,并且访问的节点不是尾节点
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
             // 把p节点从双向链表中移除    
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            // 把p节点放到双向链表的末尾    
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

(1)如果accessOrder为true,并且访问的节点不是尾节点;

(2)从双向链表中移除访问的节点;

(3)把访问的节点加到双向链表的末尾;(末尾为最新访问的元素)

示例一
public class Test {

    public static void main(String[] args) {
        LinkedHashMap linkedHashMap = new LinkedHashMap(16,0.75f,true);
        linkedHashMap.put("1","1");
        linkedHashMap.put("2","2");
        linkedHashMap.put("3","3");

        Set set = linkedHashMap.entrySet();
        System.out.println(set);

        linkedHashMap.get("1");
        set = linkedHashMap.entrySet();
        System.out.println(set);

        linkedHashMap.get("2");
        set = linkedHashMap.entrySet();
        System.out.println(set);
    }

}

      我们在构造方法中指定的是true,那么便说明我们采用的是按照访问顺序来访问对链表进行访问。

      输出如下:
在这里插入图片描述
      可以明显看到,我们访问的元素被添加到了双向链表的末尾。

示例二
public class Test {

    public static void main(String[] args) {
        LinkedHashMap linkedHashMap = new LinkedHashMap(16,0.75f,false);
        linkedHashMap.put("1","1");
        linkedHashMap.put("2","2");
        linkedHashMap.put("3","3");

        Set set = linkedHashMap.entrySet();
        System.out.println(set);

        linkedHashMap.get("1");
        set = linkedHashMap.entrySet();
        System.out.println(set);

        linkedHashMap.get("2");
        set = linkedHashMap.entrySet();
        System.out.println(set);
    }

}

      如果我们按照访问插入顺序来访问链表还是上面的输出吗?

      输出如下:
在这里插入图片描述
      可以明显看到输出两者不同,符合我们对该方法的分析。

4.5 获取元素

4.5.1 get(Object key)

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

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

      如果查找到了元素,且accessOrdertrue,则调用afterNodeAccess()方法把访问的节点移到双向链表的末尾。

(1)LinkedHashMap继承自HashMap,具有HashMap的所有特性;

(2)LinkedHashMap内部维护了一个双向链表存储所有的元素;

(3)如果accessOrderfalse,则可以按插入元素的顺序遍历元素;

(4)如果accessOrder为true,则可以按访问元素的顺序遍历元素;

(5)LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;

(6)默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;

(7)LinkedHashMap可以用来实现LRU缓存淘汰策略;

4.6 LRU 实现

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

      所以我们可以这样做,刚被访问过的元素将其放到链表尾(链表头),刚被添加的元素也被放入到链表尾(链表头),而当缓存大小满了之后,则删除链表头(链表尾)的元素,这样便实现了LRU。

      在LinkedHashMap中,如果我们访问了一个元素,则一定会调用afterNodeAccess方法,而该方法将链表中的节点放入到了链表尾,而在添加元素时,同样调用了该方法,所以我们只需要在缓存满时删除表头的元素即可。回到删除元素的方法afterNodeInsertion,在该方法中调用了removeEldestEntry方法,默认返回false,所以我们只要重写该方法即可。在缓存满时删除表头节点。

/**
 * @author wangzhao
 * @date 2019/12/7 16:26
 */
public class LRUCache extends LinkedHashMap {

    // 缓存的最大容量
    private  int capacity;

    public LRUCache(int capacity){
        // 注意,这里一定要将 accessOrder 指定为 true,只有其为true,afterNodeAccess 方法才能生效
        super(capacity,0.75f,true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > this.capacity;
    }

    public static void main(String[] args) {
        LRUCache lru = new LRUCache(3);
        lru.put("1","1");
        lru.put("2","2");
        lru.put("3","3");

        System.out.println(lru.entrySet());
        // 输出: [1=1, 2=2, 3=3]
        
        lru.get("1");
        System.out.println(lru.entrySet());
        // 输出: [2=2, 3=3, 1=1]

        lru.get("2");
        System.out.println(lru.entrySet());
		// 输出: [3=3, 1=1, 2=2]

        lru.get("3");
        System.out.println(lru.entrySet());
		// 输出: [1=1, 2=2, 3=3]

        lru.put("4","4");
        System.out.println(lru.entrySet());
        // 输出:[2=2, 3=3, 4=4]
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值