美文网首页
LinkedHashMap源码研究

LinkedHashMap源码研究

作者: 涂豪_OP | 来源:发表于2018-08-03 13:25 被阅读10次

一:概述
    上一系列文章中介绍了HashMap的一系列原理和方法,本文介绍下LinkedHashMap。LinkedHashMap继承自HashMap,所以HashMap的特性,LinkedHashMap基本上都有;LinkedHashMap还对HashMap做了一些扩展。
    在已经存在HashMap的基础上为什么还要引入LinkedHashMap呢?这是因为HashMap的遍历顺序和插入顺序不一致,比如你按顺序插入三个节点:Node1,Node2,Node3,那么遍历HashMap的时候遍历的先后结果可能是Node2,Node1,Node3;有时候需要保证插入顺序和遍历顺序保持一致,这时候HashMap就无能为力了,这就是LinkedHashMap的作用,LinkedHashMap是通过一个双向链表维护这种关系的,从网上拷了一张图来加深理解:


LinkedHashMap结构

在撸源码之前,先看一个demo,了解下HashMap和LinkedHashmap的具体区别:

Map<String , String> hashMap = new HashMap<String, String>();
        
//存入键值对
hashMap.put("覆盖沙发垫", "a");
hashMap.put("放松放松", "b");
hashMap.put("方式发顺丰", "c");
hashMap.put("各个", "d");

//遍历
for(Iterator<String> it = hashMap.values().iterator();it.hasNext();) {
    String value = it.next();
    System.out.println(value);
}

    输出结果如下:

b
a
c
d

    可以看到,HashMap的访问顺序和插入顺序不一定是一样的,这取决于各个元素的key的hash的大小。
    再来看看LinkedHashMap:

//创建LinkedHashMap,前面两个是LinkedHashMap的容量和加载因子;注意最后一
//个参数这个参数是accessOrder,他代表访问LinkedHashMap里面的元素的顺序,
//如果是false,那么按照访问的顺序排序;如果为true,那么按照插入的顺序排序
Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,false);
//按1 ---> a,2 ---> b,3 ---> c,4 ---> d的顺序插入;
map.put("1", "a");
map.put("2", "b");
map.put("3", "c");
map.put("4", "d");
for(Iterator<String> it = map.values().iterator();it.hasNext();) {
    String name = (String)it.next();
    System.out.println(name);
}

    输出结果如下:

a
b
c
d

    可以看到,LinkedHashMap的访问顺序和插入顺序一毛一样;再次修改代码:

//访问
map.get("1");
        
//更新
map.put("2", "tuhao");

    输出结果如下:

a
tuhao
c
d

    可以看到,只要accessOrder为false,那么LinkedHashMap的访问顺序就是插入顺序,不管怎么更新或者获取LinkedHashMap的数据,都是这样;再来看个例子:

//跟上面的例子相比,仅仅是accessOrder从false改为true,其他不变
Map<String, String> map1 = new LinkedHashMap<String, String>(16,0.75f,true);
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
map1.put("4", "d");
for(Iterator<String> it = map1.values().iterator();it.hasNext();) {
    String name = (String)it.next();
    System.out.println(name);
}

    输出结果如下:

a
b
c
d

    可以看到,如果是插入全新的元素,那么不管accessOrder是true还是false,访问顺序都是插入顺序,再添加代码如下:

map1.get("1");
map1.put("2", "tuhao");
map1.put("5", "e");
        
for(Iterator<String> it = map1.values().iterator();it.hasNext();) {
    String name = (String)it.next();
    System.out.println(name);
}

    输出结果如下:

c
d
a
tuhao

    可以看到,先访问了1,仅仅是访问了一下,1对应的值a就跑到4对应的值d后面去了;说明如果accessOrder为true,只要访问了LinkedHashMap中的数据,那么这个键值对就跑到LinkedHashMap的尾部了;调用put去更新LinkedHashMap中的数据也会导致更新的键值对跑到LinkedHashMap的尾部,所以LinkedHashMap在put和get的时候,肯定有将目标节点移到链表尾部的过程。
    综上可知,LinkedHashMap的访问顺序是和插入/更新的顺序相关的,而HashMap不具备这种特性;LinkedHashMap这种特性在追求有序的场景,比如缓存的地方能派上用场;至于是什么顺序就取决于accessOrder的值了。
    看完HashMap和LinkedHashMap的区别,下面就撸一把LinkedHashMap的源码,探究其中的原理:

//LinkedHashMap继承自HashMap,他有数组,有链表,也有红黑树
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     * LinkedHashMap的节点类Entry继承自HashMap的Node类,在Node的基
     * 础上新增了两个成员变量before和after,这是为了实现双向链表而增加的
     */
    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);
        }
    }
}

    LinkedHashMap的节点和HashMap的节点是不一样的,因为LinkedHashMap需要维护一个双线链表,必须具备前驱和后继两个节点,HashMap的节点Node显然胜任不了这样的工作;需要注意的是,红黑树节点TreeNode继承自LinkedHashMap的节点类Entry:

/**
 * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
 * extends Node) so can be used as extension of either regular or
 * linked node.
 */
 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
     TreeNode<K,V> parent;  // red-black tree links
     TreeNode<K,V> left;
     TreeNode<K,V> right;
     TreeNode<K,V> prev;    // needed to unlink next upon deletion
     boolean red; 
     ......
}

    红黑树节点TreeNode继承自LinkedHashMap的节点类Entry,使得在LinkedHashMap中使用红黑树的时候,也能维护双向链表的结构。

二:插入
    虽然LinkedHashMap和HashMap的结构有所差异,但是LinkedHashMap的put方法根HashMap一样,LinkedHashMap并没有重写他,差异在创建节点的时候,LinkedHashMap重写了创建节点的方法newNode:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //前面的逻辑一样的
    ......

    else {
        for (int binCount = 0; ; ++binCount) {
            if ((e = p.next) == null) {

                //创建全新的节点并插入节点的尾部;注意这个方法,LinkedHashMap对他进行了重写
                p.next = newNode(hash, key, value, null);
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key

            //获取老的值
            V oldValue = e.value;

            //onlyIfAbsent是传进来的,默认是false;所以进入if
            if (!onlyIfAbsent || oldValue == null)

                //简单的赋值
                e.value = value;

            //afterNodeAccess在HashMap中是一个空实现,
            //在研究HashMap的时候,这个方法没有分析
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //修改次数+1,HashMap是线程不安全的,这玩意就是为
    //了解决迭代的时候修改HashMap造成的异常,后面分析
    ++modCount;

    //如果HashMap的元素个数超过阈值,就调用resize()进行扩容
    if (++size > threshold)
        resize();

    //在研究HashMap的时候,这个方法没有分析
    afterNodeInsertion(evict);
    return null;
}

    LinkedHashMap和HashMap的putVal方法基本一致,除了几个差异,我们来看下这几个差异:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    //创建LinkedHashMap的节点类Entry
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);

    //调用linkNodeLast,从名字都可以看出,是把刚创建的节点插入到链表尾部
    linkNodeLast(p);

    //返回刚才创建的节点
    return p;
 }

    LinkedHashMap和HashMap的newNode方法除了创建的节点不完全一样外,还多了一个linkNodeLast的调用:

// link at the end of list
//将目标节点插入链表尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {

    //tail是LinkedHashMap的成员变量,代表双向链表的尾节点
    LinkedHashMap.Entry<K,V> last = tail;

    //首先将传过来的目标节点赋值给尾节点
    tail = p;

    //如果尾节点为空,说明双向链表还不存
    //在,那么传过来的目标节点当做头节点
    if (last == null)
        head = p;

    //如果尾节点不为空,说明链表已经存在
    else {
        //p已经是尾节点了,那么将p的前驱节点指向原来的尾节点
        p.before = last;

        //将原来的尾节点的后继节点指向现在的尾节点p
        last.after = p;
    }
}

    可以看到,LinkedHashMap在创建节点后,把节点插入了双向链表的尾部,并维护了前驱和后继节点的正确指向。在putVal方法里面有个判断if (e != null) ;这个判断是什么意思呢?put一个元素的时候,可能HashMap里面没有这个元素,那么就插入一个全新的节点;也有可能这个元素已经存在,那么就更新对应节点;是插入还是更新,就看e是不是空了;如果e不为空,那么说明是更新节点,跟新后会把这个节点返回;如果e为空,说明是插入一个全新的节点,新插入节点是不会把插入的节点返回回来的;一个节点更新了,也要移到双向链表的尾部,这就是afterNodeAccess的作用;下面分析afterNodeAccess:

    //将更新的节点移到链表的尾部(如果是按访问节点的顺序来排序节点的话)
    void afterNodeAccess(Node<K,V> e) { // move node to last

        //申明一个中间变量,代表尾节点
        LinkedHashMap.Entry<K,V> last;

        //accessOrder是一个重要的变量,他代表节点的排序方式;如果它为true,说明
        //按访问的顺序来排序(更新也是一种访问),最近一个访问(包括更新)的节点将被放
        //到链表尾部;下次访问的时候,这个节点将被最后访问到;如果他为false,那么访
        //问的顺序将是插入的顺序,每次访问(包括)节点后,节点之间的顺序不变;

        //如果尾节点不是传进来的节点,而且是按访问节点的顺序来排序
        if (accessOrder && (last = tail) != e) {

            //首先将传进来的节点赋值给p;其次将p的前驱和后继节点赋值给b、a;
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;

            //传进来的节点p(e赋值给他的)将是尾节点,所以首先将后继节点置空
            p.after = null;

            //如果p的前驱节点是空,那么将p的后继节点置为头结点。注意哦,进入这个
            //方法的前提是目标节点e(p)已经存在于链表中了,然后我们刚刚还访问了他
            //所以要将他要移到尾节点;既然这个节点已经存在于链表中了,那么他肯定有
            //前驱或者后继节点;如果他的前驱节点是空,说明我们刚才访问的是头结点,
            //要把头结点移到尾节点去,所以只能把原来的头结点的后继节点置为新的头结点
            if (b == null)
                head = a;

            //如果刚刚访问的节点p有前驱节点,那么将p的前驱
            //节点的后继节点指向p自己的后继节点,这没毛病
            else
                b.after = a;

            //如果刚刚访问的节点p的后继节点不为空,说明p不是尾节点
            if (a != null)

                //那么需要将p的后继节点的前驱节点置为p的前驱节点,没毛病
                a.before = b;
            else
                //如果刚刚访问的节点的后继节点为空,说明我们访问的节点
                //就是尾节点,这个时候将访问的节点的前驱节点赋值给last
                //last就指向了倒数第二个节点
                last = b;

            //如果last为空(if里面就对last进行了赋值),说明尾节点
            //是空,说明链表不存在,这时候将传进来的节点置为头结点
            if (last == null)
                head = p;

            //last不为null(他既可能指向尾节点,也可能指向倒数第二个节点)
            else {
                //将刚才访问的节点的前驱节点指向尾节点
                p.before = last;

                //将原来尾节点的后继节点指向刚才访问的节点,这样last就不是尾节点了
                last.after = p;
            }

            //将刚才访问的节点p置为尾节点,没毛病
            tail = p;

            //modCount自增,没毛病
            ++modCount;
        }
    }

    上面就是当accessOrder为true时,目标节点移到链表尾部的过程;下面再来看下HashMap的putVal中的最后一个方法afterNodeInsertion,这个方法在HashMap里面是个空实现,但是在LinkedHashMap实现了,也就说只有往LinkedHashMap里面put元素的时候,这个方法才会起作用,我们来看下到底是什么作用:

//可能需要删除最老的元素
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
      //removeEldestEntry的返回值写死了false,所以不会进入if
      if (evict && (first = head) != null && removeEldestEntry(first)) {
          K key = first.key;
          removeNode(hash(key), key, null, false, true);
      }
  }

    上面的代码初看会有点奇怪,为什么removeEldestEntry写死了返回false呢?这是java留给我们让我们根据具体情况来决定是不是要重写。上面说过,LinkedHashMap适合用于缓存,但是缓存也是有上限的,达到上限后,就应该把最老的,没人访问的元素删掉,给将来新的元素腾坑;如果有这种需求的话,就重写removeEldestEntry方法,比如::

@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > cacheSize;
}

    既然要删除,那么删除哪个节点呢?当然是头节点,因为所有的节点在插入和更新后,都是放在链表结尾的;所以头结点才是访问的最少的,所以afterNodeInsertion方法先拿到头节点first的key,然后去删除,删除的方法和HashMap的删除一样,不啰嗦了。
    通过HashMap和LinkedHashMap的研究发现,要想理解Java的集合类,一定要先理解常用的数据结构,比如数组,链表,队列,二叉树,红黑树等。

相关文章

网友评论

      本文标题:LinkedHashMap源码研究

      本文链接:https://www.haomeiwen.com/subject/pwtyvftx.html