Android源码之路(四、LinkHashMap)

作者: CodeInfo | 来源:发表于2018-08-08 22:21 被阅读7次

    继承自HashMap,需要先查看"HashMap"一文先了解

    要点总结

    1.LinkHaspMap继承自HashMap,除了HashMap遍历无序的外,其余特性,比如扩容机制、key和value允许为Null等都是和HashMap一致;

    2.LinkHaspMap内部额外维护了一个双向链表,在每次增删改查时,会增加或者调整链表的节点顺序,用于迭代时输出的顺序

    3.默认遍历时是按照插入节点的顺序,而HashMap是无序的,这是2者最大的区别

    4.LinkHashMap在实现时,就重写了几个方法以满足输出序列有序的需求

    5.实现了HashMap中以下的空函数,用于维护LinkHashMap中的双向链表中节点顺序:

    // Callbacks to allow LinkedHashMap post-actions
        void afterNodeAccess(Node<K,V> p) { }
        void afterNodeInsertion(boolean evict) { }
        void afterNodeRemoval(Node<K,V> p) { }
    

    6.构造函数中增加支持设置accessOrder属性用于决定遍历时是按照插入节点的顺序输出还是访问的顺序输出,默认不设置为false,即按照插入节点的顺序输出

    LinkedHashMap<String,String> linkHashMap =new LinkedHashMap<String,String>(16, (float) 0.75,true);
            linkHashMap.put("1","111");
            linkHashMap.put("2","222");
            linkHashMap.put("3","333");
            //原本顺序1,2,3
            //执行get完成后,顺序为 2,3,1
            linkHashMap.get("1");
            //执行get完成后,顺序为3,1,2
            linkHashMap.get("2");
            //执行get完成后,顺序为3,1,2,4,null
            linkHashMap.put("4","444");
            linkHashMap.put(null,null);
            Set<Map.Entry<String,String>> set=linkHashMap.entrySet();
            Iterator<Map.Entry<String,String>> iterator =set.iterator();
            while (iterator.hasNext()){
                Map.Entry<String,String> node=iterator.next();
                Log.d("tag",node.getKey()+","+node.getValue());
            }
            
            输出顺序为:
            accessOrder = true时
                3,333
                1,111
                2,222
                4,444
                null,null
            accessOrder = false时   
                1,111
                2,222
                3,333
                4,444
                null,null
    

    源码分析

    构造函数

    LinkHashMap构造函数的实现与HashMap基本相同,支持设置初始容量与负载因子,其中增加了一个参数accessOrder,用于决定遍历时是按照插入节点的顺序输出还是访问的顺序输出;

    /**
     * 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;
    /**
     * 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; //决定遍历时的输出顺序,默认false为按插入顺序,否则按访问顺序
    
        
    public LinkedHashMap(int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor);
            accessOrder = false;
        }
    
      
        public LinkedHashMap(int initialCapacity) {
            super(initialCapacity);
            accessOrder = false;
        }
    
       
        public LinkedHashMap() {
            super();
            accessOrder = false;
        }
    
       
        public LinkedHashMap(Map<? extends K, ? extends V> m) {
            super();
            accessOrder = false;
            putMapEntries(m, false);
        }
    
        
        public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
        }
    

    在HashMap节点转化为Node对象进行保存,而LinkHashMap则使用了继承自HashMap.Node的LinkedHashMapEntry对象来保存元素信息:

    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);
            }
        }
    

    与HashMap.Node相比,增加了before、after用于表示节点的前一节点和后一节点,用于形成双向链表;
    每次元素节点顺序发生改变时都会重新判断并赋值head、tail参数;

    增加、修改

    LinkHashMap的并没有重写继承自HashMap的put、putIfAbsent、replace、putAll这些增加修改元素的方法;
    从HashMap一文中可以知道,最终都会直接putVal方法,如果只是修改原节点值,则进行value的修改,如果需要增加节点的话,则调用newNode用来创建新的节点,LinkHashMap重写了newNode方法,以及afterNodeInsertion方法

        //创建一个新的节点,重写了HashMap的newNode方法
        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;
        }
        
        
        //维护更新双向链表
        // link at the end of list , 更新双向列表的头、尾节点
        private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
            LinkedHashMapEntry<K,V> last = tail;  //获取当前尾节点
            tail = p;                             //尾节点重新赋值为新增加的节点p
            if (last == null) //如果原本不存在尾节点,说明p是唯一一个元素,头节点也赋值为p
                head = p;
            else {            //如果原本LinkHashMap不为空    
                p.before = last;    //将新节点p.before指向last
                last.after = p;     //将last.after指向p,更新了双向链表
            }
        }
        
        
        //新节点插入之后回调 , 根据evict 和   判断是否需要删除最老插入的节点。如果实现LruCache会用到这个方法。
        void afterNodeInsertion(boolean evict) { // possibly remove eldest
            LinkedHashMapEntry<K,V> first;
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                removeNode(hash(key), key, null, false, true);
            }
        }
        //LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回true
        protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            return false;
        }
    

    删除

    LinkHaspMap没有重写继承自HashMap的remove(key)、remove(key,value)方法,但是有重写了clear()方法、与afterNodeRemoval(Node<K,V> e)方法

    也是简单调用super.clear()即HashMap的clear进行clear,并将自身维护的双向链表的头尾节点引用置空
    public void clear() {
            super.clear();
            head = tail = null;
        }
    
    前面HashMap一文分析过,remove节点成功时,会执行afterNodeRemoval,只是HashMap默认是空实现
    而LinkHashMap重写实现该方法,用于更新维护双向链表的头尾节点
    
    void afterNodeRemoval(Node<K,V> e) { // unlink
            //获取要移除节点e的前节点b与后节点a
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            p.before = p.after = null;
            //如果前节点为空,说明要移除的是双向链表中的第一个节点,则重新对双向链表head头节点赋值为a
            if (b == null)
                head = a;
            else//否则,将要移除的节点e的前节点b与后节点a相连,形成新链表, b->a
                b.after = a;
            //如果后节点a为空,则说明要移除的是双向链表的最后一个节点,则重新对双向链表tail尾节点赋值为b
            if (a == null)
                tail = b;
            else//否则,将要移除的节点e的后节点a与前节点b相连,形成新链表, a->b ,双向,所以a、b互相指向
                a.before = b;
        }
    

    查询

    LinkHashMap没有重写继承自HashMap的containsKey方法,但是有重写了get(k)、getOrDefault(K key,V defaultValue)、containsValue方法与afterNodeAccess(Node<K,V> e)方法;

    //根据key获取节点value
        public V get(Object key) {
            Node<K,V> e;
            if ((e = getNode(hash(key), key)) == null)
                return null;
            //LinkHashMap相比HashMap多个这个判断;如果设置了遍历需要根据访问顺序输出时,回调afterNodeAccess;
            if (accessOrder)  
                afterNodeAccess(e);
            return e.value;
        }
    
    
    //根据key获取节点value,若节点不存在则返回 defaultValue
        public V getOrDefault(Object key, V defaultValue) {
           Node<K,V> e;
           if ((e = getNode(hash(key), key)) == null)
               return defaultValue;
          //LinkHashMap相比HashMap多个这个判断;如果设置了遍历需要根据访问顺序输出时,回调afterNodeAccess;
           if (accessOrder) 
               afterNodeAccess(e);
           return e.value;
       }
       
    //判断是否存在指定value的节点元素   
    //这边直接从LinkHashMap维护的双向链表进行遍历,相比HashMap的双重for循环可以提高一定效率(因为哈希表的实际容量比存放的元素节点大,这边需要遍历整个哈希表的容量个数,而双链表的节点个数即为实际存放的元素个数)
    //而HashMap可以根据key的hash值计算出索引只需要调用getNode(int hash, Object key)单个for循环判断该位置的单向链表就行,如果全部数据遍历反而效率更低;
    
        public boolean containsValue(Object value) {
            for (LinkedHashMapEntry<K,V> e = head; e != null; e = e.after) {
                V v = e.value;
                if (v == value || (value != null && value.equals(v)))
                    return true;
            }
            return false;
        }
        
    //LinkHashMap实现了该方法用于数据访问时,将访问的节点e移动到双向链表的末尾
    //replace、get、getOrDefault以及通过put覆盖旧节点(注意是覆盖旧节点,如果是增加新节点回调的是afterNodeInsertion)会回调afterNodeAccess
    
        void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMapEntry<K,V> last;
    //accessOrder参数的判断,以及获取尾节点last,判断e不是尾节点
            if (accessOrder && (last = tail) != e) {
                LinkedHashMapEntry<K,V> p =
                    (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
    //p即将移动到尾节点,所以p.afer需要为null
                p.after = null;
    //如果b为null,则说明p原本是头节点,则将head重新执行p的下节点a
                if (b == null)
                    head = a;
    //否则将b下节点指向a                
                else
                    b.after = a;
    //a不为null,将a前节点指向b                
                if (a != null)
                    a.before = b;
    //若原本p的后置节点a为null,则p原本就是尾节点了,更新last指向p的前置节点b       
                else
                    last = b;
                if (last == null)//原本尾节点为null,则说明链表中就一个节点
                    head = p;
                else {//否则将p移动的双向链表的尾部
                    p.before = last;
                    last.after = p;
                }
    //尾节点的引用赋值成p            
                tail = p;
    //修改modCount
                ++modCount;
            }
        }
    

    值得注意的是,afterNodeAccess()函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast(即抛出抛出Concurrent Modification Exception),因为迭代的顺序已经改变。查看下面遍历的源码即可明白

    遍历

    以如下遍历为例:

        LinkedHashMap<String,String> linkHashMap =new LinkedHashMap<String,String>();
        linkHashMap.put("1","111");
        linkHashMap.put("2","222");
        linkHashMap.put("3","333");
        Set<Map.Entry<String,String>> set=linkHashMap.entrySet();
        Iterator<Map.Entry<String,String>> iterator =set.iterator();
        while (iterator.hasNext()){
            Map.Entry<String,String> node=iterator.next();
            Log.d("tag",node.getKey()+","+node.getValue());
        }
    
    transient Set<Map.Entry<K,V>> entrySet;//继承自HashMap的全局变量
     
        public Set<Map.Entry<K,V>> entrySet() {
            Set<Map.Entry<K,V>> es;
            //第一次调用=null,执行并返回new LinkedEntrySet()
            return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
        }
        
        
        
        final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
            //...
            //获取迭代器iterator
            public final Iterator<Map.Entry<K,V>> iterator() {
                return new LinkedEntryIterator();
            }
            
        }
        
        
        
        //继承自LinkedHashIterator,重写了iterator.next()方法-------->调用nextNode()
        final class LinkedEntryIterator extends LinkedHashIterator
            implements Iterator<Map.Entry<K,V>> {
            
            public final Map.Entry<K,V> next() { return nextNode(); }
        }
        
    
        //linkHashMap.keySet().iterator()
        //继承自LinkedHashIterator,重写了iterator.next()方法-------->调用nextNode()
        final class LinkedKeyIterator extends LinkedHashIterator
            implements Iterator<K> {
            public final K next() { return nextNode().getKey(); }
        }
    
        //linkHashMap.values().iterator()
        //继承自LinkedHashIterator,重写了iterator.next()方法-------->调用nextNode()
        final class LinkedValueIterator extends LinkedHashIterator
            implements Iterator<V> {
            public final V next() { return nextNode().value; }
        }
        
        //实际Iterator
        abstract class LinkedHashIterator {
            LinkedHashMapEntry<K,V> next;
            LinkedHashMapEntry<K,V> current;
            int expectedModCount;
    
            //构造函数,创建iterator实例的时候,获取双向链表头节点head,赋值expectedModCount= modCount
            //后续如果判断expectedModCount!=modCount,则代表变量过程元素发生了改变而引起fail-fast错误
            LinkedHashIterator() {
                next = head;
                expectedModCount = modCount;
                current = null;
            }
            
            //判断是否有下一节点
            public final boolean hasNext() {
                return next != null;
            }
    
            //获取下一个节点元素
            //抛出Concurrent Modification Exception情况
            //1.这边如果在遍历过程,改变哈希表元素数量的话,会导致modCount改变,从而modCount != expectedModCount
            //2.在accessOrder=true情况下,如果在遍历过程,异步get、replace操作导致触发afterNodeAccess而修改到modCount,也会致使modCount != expectedModCount
            
            final LinkedHashMapEntry<K,V> nextNode() {
                LinkedHashMapEntry<K,V> e = next;
                if (modCount != expectedModCount) //抛出Concurrent Modification Exception
                    throw new ConcurrentModificationException();
                if (e == null)
                    throw new NoSuchElementException();
                current = e;
                next = e.after; //重新对next赋值下一节点,使得hasNext()一直可以正常工作
                return e;
            }
    
            //移除当前节点,抛出ConcurrentModificationException情况同上
            public final void remove() {
                Node<K,V> p = current;
                if (p == null)
                    throw new IllegalStateException();
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                current = null;
                K key = p.key;
                removeNode(hash(key), key, null, false, false);
                //removeNode()操作本身会修改modCount,所以这边重新对expectedModCount进行赋值
                expectedModCount = modCount;
            }
        }
    

    总结

    1.LinkHashMap继承自HashMap,做了部分小改动使得其可以支持有序输出的特点:
    .内部维护了一个双向链表,迭代输出的顺序为双向链表头部到尾部各个节点的顺序
    .每次增、删、改、查可能会触发调整双向链表的节点顺序

    2.LinkHashMap还重写了containValue,使用遍历双向链表的单个for循环实现功能,相比HashMap需要整个遍历数组以及数组上每个索引位置的单向链表来得效率高; 因为HashMap数组的容量基本上比实际的有效元素节点数量大;

    3.LinkHashMap的节点对象使用继承自HashMap.Node的LinkedHashMapEntry来描述,简单修改,支持双向

    4.LinkHashMap没有重新put方法,但重新写了newNode()用于创建新节点,并将新节点链接到双向链表的尾部

    5.从迭代器中可以知道抛出Concurrent Modification Exception的情况为:

    .默认情况下(accessOrder=false)进行迭代输出,如果不通过iterator的remove()删除遍历到的节点,而是直接使用hashMap.remove、put等的话
    
    .accessOrder=true的情况下除了上诉的情况,还有在遍历过程中,如果还有其它线程在访问节点元素(get、remove),很容易造成fail-fast错误,即抛出Concurrent Modification Exception,模拟如下:
    
    .当然迭代遍历到最后一个元素节点后,因操作而引起modcout发生变化,因为要退出循环了,所以不会fail-fast
    
    accessOrder=true的情况下(相对少见),一般使用场景下,accessOrder默认为false
    
    final LinkedHashMap<String,String> linkHashMap =new LinkedHashMap<String,String>(16, (float) 0.75,true);
            linkHashMap.put("1","111");
            linkHashMap.put("2","222");
            linkHashMap.put("3","333");
            linkHashMap.put("4","444");
            linkHashMap.put(null,null);
            Set<Map.Entry<String,String>> set=linkHashMap.entrySet();
            final Iterator<Map.Entry<String,String>> iterator =set.iterator();
    
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (iterator.hasNext()){
                        //linkHashMap.get("1"); 一样会导致fail-fast
                        Map.Entry<String,String> node=iterator.next();
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //if(node.getKey()==null){//不会导致fail-fast,因为在最后一个遍历的节点访问,退出循环
                       //    linkHashMap.get("1");
                       // }
                        Log.d("tag",node.getKey()+","+node.getValue());
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    linkHashMap.get("1");
                    Log.d("tag","模拟其它线程访问");
                }
            }).start();
            
            输出:
            模拟其它线程访问
            3,333
            java.util.ConcurrentModificationException
              at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:346)
              at java.util.LinkedHashMap$EntryIterator.next(LinkedHashMap.java:375)
              at java.util.LinkedHashMap$EntryIterator.next(LinkedHashMap.java:375)
              at com.yjz.performance.demo.MainActivity$1.run(MainActivity.java:87)
              at java.lang.Thread.run(Thread.java:818)
    
    微信公众号.jpg

    相关文章

      网友评论

        本文标题:Android源码之路(四、LinkHashMap)

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