Java集合框架分析(五)LinkedHashMap分析

LinkedHashMap简介

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
复制代码

继承自 HashMap,一个有序的 Map 接口实现,这里的有序指的是元素能够按插入顺序或访问顺序排列;与 HashMap 相比,由于 LinkedHashMap 是继承自 HashMap,所以LinkedHashMap,一样是基于散列表实现。同时实现了 Serializable 和 Cloneable 接口,支持序列化和克隆。而且一样不是线程安全的。区别是其内部维护了一个双向循环链表,该链表是有序的,能够按元素插入顺序或元素最近访问顺序 (LRU) 排列。算法

LinkedHashMap数据结构

LinkedHashMap 不只像 HashMap 那样对其进行基于哈希表和单链表的 Entry 数组+ next 链表的存储方式,并且还结合了 LinkedList 的优势,为每一个 Entry 节点增长了前驱和后继,并增长了一个为 header 头结点,构造了一个双向循环链表。也就是说,每次 put 进来 KV,除了将其保存到对哈希表中的对应位置外,还要将其插入到双向循环链表的尾部。数组

在这里插入图片描述

上图是 LinkedHashMap 的所有数据结构,包含散列表和循环双向链表,因为循环双向链表线条太多了,很差画,简单的画了一个节点(黄色圈出来的)示意一下,注意左边的红色箭头引用为 Entry 节点对象的 next 引用(散列表中的单链表),绿色线条为 Entry 节点对象的 before, after 引用(循环双向链表的先后引用);缓存

在这里插入图片描述

上图专门把循环双向链表抽取出来,直观一点,注意该循环双向链表的头部存放的是最久访问的节点或最早插入的节点,尾部为最近访问的或最近插入的节点,迭代器遍历方向是从链表的头部开始到链表尾部结束,在链表尾部有一个空的 header 节点,该节点不存放 key-value 内容,为 LinkedHashMap 类的成员属性,循环双向链表的入口;安全

LinkedHashMap源码

上面是分析 LinkedHashMap 源码的常规知识点,了解一下,才能更好的分析它的源码,下面咱们便开始正式的进行分析工做。bash

属性:

//属性设置,序列化ID
private static final long serialVersionUID = 3801124242820219131L;
//双向链表的头部
private transient LinkedHashMapEntry<K,V> header;
//迭代的时候所用到的顺序,若是为FALSE,则按照插入的时候顺序
private final boolean accessOrder;
复制代码

这些属性虽然简单,可是比较重要,一开始就直接详细说明,不大好理解,等咱们分析完了代码再来回顾一下它们所表示的意思。咱们来分析分析它的构造函数。数据结构

构造器分析

设置初始容量和加载因子的构造器app

/**
  * 设置初始容量和加载因子的构造器
  */
 public LinkedHashMap(int initialCapacity, float loadFactor) {
     super(initialCapacity, loadFactor);
     accessOrder = false;
 }
复制代码

设置初始容量的构造器ide

/**
  * 设置初始容量的构造器
  * @param  initialCapacity the initial capacity
  * @throws IllegalArgumentException if the initial capacity is negative
  */
 public LinkedHashMap(int initialCapacity) {
     super(initialCapacity);
     accessOrder = false;
 }
复制代码

默认的空参数的构造器,默认容量为16以及加载因子为0.75函数

/**
  * 默认的空参数的构造器,默认容量为16以及加载因子为0.75
  * with the default initial capacity (16) and load factor (0.75).
  */
 public LinkedHashMap() {
     super();
     accessOrder = false;
 }
复制代码

使用一个现有的Map来构造LinkedHashMap学习

/**
  * 使用一个现有的Map来构造LinkedHashMap
  * @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(m);
     accessOrder = false;
 }
复制代码

设定迭代顺序的构造器

/**
  * 设定迭代顺序的构造器
  * @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;
 }
复制代码

这些构造器都比较简单,咱们稍微说起一下,若未指定初始容量 initialCapacity,则默认为使用 HashMap 的初始容量,即 16。若未指定加载因子 loadFactor,则默认为 0.75。accessOrder 默认为 faslse。这里须要介绍一下这个布尔值,它是双向链表中元素排序规则的标志位。

accessOrder 若为 false,遍历双向链表时,是按照插入顺序排序。 accessOrder 若为 true,表示双向链表中的元素按照访问的前后顺序排列,最早遍历到(链表头)的是最近最少使用的元素。

从构造方法中能够看出,默认都采用插入顺序来维持取出键值对的次序。全部构造方法都是经过调用父类的构造方法来建立对象的。

在父类的构造器中咱们能够看到它调用了 init 方法,即在 Map 类中的构造器中调用了 init 方法,咱们进入查看一下内容

@Override
    void init() {
        header = new LinkedHashMapEntry<>(-1, null, null, null);
        header.before = header.after = header;
    }
复制代码

这个 init 方法主要是对 header 节点进行初始化的,构成一个双向链表。分析完了构造器,接着咱们分析一下最多见的一个属性 Entry。

LinkedHashMapEntry分析

//这个Entry继承自HashMapEntry
 private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
        //双向节点的先后引用
        // These fields comprise the doubly linked list used for iteration.
        LinkedHashMapEntry<K,V> before, after;
        
        //构造器
        LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) {
            super(hash, key, value, next);
        }
        //移除一个节点
        private void remove() {
            before.after = after;
            after.before = before;
        }
        /**
         * 在指定的位置前面插入一个节点
         */
        private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }
        /*
         *在HashMap的put和get方法中,会调用该方法,在HashMap中该方法为空
         * 在LinkedHashMap中,当按访问顺序排序时,该方法会将当前节点插入到链表尾部(头结点的前一个节点),不然不作任何事
         */
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            //当LinkedHashMap按访问排序时
            if (lm.accessOrder) {
                lm.modCount++;
                //移除当前节点
                remove();
                //将当前节点插入到头结点前面
                addBefore(lm.header);
            }
        }
        void recordRemoval(HashMap<K,V> m) {
            remove();
        }
    }
复制代码

接着分析最经常使用的方法 put。

put分析

咱们在使用 LinkedHashMap 的 put 方法时,发现它调用的是 HashMap 的 put 方法,本身自己没有复写 put 方法,因此这种状况下,咱们就得分两种状况来讨论 LinkedHashMap 的 put 操做了。

Key已存在的状况

在 HashMap 的 put 方法中,在发现插入的 key 已经存在时,除了作替换工做,还会调用recordAccess() 方法,在 HashMap 中该方法为空。LinkedHashMap 覆写了该方法,(调用LinkedHashmap 覆写的 get 方法时,也会调用到该方法),LinkedHashMap 并无覆写 HashMap 中的 put 方法,recordAccess() 在 LinkedHashMap 中的实现以下:

//若是当前标明的accessOrder为TRUE的话,则将当前访问的Entry放置到双向循环链表的尾部,以标明最近访问 ,这也是为何在HashMap.Entry中有一个空的 recordAccess(HashMap<K,V> m)方法的缘由
void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            //LRU算法,将访问的节点插入到链表尾部
            if (lm.accessOrder) {
                lm.modCount++;
                //删除当前节点
                remove();
                //将当前节点插入到头结点前面
                addBefore(lm.header);
            }
        }
        
//将当前节点插入到头结点前面
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }
     
复制代码

key不存在的状况下

在 put 新 Entry 的过程当中,若是发现 key 不存在时,除了将新 Entry 放到哈希表的相应位置,还会调用 addEntry 方法,它会调用 creatEntry 方法,该方法将新插入的元素放到双向链表的尾部,这样作既符合插入的前后顺序,又符合了访问的前后顺序。

//建立节点,插入到LinkedHashMap中,该方法覆盖HashMap的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
        //注意头结点的下个节点即header.after,存放于链表头部,是最不常常访问或第一个插入的节点,
        LinkedHashMapEntry<K,V> eldest = header.after;
        //若是有必要,则删除掉该近期最少使用的节点
        if (eldest != header) {
            boolean removeEldest;
            size++;
            try {
                //removeEldestEntry方法的实现,这里默认为false
                removeEldest = removeEldestEntry(eldest);
            } finally {
                size--;
            }
            if (removeEldest) {
                removeEntryForKey(eldest.key);
            }
        }
        //调用HashMap的addEntry方法
        super.addEntry(hash, key, value, bucketIndex);
    }
    
//建立节点,并将该节点插入到链表尾部
 void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> old = table[bucketIndex];
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
        table[bucketIndex] = e;
        //并将其移到双向链表的尾部  
        e.addBefore(header);
        size++;
    }
复制代码

在上面的 addEntry 方法中有一个 removeEldestEntry 方法,这个方法能够被覆写,好比能够将该方法覆写为若是设定的内存已满,则返回 true,这样就能够将最近最少使用的节点(header 后的节点)删除掉。

为何这个方法始终返回 false?

结合上面的 addEntry(int hash,K key,V value,int bucketIndex) 方法,这样设计可使LinkedHashMap 成为一个正常的 Map,不会去移除“最老”的节点。 为何不在代码中直接去除这部分逻辑而是设计成这样呢?这为开发者提供了方便,若但愿将 Map 当作 Cache 来使用,而且限制大小,只需继承 LinkedHashMap 并重写 removeEldestEntry(Entry<K,V> eldest) 方法,像这样:

private static final int MAX_ENTRIES = 100;
protected boolean removeEldestEntry(Map.Entry eldest) {
      return size() > MAX_ENTRIES;
}
复制代码

总结一下 只要是 put 进来的新元素,无论 accessOrder 标志位是什么,均将新元素放到双链表尾部,而且能够在须要实现Lru算法时时覆写 removeEldestEntry 方法,剔除最近最少使用的节点。

get分析

//覆写HashMap中的get方法,经过getEntry方法获取Entry对象。  
//注意这里的recordAccess方法,  
//若是链表中元素的排序规则是按照插入的前后顺序排序的话,该方法什么也不作,  
//若是链表中元素的排序规则是按照访问的前后顺序排序的话,则将e移到链表的末尾处。
public V get(Object key) {
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }
    
void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }
复制代码

get(Object key) 方法经过 HashMap 的 getEntry(Object key) 方法获取节点,并返回该节点的 value 值,获取节点若是为 null 则返回 null。经过 key 获取 value,与 HashMap 的区别是:当 LinkedHashMap 按访问顺序排序的时候,会将访问的当前节点移到链表尾部(头结点的前一个节点)。

到这里咱们来具体总结一下 accessOrder 标志位的做用原理。

一、accessOrder 不起做用

对于 put 操做时,无论 accessOrder 标志位是什么,咱们都将节点插入到链表的尾部,可是呢,能够在须要实现 Lru 算法时时覆写 removeEldestEntry 方法,剔除最近最少使用的节点。

二、accessOrder 起做用

当咱们进行 put 操做是,若是 key 不等于 null 的话,会调用 recordAccess 方法,在该方法中 accessOrder 就得起做用了,若是 accessOrder 为 fasle 时,什么也不作,也就是说当咱们放入已经存在 Key 的键值对,它在双链表中的位置是不会变的。accessOrder 设置为 true 时, put 操做会将相关元素放置到双链表的尾部。

另一种状况就是 get 操做,get 操做咱们同时也会调用 recordAccess 方法,对于这个方法,咱们须要判断 accessOrder 的状态,若是 accessOrder 为 fasle 时,什么也不作,也就是说当咱们放入已经存在 Key 的键值对,它在双链表中的位置是不会变的。accessOrder 设置为 true 时,put 操做会将相关元素放置到双链表的尾部。在缓存的角度来看,这就是所谓的“脏数据”,即最近被访问过的数据,所以在须要清理内存时(添加进新元素时),就能够将双链表头节点(空节点)后面那个节点剔除。

不经常使用方法

到此为止,基本上 LinkedHashMap 比较重要的方法就分析过了,还剩一些比较不重要的方法,咱们一次性给它注视下,稍微看下。

//
@Override
void transfer(HashMapEntry[] newTable) {
    int newCapacity = newTable.length;
    for (LinkedHashMapEntry<K,V> e = header.after; e != header; e = e.after) {
        int index = indexFor(e.hash, newCapacity);
        e.next = newTable[index];
        newTable[index] = e;
    }
}
复制代码

transfer(HashMap.Entry[] newTable) 方法和 init() 方法同样也在 HashTable 中被调用。transfer(HashMap.Entry[] newTable) 方法在 HashMap 调用 resize(int newCapacity) 方法的时候被调用。根据链表节点 e 的哈希值计算 e 在新容量的 table 数组中的索引,并将 e 插入到计算出的索引所引用的链表中。

public boolean containsValue(Object value) {
        // Overridden to take advantage of faster iterator
        if (value==null) {
            for (LinkedHashMapEntry e = header.after; e != header; e = e.after)
                if (e.value==null)
                    return true;
        } else {
            for (LinkedHashMapEntry e = header.after; e != header; e = e.after)
                if (value.equals(e.value))
                    return true;
        }
        return false;
    }
复制代码

重写父类的 containsValue(Object value) 方法,直接经过 header 遍历链表判断是否有值和 value 相等,利用双向循环链表的特色进行查询,少了对数组的外层 for 循环 ,而不用查询 table 数组。

public void clear() {
       super.clear();
       header.before = header.after = header;
   }
复制代码

clear() 方法先调用父类的方法 clear() 方法,以后将链表的 header 节点的 before 和 after 引用都指向 header 自身,即 header 节点就是一个双向循环链表。这样就没法访问到原链表中剩余的其余节点,他们都将被 GC 回收。清空 HashMap 的同时,将双向链表还原为只有头结点的空链表。

以上即是 LinkedHashMap 源码主要方法的分析,到这里就要结束了,咱们来总结一下关于 HashMap 和 LinkedHashMap 的相关东西。

总结

对于 LinkedHashMap,咱们总结了如下几点内容:

一、因为 LinkedHashMap 继承自 HashMap,因此它不只像 HashMap 那样对其进行基于哈希表和单链表的 Entry 数组+ next 链表的存储方式,并且还结合了 LinkedList 的优势,为每一个 Entry 节点增长了前驱和后继,并增长了一个为 header 头结点,构造了一个双向循环链表。(多一个以 header 为头结点的双向循环链表,也就是说,每次 put 进来 KV,除了将其保存到对哈希表中的对应位置外,还要将其插入到双向循环链表的尾部。)

二、LinkedHashMap 的属性比 HashMap 多了一个 accessOrder 属性。当它 false 时,表示双向链表中的元素按照 Entry 插入 LinkedHashMap 到中的前后顺序排序,即每次 put 到 LinkedHashMap 中的 Entry 都放在双向链表的尾部,这样遍历双向链表时,Entry 的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序;当它为 true 时,表示双向链表中的元素按照访问的前后顺序排列,能够看到,虽然 Entry 插入链表的顺序依然是按照其 put 到 LinkedHashMap 中的顺序,但 put 和 get 方法均有调用 recordAccess 方法(put 方法在 key 相同,覆盖原有的 Entry 的状况下调用 recordAccess 方法), 该方法判断 accessOrder 是否为 true,若是是,则将当前访问的 Entry(put 进来的 Entry 或 get 出来的 Entry)移到双向链表的尾部(key 不相同时,put 新 Entry 时,会调用 addEntry,它会调用 creatEntry,该方法一样将新插入的元素放入到双向链表的尾部,既符合插入的前后顺序,又符合访问的前后顺序,由于这时该 Entry 也被访问了),不然,什么也不作。

三、构造函数中有设置 accessOrder 的方法,若是咱们须要实现 LRU 算法时,就须要将 accessOrder 的值设定为 TRUE。

四、在 HashMap 的 put 方法中,若是 key 不为 null 时且哈希表中已经在存在时,循环遍历 table[i] 中的链表时会调用 recordAccess 方法,而在 HashMap 中这个方法是个空方法,在LinkedHashMap中则实现了该方法,该方法会判断 accessOrder 是否为 true,若是为 true,它会将当前访问的 Entry(在这里指 put 进来的 Entry)移动到双向循环链表的尾部,从而实现双向链表中的元素按照访问顺序来排序(最近访问的 Entry 放到链表的最后,这样屡次下来,前面就是最近没有被访问的元素,在实现 LRU 算法时,当双向链表中的节点数达到最大值时,将前面的元素删去便可,由于前面的元素是最近最少使用的),不然什么也不作。

关于做者

专一于 Android 开发多年,喜欢写 blog 记录总结学习经验,blog 同步更新于本人的公众号,欢迎你们关注,一块儿交流学习~

在这里插入图片描述
相关文章
相关标签/搜索