LinkedHashMap原理(浅谈)

一. 回顾

前面深入了解了HashMap实现原理以及HashMap常见问题,今天简略了解LinkedHashMap原理。

此笔记仅供自己学习完后复习回顾参考,还有很多待提高的地方,如有错误请指正

二. LinkedHashMap

打开IDEA,按两下shift搜索LinkedHashMap,打开源码,可以看到如下:

 * @since   1.4
 */
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{

解释: @since1.4代表LinkedHashMap在jdk1.4才开始有。LinkedHashMap继承了HashMap,实现了Map接口。由此可以看出LinkedHashMap是一个HashMap,推理出其在HashMap的基础上增加了某些东西。

2.1 成员变量

head、tail: 从中文意思可看出这是节点的头尾指针,也就是在HashMap的基础上加了头尾指针。 指针的类型是LinkedHashMap.Entry<K,V>,它是LinkedHashMap的静态内部类,后面再详述。

 /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;

accessOrder: 从中文意思可看出这是访问顺序。这也是在HashMap的基础上加的东西。注释中作出了解释,这是给LinkedHashMap作迭代用的。当值为true的时候,该变量代表访问顺序(后面再用例子详述什么是访问顺序);当值为false的时候,该变量代表插入顺序。(即存键值对进去是什么顺序,迭代打印出来的就是什么顺序)。

/**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

2.2 构造方法

因为LinkedHashMap继承了HashMap,所以两者底层架构并没有很大的差异(同样都是数组+链表+红黑树)。

两者的差别就在:LinkedHashMap的数据节点多了2个指针:头指针head尾指针tail

它有5个构造方法,比HashMap多了1个,其他的四个构造方法和HashMap的都差不多。如下:
在这里插入图片描述


LinkedHashMap(): 无参构造,调用父类HashMap的无参构造(初始容量为16,负载因子为0.75),并且设置accessOrder=false;(即使用插入顺序)

/**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the default initial capacity (16) and load factor (0.75).
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

LinkedHashMap(int initialCapacity): 调用父类HashMap(initialCapacity),指定初始容量,并且设置accessOrder=false;(即使用插入顺序)

 /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the specified initial capacity and a default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity
     * @throws IllegalArgumentException if the initial capacity is negative
     */
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

LinkedHashMap(int initialCapacity, float loadFactor): 调用父类HashMap(initialCapacity, loadFactor),指定初始容量和负载因子,并且设置accessOrder=false;(即使用插入顺序)

 /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the specified initial capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

LinkedHashMap(Map<? extends K, ? extends V> m): 调用父类HashMap的空参构造,并且设置accessOrder=false;(即使用插入顺序),调用putMapEntries()方法将map集合转为LinkedHashMap集合。容量由入参map集合决定

/**
     * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
     * the same mappings as the specified map.  The <tt>LinkedHashMap</tt>
     * instance is created with a default load factor (0.75) and an initial
     * capacity sufficient to hold the mappings in the specified map.
     *
     * @param  m the map whose mappings are to be placed in this map
     * @throws NullPointerException if the specified map is null
     */
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder):
调用父类HashMap的(initialCapacity, loadFactor)并且设置指定的accessOrder(即使用指定的顺序)此三个入参的构造方法在HashMap并没有

/**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

2.3 静态内部类

这是增加了head、tail两个指针的类型,继承了HashMap.Node<K,V>,也就是说拥有HashMap.Node<K,V>的特性。构造方法调用了HashMap.Node<K,V>的Node(int hash, K key, V value, Node<K,V> next)

 /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }

2.4 linkNodeLast()方法

在链表末尾添加节点,如下:

// link at the end of list
    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;
        }
    }

可以看出LinkedHashMap加了head、tail的指针,作用和C语言的链表差不多。

2.5 访问顺序

上面介绍成员变量accessOrder的时候,涉及到访问顺序这个词。


在解释这个知识点之前,先回顾一下LRU知识点

LRU:Least Recently Used,中文是最近最少使用。是一种常用的页面置换算法。在内存不命中的条件下,内存缺页需要将页面置换。详情如下:
在这里插入图片描述


下面用一个测试实例来解释说明。
下面的测试目的是证明:

  1. HashMap插入的顺序与取出的顺序是不一样的,即HashMap是无序的
  2. LinkedHashMap插入的顺序与取出的顺序是一样的,即LinkedHashMap是有序的
  @Test
    public void test1(){
        Map<String, String> linkMap = new LinkedHashMap<String, String>();
        Map<String, String> hashMap = new HashMap<String, String>();

        linkMap.put("2", "2");
        linkMap.put("1", "1");
        linkMap.put("3", "3");

        hashMap.put("2", "2");
        hashMap.put("1", "1");
        hashMap.put("3", "3");

        Set<Map.Entry<String, String>> linkEntries = linkMap.entrySet();
        Set<Map.Entry<String, String>> hashEntries = hashMap.entrySet();

        System.out.print("LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }

        System.out.println();

        System.out.print("HashMap:");
        for (Map.Entry<String, String> entry : hashEntries) {
            System.out.print(" " + entry + " ");
        }
    }

测试结果:
在这里插入图片描述


下面测试开启accessOrder访问顺序(即accessOrder=true)。如下:

@Test
    public void test2() {
        //使用三个入参的构造器来设置accessOrder=true
        Map<String, String> linkMap =
                new LinkedHashMap<String, String>(16, 0.75f, true);

        linkMap.put("2", "2");
        linkMap.put("1", "1");
        linkMap.put("3", "3");

        Set<Map.Entry<String, String>> linkEntries = linkMap.entrySet();//获取键值对集合

        System.out.print("访问前的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }
        System.out.println();

        //访问linkMap
        linkMap.get("2");
        System.out.print("访问2后的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }
        System.out.println();
        
        //访问linkMap
        linkMap.get("1");
        System.out.print("访问1后的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }


    }

测试结果:
在这里插入图片描述
可以看到被访问后的元素,被置顶了。也就是说,LinedHashMap访问某元素,某元素将被置顶。这样的功能应用在LRU缓存方面。 经常访问的元素就可以很快速地找到了,而不被经常访问的元素可以沉到底甚至可以被覆盖,LinkedHahsMap这样的功能使得它在缓存技术方面发扬光大。

总结:从上面的访问顺序,也可以总结出,遍历LinkedHashMap是按照从链表开始扫描,扫到尾部的。get元素后,元素被放到链表的尾部,所以每次get后再遍历LinkedHashMap,get的元素都会出现在末尾。

2.6 LinkedHashMap是如何实现LRU算法?

Mybatis中的Lrucache底层是用LinkedHashMap。我们从源码分析LinkedHashMap怎么可以实现LRU。

因为涉及的是访问顺序,而它又是因get()方法引起的,所以首先从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;
    }

发现与afterNodeAccess(e)有关,点进去看看,如下:

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

看到方法开头就已经有一行注释了// move node to last,意思就是将元素移到链表尾部。

总结:当访问某元素,如果设置了访问顺序accessOrder=true,则该元素被移到链表尾部。

按照这个思路,如果满足当前长度大于某值就移除链头的元素,这样就实现了LRU缓存。

由此可推理当我们put元素进去,LinkedHashMap会判断元素是否已经达到了LRU设定的缓存大小,如果达到就会实行LRU。将很久不被使用的元素移除。因此我们查看LinkedHashMap的put()方法,发现没有这个方法。那说明LinkedHashMap的put方法调用的是父类HashMap的put()方法。打开HashMap找到put()方法,发现有一个afterNodeInsertion(),如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上图中此afterNodeInsertion()方法并没有在HashMap中实现而是在LinkedHashMap实现。

打开LinkedHashMap查看afterNodeInsertion()方法,如下:
在这里插入图片描述
点进去看removeEldestEntry(first)该方法默认是返回false(即默认不实现缓存功能)。如下:

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

总结:综上所述只要我们重写这个方法在某条件下返回true就可以实现LRU缓存。

测试例子:用LinkedHashMap实现LRU缓存

 @Test
    public void test3() {
        Map<String, String> map =
                new LinkedHashMap<String, String>(5, 0.75f, true) {
                    @Override
                    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                        //当前长度>=5,再插入就移除。即达到阈值时就实现LRU
                        return this.size()>=5;
                    }
                };

        map.put("2", "2");
        map.put("1", "1");
        map.put("3", "3");
        map.put("9", "9");

        Set<Map.Entry<String, String>> linkEntries = map.entrySet();//获取键值对集合

        //访问linkMap
        map.get("2");
        System.out.print("访问2后的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }
        System.out.println();

        map.put("5", "5");

        System.out.print("存入5后的LinkedHashMap:");
        for (Map.Entry<String, String> entry : linkEntries) {
            System.out.print(" " + entry + " ");
        }
        System.out.println();
    }

测试结果:
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章