LinkHashMap分析,Link体现在哪里?如何实现

210 阅读6分钟

linkHashMap分析

1. link体现在哪里?

LinkHashmap继承与HashMap。重写了其中的几个方法。并且在hashMap的基础上维护了一个双向列表。用于连接所有的实体,链表连接的顺序通常来说是插入的顺序,注意这里说的是通常,也是可以改变的。简单来说,LinkHashmap维护了一个链表,用于保持节点的顺序(插入或者查找)

从源码角度来看

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
  
 
    /**
     * 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;

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

在LinkHashMap中。新增了几个属性

  • LinkedHashMap.Entry<K,V> head:链表的头节点

  • LinkedHashMap.Entry<K,V> tail:链表的尾节点

  • boolean accessOrder:链表连接的顺序

    说明

    在构建列表的时候,是按照插入顺序来构建的,并且利用accessOrder是可以实现LRU的功能。其实他自己来实现了这样的功能

    • true:访问模式,意思就是在访问的时候,会将这次访问的元素移动到链表的的末尾。
    • false:插入模式,在插入的时候会将这个元素连接到链表的末尾。但是访问的时候并不会。
链表什么时候链接?链表怎么链接?

在put方法里面newNode的时候链接链表的。并且通过尾插法

LinkHashMap重写了HashMap的newNode方法,自己写了一个内部类Entry,在这里就链接链表。这样就能在之前HashMap的基础上,通过一个链表来维护插入的顺序,同样的,在之后的循环操作肯定也是通过链表来循环的。

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

// LinkedHashMap.Entry
   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);
        }
    }
// 链表的连接在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;
        }
    }

2. 和HashMap不同的地方在哪里?

重写了几个拓展的方法,并且存放的Node的类型不一样,还有迭代器相关的部分。

  1. 支持顺序

  2. 重写了newNode方法,创建了LinkedHashMap.Entry对象。这个对象在上面已经介绍了。

  3. 重写了下面三个重要的拓展方法

    void afterNodeAccess(Node<K,V> p) { } //访问节点
    void afterNodeInsertion(boolean evict) { } //插入节点
    void afterNodeRemoval(Node<K,V> p) { } //移除节点
    

    通过这三个方法并且结合双向链表。能实现很多有意思的东西。链表的移动的时间复杂度是o(1)。

  4. 支持在插入和访问的时候做拓展。

  5. 不同于HashMap的迭代器和EntrySet

3. accessOrder分析

accessOrder为true,访问顺序具体是怎么做的,实现的样子是什么。

先从列子开始

   @Test
    public void testLinkHashMap(){
        LinkedHashMap<String, String> hashMap = new LinkedHashMap<>(16,0.75F,true);
        hashMap.put("b","b");
        hashMap.put("a","a");
        hashMap.put("c","c");
        System.out.println(hashMap);
        hashMap.get("a");
        System.out.println(hashMap);
    }

结果:

{b=b, a=a, c=c} {b=b, c=c, a=a}

可以看到,设置为true之后,在访问的时候会将当前访问的节点移动到链表的末尾。这样一来,链表的头就是最老的元素了(没有访问过得),要实现LRU的话,直接移除链表的第一个节点就好了。前提是链表的长度不是无限的,否则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方法可以继
afterNodeAccess
//这里的代码逻辑也很简单, 就是对双向链表的操作。
void afterNodeAccess(Node<K,V> e) { // move node to last 
        LinkedHashMap.Entry<K,V> last;
        //首先,当前这个节点不是末尾节点。
        if (accessOrder && (last = tail) != e) {
          //p就是e,b是p.before,a是p.after;
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
          // 没有前继节点,那只能说明是当前的这个节点是头节点
            if (b == null)
               //直接将头结点赋值为a。
                head = a;
            else
               //不是头结点,就正常连。
                b.after = a;
          
           //a后面不是null
            if (a != null)
               //a的前一个就是b
                a.before = b;
            else //a==null。
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
          
          
           // 将p赋值给尾节点。
            tail = p;
          
          //这是快速失败的关键。记录真实修改数。
            ++modCount;
        }
    }

就是将p移动到尾节点, 在这个中间对几种情况进行判断,比如头结点,尾节点。

accessOrder为false,访问顺序具体是怎么做的,实现的样子是什么。

先从列子开始

 @Test
    public void testLinkHashMap(){
        LinkedHashMap<String, String> hashMap = new LinkedHashMap<>(16,0.75F);
        hashMap.put("b","b");
        hashMap.put("a","a");
        hashMap.put("c","c");
        System.out.println(hashMap);
        hashMap.get("a");
        System.out.println(hashMap);
        hashMap.remove("a");
        System.out.println(hashMap);
        hashMap.put("a","a");
        System.out.println(hashMap);
    }

结果

{b=b, a=a, c=c} {b=b, a=a, c=c} {b=b, c=c} {b=b, c=c, a=a}

从put方法进去在看看,其实就是上面说的构建链表的地方,newNode的时候构建的,因为accesOrder是false,所以,在get的时候不会走afterNodeAccess,但是这里我还得在说一下afterNodeInsertion方法

//evict的为true,这个值是从putVal方法里面传递进来的
//在removeEldestEntry这个方法是干嘛的?可以继续看看,如果说evict为true,并且head不是null,并且removeEldestEntry为true的话,就会移除头节点。这个听起来就是LRU的实现。
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);
        }
    }

在linkHashMap里面默认返回为false。那如果重写了这个方法,然后基于某种条件返回true,比如,元素的数量不能超过100,超过的话,就会移除头节点。配合accessOrder为true,在使用一次之后就会将节点移动到链表末尾。这不就实现了LRU了吗?建议看看removeEldestEntry方法上面的注释。说的很明确。具体的列子看看往下看,在第四节里面会说。

至于afterNodeRemoval方法,这方法里面干的事情就是在元素从table中移除之后,这个元素也得从链表里面移除。

//看人家这注释,很明确unlink。
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. linkHashMap实现LRU

说明:

这样的例子多的是,可以查看LinkHashMap类关系图,看看。下面的这个是我自己的demo

   class MyLruCache<K,V> extends LinkedHashMap<K,V>{
        public MyLruCache(){
            super(16,0.75F,true);
        }

       @Override
       protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
           return this.size()>3;
       }
   }

 @Test
   public void testLruCache(){
       MyLruCache<String, String> lruCache = new MyLruCache<>();
       lruCache.put("b","b");
       lruCache.put("a","a");
       lruCache.put("c","c");
       System.out.println("初始化:" + lruCache);
       lruCache.get("a");
       System.out.println("获取a之后:" + lruCache);
       lruCache.put("d","d");
       System.out.println("put d之后" + lruCache);
   }
  

初始化:{b=b, a=a, c=c} 获取a之后:{b=b, c=c, a=a} put d之后{c=c, a=a, d=d}

可以看到,获取a之后,a放在了末尾,放了d之后,b为队首。b被移除。

发现了一个好玩的事情

   @Test
   public void testLinkHashMap骚操作(){
       LinkedHashMap<String, String> hashMap = new LinkedHashMap<>(16,0.75F,true);
       hashMap.put("b","b");
       hashMap.put("a","a");
       hashMap.put("c","c");
       System.out.println(hashMap);
       hashMap.put("a","a1");
       System.out.println(hashMap);
   }

答案:

{b=b, a=a, c=c} {b=b, a=a1, c=c}

如果accessOrder为true。在key重复的时候,并且onlyIfAbsent为false。那么在key相同的时候的前提下, onlyIfAbsent为false或者key对应的value为null,就会覆盖原来的值。会调用afterNodeAccess(e);方法。就会将当前访问的key放在列表的末尾。

  if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

关于LinkHashMap就分析到这里了。如有不正确的地方,欢迎指出。谢谢。