面试必备2:JDK1.8LinkedHashMap实现原理及源码分析

JDK1.8LinkedHashMap实现原理及源码分析

    • 概述
    • LinkedHashMap的数据结构
    • 增、改put(key,value)方法源码
      • 1:重写了newNode()方法源码
      • 2:复写了afterNodeAccess(Node e )
      • 3:复写了afterNodeInsertion(Node e)
    • 查:get(key)和getOrDefault( key, defaultValue)方法源码
    • 删:remove()方法源码
    • 遍历:entrySet()方法源码
      • LinkedHashMap的迭代器`LinkedEntryIterator`源码
    • 总结

首先附上我的几篇其它文章链接感兴趣的可以看看,如果文章有异议的地方欢迎指出,共同进步,顺便点赞谢谢!!!
Android framework 源码分析之Activity启动流程(android 8.0)
Android studio编写第一个NDK工程的过程详解(附Demo下载地址)
面试必备1:HashMap(JDK1.8)原理以及源码分析
Android事件分发机制原理及源码分析
View事件的滑动冲突以及解决方案
Handler机制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源码
Android三级缓存原理及用LruCache、DiskLruCache实现一个三级缓存的ImageLoader

概述

本文对LinkedHashMap的源码分析是基于JDK1.8,因为LinkedHashMap是在HashMap的基础上进行的功能扩展,所以需要掌握HashMap的源码和实现原理,如果不了解请先阅读我的另一篇HashMap的实现原理和源码分析

重点:本文如果有分析的不对的地方请大家留言指正!!!!!
再次强调一下:读此文章之前需要先去了解HashMap的源码请先阅读我的另一篇HashMap的实现原理和源码分析,以便理解。

LinkedHashMap的数据结构

想要知道 LinkedHashMap的实现原理,就必须先去了解它的数据结构(即存储机制),和HashMap一样LinkedHashMap的数据结构也是通过数组和链表组成的散列表,同样线程也是不安全的允许null值null键,不同的是LinkedHashMap在散列表的基础上内部维持的是一个双向链表在每次增、删、改、 查时增加或删除或调整链表的节点顺序。

  1. 在默认情况下LinkedHashMap遍历时的顺序是按照插入节点顺序,我们可一再构造器中通过传入accessOrder=true参数,使得其遍历顺序按照访问的顺序输出。
  2. 因继承自HashMap的一些特性LinkedHashMap都有,比如扩容的策略,哈希桶长度一定是2的N次方等等。
  3. 本质上LinkedHashMap是通过复写父类HashMap的几个抽象方法,去实现有序输出

LinkedHashMap的链表节点LinkedHashMapEntry:
LinkedHashMap与HashMap都是有数组和链表组成的散列表,不同的是LinkedHashMap的LinkedHashMapEntry继承HashMap的Node,并在其基础上进行扩展成一个双向链表其源码如下:

static class LinkedHashMapEntry extends HashMap.Node {
        LinkedHashMapEntry before, after;//分别指向当前节点的前后节点
        LinkedHashMapEntry(int hash, K key, V value, Node next) {
            super(hash, key, value, next);//其他的就是父类HashMap的Node
        }
    }

此外LinkedHashMap还增加了两个成员变量分别指向链表的头节点和尾节点

/**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMapEntry head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMapEntry tail;

增、改put(key,value)方法源码

LinkedHashMap并没有重写HashMap的put、putVal()方法,只是重写了putVal()中调用的以下三个方法

  1. 构建新节点时调用的newNode()方法,去构建LinkedHashMapEntry节点,而不是Node节点
  2. 节点被访问后afterNodeAccess(e)抽象方法
  3. 节点被插入后afterNodeInsertion(evict)抽象方法

 public V put(K key, V value) {
 		//计算hash值,调用putVal方法,与父类相同,再此不做分析
        return putVal(hash(key), key, value, false, true);
    }

在这里我将从这三个复写的方法源码进行分析,至于put()、putVal()方法的详细分析请阅读上文HashMap的实现原理和源码分析中put方法源码

1:重写了newNode()方法源码

不同的是重写了在putVal()方法中调用的newNode方法,并且在创建新节点时将该节点链接在链表尾部 linkNodeLast(p)

 Node newNode(int hash, K key, V value, Node e) {
       //创建的是LinkedHashMapEntry节点
        LinkedHashMapEntry p =
            new LinkedHashMapEntry(hash, key, value, e);
          //并且在创建新节点时将该节点链接在链表尾部
        linkNodeLast(p);
        return p;
    }

	// link at the end of list   将新增的节点,放在在链表的尾部
    private void linkNodeLast(LinkedHashMapEntry p) {
        LinkedHashMapEntry last = tail;
        tail = p;
        //集合之前是空的
        if (last == null)
            head = p;
        else {
           //将新节点连接在链表的尾部
            p.before = last;
            last.after = p;
        }
    }

2:复写了afterNodeAccess(Node e )

当节点被访问后,即put的键已经存在时,调用afterNodeAccess(Node e)方法进行排序

    /**
     * 当put(key,value)中key存在的时候,即访问某个存在的节点时
     * 如果assessOrder=true,将该节点移动到最后
     * @param e
     */
    void afterNodeAccess(Node e) { // move node to last
        LinkedHashMapEntry last;//记录尾部节点的临时变量
        if (accessOrder && (last = tail) != e) {//如果assessOrder&&该节点不在链表尾部则将其移动到尾部
            LinkedHashMapEntry p =
                    (LinkedHashMapEntry)e, b = p.before, a = p.after;//p记录当前访问的节点  b和a分别记录当前节点的前、后节点
            p.after = null;//将当前节点的after置null,因为链表尾部节点没有after节点
            //以下是断链和重新连接链表的过程   
            if (b == null)
               //b==null表示,p的前置节点是null,即p以前是头结点,所以更新现在的头结点是p的后置节点a
                head = a;
            else
                //否则直接将当前节点的前后节点相连,移除当前节点
                b.after = a;
            if (a != null)
                 //a != null表示p的后置节点不是null,则更新后置节点a的前置节点为b
                a.before = b;
            else//如果原本p的后置节点是null,则p就是尾节点。 此时 更新last的引用为 p的前置节点b
                last = b;
            if (last == null)//原本尾节点是null  则,链表中就一个节点
                //记录新的链表头
                head = p;
            else {
                //否则 更新当前节点p的前置节点为 原尾节点last, last的后置节点是p
                p.before = last;
                last.after = p;
            }
            //修改成员变量  为节点为当前节点
            tail = p;
            //修改modCount
            ++modCount;
        }
    }

3:复写了afterNodeInsertion(Node e)

/**
     * 当插入新节点后,根据evict和判断是否需要删除最老插入的节点
     * @param evict   为false时表示初始化时调用
     */
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMapEntry first;//记录链表头节点
        //是否初始化&&链表不为null&&是否移除最老的节点,莫认返回false 则不删除节点
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

    /**
     *LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。
     * 通常构建一个LruCache会在达到Cache的上限是返回true
     * @param eldest  移除最老的节点
     * @return
     */
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return false;
    }

注意: 这两个方法void afterNodeInsertion(boolean evict)boolean removeEldestEntry(Map.Entry eldest)是构建LruCache需要的回调,在LinkedHashMap里可以忽略它们。

查:get(key)和getOrDefault( key, defaultValue)方法源码

和父类HashMap相比复写了get(key)和getOrDefault( key, defaultValue)两个方法,其查找过程和父类的相同,只是在查找完成后根据构造器中的accessOrder=true时调用了afterNodeAccess(e)方法会将当前被访问到的节点e,移动至内部的双向链表的尾部

 public V get(Object key) {
        Node e;
        //获取Node节点过程和父类HashMap的相同,在这里不做分析
        if ((e = getNode(hash(key), key)) == null)
            return null;
         //accessOrder,将当前访问节点移动到双向链表的尾部
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }


 /**
     * {@inheritDoc}
     */
    public V getOrDefault(Object key, V defaultValue) {
       Node e;
        //获取Node节点过程和父类HashMap的相同,在这里不做分析
       if ((e = getNode(hash(key), key)) == null)
           return defaultValue;
           // //accessOrder,将当前访问节点移动到双向链表的尾部
       if (accessOrder)
           afterNodeAccess(e);
       return e.value;
   }

删:remove()方法源码

LinkedHashMap的remove()和HashMap的逻辑相同,故而没有重写removeNode()方法,在这里不做过多分析,详细过程请阅读HashMap的实现原理和源码分析中的remove()方法的源码,只是重写了removeNode()方法中删除节点后调用的afterNodeRemoval()这个回调方法,其源码如下:

/**
     * 在删除节点e时,同步将e从双向链表上删除
     * @param e  被删除的节点
     */
    void afterNodeRemoval(Node e) { // unlink
        LinkedHashMapEntry p =
                (LinkedHashMapEntry)e, b = p.before, a = p.after;
        //待删除节点 p 的前置后置节点都置空
        p.before = p.after = null;

        //如果前置节点是null,则现在的头结点应该是后置节点a
        if (b == null)
            head = a;
        else
            //否则将前置节点b的后置节点指向a
            b.after = a;
        if (a == null)//同理如果后置节点时null ,则尾节点应是b
            tail = b;
        else
            //否则更新后置节点a的前置节点为b
            a.before = b;
    }

扩展:复写了containsValue(Object value)相比HashMap的实现,更为高效。

 public boolean containsValue(Object value) {
 		//一次for循环遍历链表,查找Value相同的
        for (LinkedHashMapEntry e = head; e != null; e = e.after) {
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }

而HashMap的containsValue方法,是由两个for循环组成,查询效率相对较低源码如下:

public boolean containsValue(Object value) {
        Node[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {//遍历数组
                for (Node e = tab[i]; e != null; e = e.next) {//遍历链表
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

遍历:entrySet()方法源码

这里需要和上文HashMap的entrySet()方法源码对比分析,更容易理解
LinkedHashMap的entrySet()方法源码:

 public Set> entrySet() {
        Set> es;
        //直接返回 LinkedEntrySet() 集合
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
    }

遍历主要用的是LinkedEntrySet的Iterator> iterator()方法,LinkedEntrySet是LinkedHashMap 的内部类,继承成AbstractSet>集合,其详细源码比较简单不在这里进行详细分析

LinkedHashMap中内部类LinkedEntrySetiterator()方法

final class LinkedEntrySet extends AbstractSet> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        public final Iterator> iterator() {
        	//放回LinkedEntryIterator迭代器
            return new LinkedEntryIterator();
        }
        ....省略部分源码...
    }

通过上文HashMap的extrySet方法源码和以上源码分析可以知道,当我们HashMap或LinkedHashMap调用entrySet()的Itrrator()方法返回的Iterator不同,分别是new EntryIterator();new LinkedEntryIterator(),通过返回的Iterator对象进行集合的遍历过程,接下来我将对其其源码入手分析集合的遍历过程

LinkedHashMap的迭代器LinkedEntryIterator源码

final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator> {
        //迭代器的next方法就是返回通过调用`nextNode()`返回下一个节点
        public final Map.Entry next() { return nextNode(); }
    }

nextNode()方法是父类LinkedHashIterator中的方法,LinkedHashMap的本质LinkedHashIterator是实现集合遍历,其源码分析如下

abstract class LinkedHashIterator {
        LinkedHashMapEntry next;//下一个节点
        LinkedHashMapEntry current;//当前操作的节点
        int expectedModCount;
		 /**
            * 构造器做初始化动作
           * 需要注意的是:expectedModCount的作用:
           * 和HashMap一样,LinkedHashMap不是线程安全的,所以在迭代的时候,会将modCount赋值到迭代器的expectedModCount属性中,
           * 然后进行迭代,如果在迭代的过程中HashMap被其他线程修改了,modCount的数值就会发生变化,
          * 这个时候expectedModCount和ModCount不相等,迭代器就会抛出 ConcurrentModificationException()异常
     */
        LinkedHashIterator() {
        	//初始化的时候next为双向链表的表头
            next = head;
           //遍历前,先记录modCount值
            expectedModCount = modCount;
            //当前节点初始化为null
            current = null;
        }
        public final boolean hasNext() {
        	//判断是否有下一个节点,即判断next是否为null
            return next != null;
        }
		
		/**
          * nextNode()的方法,就是Iterator中next方法中调用的,
         * 其遍历LinkedHashMap过程就是,就是从内部维护的双向链表的表头开始循环输出。
        */
        final LinkedHashMapEntry nextNode() {
        	//e用于记录返回的节点
            LinkedHashMapEntry e = next;
            //线程安全判断处理
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
             //current  =next
            current = e;
            //next指向next的下一个节点,遍历链表
            next = e.after;
            return e;
        }
		 /**
           * Iterator删除方法 本质上还是调用了HashMap的removeNode方法
          * 只是在调用之前,通过modCount != expectedModCount时抛出并发修改异常,处理线程不安全问题,
          * 如果相等则调用HashMap的removeNode方法移除节点
          * 
        */
        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;
            //调用HashMap的,removeNode方法
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

HashMap的遍历本质上是通过Iterator的next()方法实现,而next()就是调用nextNode()方法,从nextNode的源码可以看出:迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出。而双链表节点的顺序在LinkedHashMap的增、删、改、查时都会更新。以满足按照插入顺序输出,还是访问顺序输出。

总结

本文是在上文HashMap的实现原理和源码分析基础上做的分析

  1. LinkedHashMap的数据结构就是在HashMap的散列表的基础上维持了一个双向链表,在每次增、删、改、 查时增加或删除或调整链表的节点顺序。
  2. LinkedHashMap就是复写了HashMap提供的几个抽象方法,在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序以改变它迭代遍历时的顺序

你可能感兴趣的:(Java源码分析)