美文网首页
Java集合(十)--LinkedHashMap简析

Java集合(十)--LinkedHashMap简析

作者: swz_android | 来源:发表于2019-01-04 21:22 被阅读0次

    漏掉一个Map,补一下

    LinkedHashMap是HashMap的子类,此实现与HashMap的不同之处在于它维护了一个贯穿其所有条目的双向链表。 此链表定义迭代排序,通常是键插入映射的顺序(插入顺序)。 请注意,如果将键重新插入Map,则不会影响插入顺序。

    定义及说明

    定义如下:

    public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{}
    

    它继承于HashMap,底层使用哈希表和链表来维护集合。

    构造方法为:

    //此链表哈希映射的迭代排序方法:true表示访问顺序,false表示插入顺序。
    final boolean accessOrder;
    
    //使用指定的初始容量和加载因子构造一个空的插入排序的LinkedHashMap实例
    public LinkedHashMap(int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor);
            accessOrder = false;
        }
    
    //使用指定的初始容量和默认加载因子(0.75)构造一个空的插入排序的LinkedHashMap实例
    public LinkedHashMap(int initialCapacity) {
            super(initialCapacity);
            accessOrder = false;
        }
    
    //使用默认初始容量(16)和加载因子(0.75)构造一个空的插入顺序LinkedHashMap实例。
    public LinkedHashMap() {
            super();
            accessOrder = false;
        }
    
    //构造一个插入有序的LinkedHashMap实例,其具有与指定映射相同的映射。 LinkedHashMap实例使用默认加载因子(0.75)和足以保存指定映射中的映射的初始容量创建。
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
            super();
            accessOrder = false;
            putMapEntries(m, false);
        }
    
    //使用指定的初始容量,加载因子和排序模式构造一个空的LinkedHashMap实例。
    public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
        }
    

    我们可以看到,LinkedHashMap是调用父类的构造函数做初始化。其构造函数中的accessOrder属性决定了迭代顺序,true表示访问顺序,false表示插入顺序。默认使用插入顺序,即使用通过put或其类似方法插入的顺序进行排序。

    源码及简析

    按照惯例应该是分析put()方法了,但是,很尴尬,LinkedHashMap并没有重写put()方法,也就是说,它的put()方法跟HashMap是相同的。但是,他重写了putVal()方法中调用的afterNodeAccess()和afterNodeInsertion()方法,我们看一下:

    //LinkedHashMap自己的Entry
    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);
            }
        }
    
    //双向链表的头部
    transient LinkedHashMapEntry<K,V> head;
    
    //双向链表的尾部
    transient LinkedHashMapEntry<K,V> tail;
    
    
    //将节点移动到最后
    void afterNodeAccess(Node<K,V> e) {
            LinkedHashMapEntry<K,V> last;
            //如果是按访问顺序排序,且e不是链表的尾部
            if (accessOrder && (last = tail) != e) {
                //将e节点赋值给p,将e的前节点赋值给b,将e的后节点赋值给a,将tail赋值给last
                LinkedHashMapEntry<K,V> p =
                    (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
                //将p的后一个节点置为空
                p.after = null;
                //如果p为链表头部
                if (b == null)
                    //将a置为链表头部
                    head = a;
                else
                    //否则,将a置为b的后一个元素
                    b.after = a;
                //如果p不是链表的尾部
                if (a != null)
                    //将a的上一个元素置为b
                    a.before = b;
                else
                    //否则,将b赋值给last
                    last = b;
                //当前链表如果为空
                if (last == null)
                    //将p置为链表的头部
                    head = p;
                else {
                    //否则,将p置为last的下一个元素
                    p.before = last;
                    last.after = p;
                }
                //将p置为链表的尾部
                tail = p;
                ++modCount;
            }
        }
    
    //在HashMap中传过来的evict为true
    void afterNodeInsertion(boolean evict) {
            LinkedHashMapEntry<K,V> first;
            //判断是否删除过时的条目
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                //调用HashMap的方法
                removeNode(hash(key), key, null, false, true);
            }
        }
    
    //如果此映射应删除其最旧条目,则返回true。它允许映射通过删除过时条目来减少内存消耗。
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            //糟老头子坏的很,它直接返回了false!!!
            return false;
        }
    

    通过上面分析,我们发现,如果是按照访问顺序排序,则经过一系列的判断和赋值后,将新元素插入到链表的尾部。也就是说,如果按照访问的顺序排序,则排序靠后的,就是最近插入的元素。要注意,这个排序跟在Map中的存储顺序没有关系,只是在双向链表中的顺序。我们还发现在LinkedHashMapEntry类内部,出现了before和after两个属性,而根据在afterNodeAccess()方法中的分析,他们分别指向元素的上一个和下一个节点。那么,由此得出,在LinkedHashMap中的链表是一个双向链表。

    删除条目时,调用了HashMap的remove()方法,LinkedHashMap重写了其中调用的afterNodeRemoval(node)方法(在removeNode方法中调用),我们现在来看一下:

    void afterNodeRemoval(Node<K,V> e) {
            //将e的值赋予p,将p的上个节点的值赋予b,p的下个节点的值赋予a
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            //将p前、后节点置为空
            p.before = p.after = null;
            //如果p为链表头部
            if (b == null)
                //将a设置为链表头部
                head = a;
            else
                //否则,将b的下个元素由变为a
                b.after = a;
            //如果p为链表尾部
            if (a == null)
                //将b设为链表尾部
                tail = b;
            else
                //否则,将b设置为a的上个元素
                a.before = b;
        }
    

    可以看到,通过对指定元素e前后节点b、a的设置,使得其b、a的指向不再指向p,以将其删除。

    接下来,我们看一下它的get()方法:

    public V get(Object key) {
            //初始化一个节点对象
            Node<K,V> e;
            //判断key所对应的节点是否为空,不为空则获取key所对应的节点值
            if ((e = getNode(hash(key), key)) == null)
                return null;
            //判断排序方式
            if (accessOrder)
                //如果是访问顺序,则重排序
                afterNodeAccess(e);
            return e.value;
        }
    
    
    //重排序(或是调用了put()方法之后,为节点设置前、后位置的元素)
    void afterNodeAccess(Node<K,V> e) { 
            LinkedHashMapEntry<K,V> last;
            //判断排序方式是按访问顺序排序,且e节点不是双向链表的尾部
            if (accessOrder && (last = tail) != e) {
                //将e节点赋值给p,将e的前节点赋值给b,将e的后节点赋值给a
                LinkedHashMapEntry<K,V> p =
                    (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
                //将p的后一个节点置为空
                p.after = null;
                if (b == null)
                    //如果b为空,证明e为首节点,则将e的下个节点置为首节点
                    head = a;
                else
                    //如果b不为空,则将a置为其下个节点,替代e的位置
                    b.after = a;
                if (a != null)
                    //如果a不为空,则将a的上一个节点置为b,替代e的位置
                    a.before = b;
                else
                    //如果a为空,则将b的值赋值给last
                    last = b;
                if (last == null)
                    //如果last为空(即tail为空,且b为空,说明只有p一个元素),则将p置为首节点
                    head = p;
                else {
                    //如果last不为空,则将p的前节点置为last,last的后节点置为p
                    p.before = last;
                    last.after = p;
                }
                /将p置为尾节点
                tail = p;
                ++modCount;
            }
        }
    

    可以发现,我们把get方法访问过的元素放到链表的尾部,而通过上面的分析,我们发现,如果主动删除元素,则从链表的头部开始。这样,就形成了一个简单的LRU。不过,由于其removeEldestEntry()方法直接返回了false,所以,如果我们要用它做LRU,就需要自己写一个类继承LinkedHashMap,并重写removeEldestEntry()方法。比如,我们要求Map的长度在100以内:

    private static final int MAX_ENTRIES = 100;
    
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
         return size() > MAX_ENTRIES;
    }
    

    小结

    1、LinkedHashMap继承于HashMap,是一个基于HashMap和双向链表实现的Map。

    2、LinedHashMap是有顺序的,分别为按照插入顺序排序和按照访问顺序排序,默认为按照插入顺序排序。

    3、LinkedHashMap是非同步的。

    相关文章

      网友评论

          本文标题:Java集合(十)--LinkedHashMap简析

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