美文网首页
LinkedHashMap解析

LinkedHashMap解析

作者: jxiang112 | 来源:发表于2018-12-29 19:38 被阅读6次

注:本文来自Android中LinkedHashMap的解析
阅读本篇文字建议先阅读以下HashMap解析
首先LinkedHashMap的字面意思链式Map,其实它继承自HashMap,所以它具备HashMap的特性:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
 //...省略
}

既然继承自HashMap,那么LinkedHashMap与HashMap有什么不一样的功能呢?
答案是LinkedHashMap比HashMap多的唯一功能是有序的,ok,那我就开始罗列问题了:
1、LinkedHashMap采用的数据结构是什么?
2、LinkedHashMap的有序是key的有序还是value有序?
3、LinkedHashMap是怎么确保有序的?
4、LinkedHashMap是怎么实现有序访问?
5、LinkedHashMap的应用场景?
6、LinkedHashMap与HashMap的区别

问题1、LinkedHashMap采用的数据结构是什么?

因为LinkedHashMap是继承自HashMap的,其存储还是采用一维数组+链表结构,但这里注意了这里链表是双向链表,而HashMap采用的是单向链表;其有序性是通过外层的双向链表确保的

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

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMapEntry<K,V> tail;
  
//节点结构
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
        LinkedHashMapEntry<K,V> before, after;
        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

上述代码可以看到,LinkedHashMap额外添加的head和tail是用来确保有序的,分别表示链表的头部和尾部。其中节点使用的时候LinkedHashMapEntry,LinkedHashMapEntry中通过before, after来确保双向的

问题2、LinkedHashMap的有序是key的有序还是value有序?

答案是既不是key有序也不是value有序,其有两种有序:一种是插入有序;一种是访问有序。插入有序即遍插入时后插入的排在后面,迭代访问时是按照插入的顺序进行迭代的;访问有序,这个是不是有点懵逼?访问有序指的是每次访问元素时,先将元素从链表中删除,接着再将此元素放到链表末尾,这样确保访问的有序性。
我们先来看其中一个很中的字段描述:

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

accessOrder就是用于确定是插入有序还是访问有序的。注释也说的很明白:当accessOrder为false是插入有序;当accessOrder为true是是访问有序,默认值为false,采用插入有序。

问题3、LinkedHashMap是怎么确保有序的?

问题2中accessOrder是确定采用的是那种有序,我们也知道有两种有序方式,那么下面我们分别来看下源码中这两种有序的实现:

插入有序

LinkedHashMap并没有重新put的方法,也就是说put操作默认采用的是HashMap的put实现,那么LinkedHashMap是通过什么来确保插入有序呢?我们重新看下HashMap的添加节点框架:

//HashMap.java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //..省略
}

重点在于newNode方法,我们看到LinedHashMap是有覆盖了此方法的实现的:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        //生成新的节点
        LinkedHashMapEntry<K,V> p =
            new LinkedHashMapEntry<K,V>(hash, key, value, e);
        //将节点放入有序链表的末尾
        linkNodeLast(p);
        return p;
    }

private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
        //当前有序链表的末尾节点
        LinkedHashMapEntry<K,V> last = tail;
        //将当前有序链表的末尾节点设置为新的节点p
        tail = p;
        
        if (last == null)
            //如果原有序链表的末尾节点为空,代表有序链表是空的
            head = p;
        else {
            //如果原有序链表的末尾节点不为空,将新的节点放入有序链表的末尾
            p.before = last;
            last.after = p;
        }
    }

插入有序采用的是HashMap的put操作,并不会更改HashMap的put逻辑,只是在其基础上通过新增有序链表来确保插入的有序性,在添加新的节点时将新节点放入有序链表的末尾即可。
这个里面的信息量还是挺多的:

  • LinekedHashMap的put操作还是用的是HashMap的put操作
  • LinkedHashMap 散列表还是用的HashMap的散列表实现方式
  • LinkedHashMap只是新增额外的有序链表来确保插入的有序性
访问有序

访问有序顾名思义对每次访问的元素进行排序,后面访问的元素排在前面访问的元素的后面。既然是访问,那么对于的就是get方法,LinkedHashMap是有重新get方法的,我们来看下其实现源码:

public V get(Object key) {
        Node<K,V> e;
        // 通过父类的getNode方法获取key对于的元素
        if ((e = getNode(hash(key), key)) == null)
            return null;
        
        if (accessOrder)
            //如果采用的是访问有序,那么调用afterNodeAccess方法将访问的元素放入有序链表的表尾
            afterNodeAccess(e);
        return e.value;
    }
 
/**
将当前访问节点放入有序链表的表尾,思想是先将当前访问节点进行链表删除操作,再将访问的节点放入链表末尾
*/
void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            // 如果是访问有序方式
           /**
           p,e: 当前访问的节点
           b:当前访问节点的前一个节点
           a:   当前访问节点的后一个节点
          */
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
           //将当前节点的后一个节点设置为空,代表此节点是表尾节点
            p.after = null;
            
            if (b == null)
                /**
                  当前访问节点的前一个节点为空,代表链表为空,所以设置表头为a(当前访问节点的后台一个节点)。意思对当前访问节点的删除操作
                */
                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;
        }
    }

访问节点的实现思想是:先将访问的节点从有序链表中删除,再放入有序链表的表尾。

  • 综合插入有序和访问有序的源码,细心的会发现,不管是插入有序还是访问有序,每次添加的元素都是放入有序链表的表尾。
问题4、LinkedHashMap是遍历有序?

如果说key是int类型,并且值分别0,1,2,3,那么是否通过for已经自增key来get是否是有序的呢?答案是否定的,前面也分析了LinkedHashMap是插入有序或者访问有序,而不是key或者value有序。所以只能通过迭代器对有序链表进行迭代才是王道,可以使用LinkedHashMap的keySet()、values()、entrySet()进行迭代实现遍历的有序,我们看下其内部LinkedEntrySet类迭代的实现:

public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            if (action == null)
                throw new NullPointerException();
            int mc = modCount;
            // Android-changed: Detect changes to modCount early.
            //对有序链表进行迭代访问,实现有序访问
            for (LinkedHashMapEntry<K,V> e = head; (e != null && mc == modCount); e = e.after)
                action.accept(e);
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
问题5、LinkedHashMap的应用场景?

LinkeHashMap的特性是可以实现插入有序和访问有序,其最常见的应用场景就是实现LRU(Least Recently Used)缓存算法。LRU缓存算法的思想是有序淘汰最近最少使用(最不常访问)的数据,而LinkedHashMap访问排序方式是可以轻松的实现此需求的,LinkedHashMap中每次访问的元素(最近访问的数据)放入有序链表的末尾,意思表头是最近最不常使用的数据,所以如果LinkedHashMap实现LRU算法,优先淘汰的是表头的数据。

问题6、LinkedHashMap与HashMap的区别

主要的区别是:LinkedHashMap是插入有序或者访问有序的散列表,而HashMap不是有序散列表

相关文章

  • java 集合 5 - LinkedHashMap

    参考文章:Java LinkedHashMap 源码解析,图解集合6:LinkedHashMap 基于 jdk 1...

  • LinkedHashMap原理分析

    本文主要内容 LinkedHashMap使用 LinkedHashMap源码解析 Lru算法 今天打算看看andr...

  • Java基础之LinkedList源码解析

    Java集合源码解析系列 Java基础之HashMap源码解析 Java基础之LinkedHashMap源码解析 ...

  • Java基础之ArrayList源码解析

    Java集合源码解析系列 Java基础之HashMap源码解析 Java基础之LinkedHashMap源码解析 ...

  • Java基础之HashTable源码解析

    Java集合源码解析系列 Java基础之HashMap源码解析 Java基础之LinkedHashMap源码解析 ...

  • LinkedHashMap解析

    前言 LinkedHashMap看名字就知道是链表结构, LinkedHashMap继承了HashMap, 上篇文...

  • LinkedHashMap解析

    建议阅读本文前先了解HashMap,鄙人文章 HashMap解析 很简单,LinkedHashMap的Entry只...

  • LinkedHashMap解析

    注:本文来自Android中LinkedHashMap的解析阅读本篇文字建议先阅读以下HashMap解析首先Lin...

  • LinkedHashMap源码解析

    一 成员变量解析 二 关键方法解析 1 添加元素 2 获取元素 LinkedHashMap 继承了 HashMap...

  • LruCache

    LruCache的使用 LruCache部分源码解析 LruCache 利用 LinkedHashMap 的一个特...

网友评论

      本文标题:LinkedHashMap解析

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