zoukankan      html  css  js  c++  java
  • 给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析

      前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表)。接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序。
         来具体看下LinkedHashMap的实现。
         
    1.定义 
    1 public class LinkedHashMap<K,V>
    2     extends HashMap<K,V>
    3     implements Map<K,V>
      从定义可以看到LinkedHashMap继承于HashMap,且实现了Map接口。这也就意味着HashMap的一些优秀因素可以被继承下来,比如hash寻址,使用链表解决hash冲突等实现的快速查找,对于HashMap中一些效率较低的内容,比如容器扩容过程,遍历方式,LinkedHashMap是否做了一些优化呢。继续看代码吧。
     
    2.底层存储
     
         开篇我们说了LinkedHashMap是基于HashMap,并在其基础上维持了一个双向链表,也就是说LinkedHashMap是一个hash表(数组+单向链表) +双向链表的实现,到底实现方式是怎么样的,来看一下:
     1     /**
     2      * The head of the doubly linked list.
     3      */
     4     private transient Entry<K,V> header ;
     5 
     6     /**
     7      * The iteration ordering method for this linked hash map: <tt>true</tt>
     8      * for access -order, <tt> false</tt> for insertion -order.
     9      *
    10      * @serial
    11      */
    12     private final boolean accessOrder;
      看到了一个无比熟悉的属性header,它在LinkedList中出现过,英文注释很明确,是双向链表的头结点对不对。再看accessOrder这个属性,true表示最近较少使用顺序,false表示插入顺序。当然你说怎么没看到数组呢,别忘了LinkedHashMap继承于HashMap,Entry[]这个东东就不用写了吧。。。
     
         再来看下Entry这个节点类和HashMap中的有什么不同。
     1     /**
     2      * LinkedHashMap entry.
     3      */
     4     private static class Entry<K,V> extends HashMap.Entry<K,V> {
     5         // These fields comprise the doubly linked list used for iteration.
     6         // 双向链表的上一个节点before和下一个节点after
     7         Entry<K,V> before, after ;
     8 
     9        // 构造方法直接调用父类HashMap的构造方法(super)
    10        Entry( int hash, K key, V value, HashMap.Entry<K,V> next) {
    11             super(hash, key, value, next);
    12         }
    13 
    14         /**
    15          * 从链表中删除当前节点的方法
    16          */
    17         private void remove() {
    18             // 改变当前节点前后两个节点的引用关系,当前节点没有被引用后,gc可以回收
    19             // 将上一个节点的after指向下一个节点
    20             before.after = after;
    21             // 将下一个节点的before指向前一个节点
    22             after.before = before;
    23         }
    24 
    25         /**
    26          * 在指定的节点前加入一个节点到链表中(也就是加入到链表尾部)
    27          */
    28         private void addBefore(Entry<K,V> existingEntry) {
    29             // 下面改变自己对前后的指向
    30             // 将当前节点的after指向给定的节点(加入到existingEntry前面嘛)
    31             after  = existingEntry;
    32             // 将当前节点的before指向给定节点的上一个节点
    33             before = existingEntry.before ;
    34 
    35             // 下面改变前后最自己的指向
    36             // 上一个节点的after指向自己
    37             before.after = this;
    38             // 下一个几点的before指向自己
    39             after.before = this;
    40         }
    41 
    42         // 当向Map中获取查询元素或修改元素(put相同key)的时候调用这个方法
    43         void recordAccess(HashMap<K,V> m) {
    44             LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    45             // 如果accessOrder为true,也就是使用最近较少使用顺序
    46             if (lm.accessOrder ) {
    47                 lm. modCount++;
    48                 // 先删除,再添加,也就相当于移动了
    49                 // 删除当前元素
    50                 remove();
    51                 // 将当前元素加入到header前(也就是链表尾部)
    52                 addBefore(lm. header);
    53             }
    54         }
    55 
    56         // 当从Map中删除元素的时候调动这个方法
    57         void recordRemoval(HashMap<K,V> m) {
    58             remove();
    59         }
    60 }

      可以看到Entry继承了HashMap中的Entry,但是LinkedHashMap中的Entry多了两个属性指向上一个节点的before和指向下一个节点的after,也正是这两个属性组成了一个双向链表。等等。。。Entry还有一个继承下来的next属性,这个next是单向链表中用来指向下一个节点的,怎么回事嘛,怎么又是单向链表又是双向链表呢,都要晕了对不对,其实想的没错,这里的节点即是Hash表中的单向链表中的一个节点,它又是LinkedHashMap维护的双向链表中的一个节点,是不是瞬间觉得高大上了。图解一下吧(不要告诉我图好乱,我看不懂。。。)

      注:黑色箭头指向表示单向链表的next指向,红色箭头指向表示双向链表的before指向,蓝色箭头指向表示双向链表的after指向。另外LinkedHashMap种还有一个header节点是不保存数据的,这里没有画出来。
         
         从上图可以看出LinkedHashMap仍然是一个Hash表,底层由一个数组组成,而数组的每一项都是个单向链表,由next指向下一个节点。但是LinkedHashMap所不同的是,在节点中多了两个属性before和after,由这两个属性组成了一个双向循环链表(你怎么知道是循环,下面在说喽),而由这个双向链表维持着Map容器中元素的顺序。看下Entry中的recordRemoval方法,该方法将在节点被删除时候调用,Hash表中链表节点被正常删除后,调用该方法修正由于节点被删除后双向链表的前后指向关系,从这一点来看,LinkedHashMap比HashMap的add、remove、set等操作要慢一些(因为要维护双向链表 )
         
         明白了LinkedHashMap的底层存储结构后,我们来看一下它的构造方法以及怎么样对链表进行初始化的。
     
    3.构造方法
     1 /**
     2      * 构造一个指定初始容量和加载因子的LinkedHashMap,默认accessOrder为false
     3      */
     4     public LinkedHashMap( int initialCapacity, float loadFactor) {
     5         super(initialCapacity, loadFactor);
     6         accessOrder = false;
     7     }
     8 
     9     /**
    10      * 构造一个指定初始容量的LinkedHashMap,默认accessOrder为false
    11      */
    12     public LinkedHashMap( int initialCapacity) {
    13         super(initialCapacity);
    14         accessOrder = false;
    15     }
    16 
    17     /**
    18      * 构造一个使用默认初始容量(16)和默认加载因子(0.75)的LinkedHashMap,默认accessOrder为false
    19      */
    20     public LinkedHashMap() {
    21         super();
    22         accessOrder = false;
    23     }
    24 
    25     /**
    26      * 构造一个指定map的LinkedHashMap,所创建LinkedHashMap使用默认加载因子(0.75)和足以容纳指定map的初始容量,默认accessOrder为false 。
    27      */
    28     public LinkedHashMap(Map<? extends K, ? extends V> m) {
    29         super(m);
    30         accessOrder = false;
    31     }
    32 
    33     /**
    34      * 构造一个指定初始容量、加载因子和accessOrder的LinkedHashMap
    35      */
    36     public LinkedHashMap( int initialCapacity,
    37                       float loadFactor,
    38                          boolean accessOrder) {
    39         super(initialCapacity, loadFactor);
    40         this.accessOrder = accessOrder;
    41 }
      构造方法很简单基本都是调用父类HashMap的构造方法(super),只有一个区别就是对于accessOrder的设定,上面的构造参数中多数都是将accessOrder默认设置为false,只有一个构造方法留了一个出口可以设置accessOrder参数。看完了构造方法,发现一个问题,咦?头部节点header的初始化跑哪里去了?
         回忆一下,看看HashMap的构造方法:
     1 /**
     2      * Constructs an empty <tt>HashMap</tt> with the specified initial
     3      * capacity and load factor.
     4      *
     5      * @param  initialCapacity the initial capacity
     6      * @param  loadFactor      the load factor
     7      * @throws IllegalArgumentException if the initial capacity is negative
     8      *         or the load factor is nonpositive
     9      */
    10     public HashMap( int initialCapacity, float loadFactor) {
    11         if (initialCapacity < 0)
    12             throw new IllegalArgumentException( "Illegal initial capacity: " +
    13                                                initialCapacity);
    14         if (initialCapacity > MAXIMUM_CAPACITY)
    15             initialCapacity = MAXIMUM_CAPACITY;
    16         if (loadFactor <= 0 || Float.isNaN(loadFactor))
    17             throw new IllegalArgumentException( "Illegal load factor: " +
    18                                                loadFactor);
    19 
    20         // Find a power of 2 >= initialCapacity
    21         int capacity = 1;
    22         while (capacity < initialCapacity)
    23             capacity <<= 1;
    24 
    25         this.loadFactor = loadFactor;
    26         threshold = (int)(capacity * loadFactor);
    27         table = new Entry[capacity];
    28         init();
    29     }
    30 
    31     /**
    32      * Initialization hook for subclasses. This method is called
    33      * in all constructors and pseudo -constructors (clone, readObject)
    34      * after HashMap has been initialized but before any entries have
    35      * been inserted.  (In the absence of this method, readObject would
    36      * require explicit knowledge of subclasses.)
    37      */
    38     void init() {
    39     

      哦,明白了,init()在HashMap中是一个空方法,也就是给子类留的一个回调函数,ok,我们来看下LinkedHashMap对init()方法的实现吧。

     1     /**
     2      * Called by superclass constructors and pseudoconstructors (clone,
     3      * readObject) before any entries are inserted into the map.  Initializes
     4      * the chain.
     5      */
     6     void init() {
     7         // 初始化话header,将hash设置为-1,key、value、next设置为null
     8         header = new Entry<K,V>(-1, null, null, null);
     9         // header的before和after都指向header自身
    10         header.before = header. after = header ;
    11     
       init()方法看完了,看出点什么嘛?LinkedHashMap中维护的是个双向循环链表对不对?(什么?还不明白,去好好看看给jdk写注释系列之jdk1.6容器(2)-LinkedList源码解析
     
    4.增加
     
         LinkedHashMap没有重写put方法,只是重写了HashMap中被put方法调用的addEntry。
     1     /**
     2      * This override alters behavior of superclass put method. It causes newly
     3      * allocated entry to get inserted at the end of the linked list and
     4      * removes the eldest entry if appropriate.
     5      */
     6     void addEntry( int hash, K key, V value, int bucketIndex) {
     7         // 调用createEntry方法创建一个新的节点
     8         createEntry(hash, key, value, bucketIndex);
     9 
    10         // Remove eldest entry if instructed, else grow capacity if appropriate
    11         // 取出header后的第一个节点(因为header不保存数据,所以取header后的第一个节点)
    12         Entry<K,V> eldest = header.after ;
    13         // 判断是容量不够了是要删除第一个节点还是需要扩容
    14         if (removeEldestEntry(eldest)) {
    15             // 删除第一个节点(可实现FIFO、LRU策略的Cache)
    16             removeEntryForKey(eldest. key);
    17         } else {
    18             // 和HashMap一样进行扩容
    19             if (size >= threshold)
    20                 resize(2 * table.length );
    21         }
    22     }
    23 
    24     /**
    25      * This override differs from addEntry in that it doesn't resize the
    26      * table or remove the eldest entry.
    27      */
    28     void createEntry( int hash, K key, V value, int bucketIndex) {
    29         // 下面三行代码的逻辑是,创建一个新节点放到单向链表的头部
    30         // 取出数组bucketIndex位置的旧节点 
    31         HashMap.Entry<K,V> old = table[bucketIndex];
    32         // 创建一个新的节点,并将next指向旧节点
    33        Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
    34         // 将新创建的节点放到数组的bucketIndex位置
    35         table[bucketIndex] = e;
    36 
    37         // 维护双向链表,将新节点添加在双向链表header前面(链表尾部)
    38         e.addBefore( header);
    39         // 计数器size加1
    40         size++;
    41     }
    42 
    43     /**
    44      * 默认返回false,也就是不会进行元素删除了。如果想实现cache功能,只需重写该方法
    45      */
    46     protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    47         return false;
    48 }
      可以看到,在添加方法上,比HashMap中多了两个逻辑,一个是当Map容量不足后判断是删除第一个元素,还是进行扩容,另一个是维护双向链表。而在判断是否删除元素的时候,我们发现removeEldestEntry这个方法竟然是永远返回false,这什么鬼。。。哦,想了下,原来想要实现Cache功能,需要自己继承LinkedHashMap然后重写removeEldestEntry方法,这里默认提供的是容器的功能。
         
    5.删除
     
         LinkedHashMap没有重写remove方法,只是在实现了Entry类的recordRemoval方法,该方法是HashMap的提供的一个回调方法,在HashMap的remove方法进行回调,而LinkedHashMap中recordRemoval的主要当然是要维护双向链表了,返回上面去看下Entry类的recordRemoval方法吧。
     
    6.查找
     
         LinkedHashMap重写了get方法,但是确复用了HashMap中的getEntry方法,LinkedHashMap是在get方法中指加入了调用recoreAccess方法的逻辑,recoreAccess方法的目的当然也是维护双向链表了,具体逻辑返回上面去看下Entry类的recoreAccess方法吧。
    1 public V get(Object key) {
    2         Entry<K,V> e = (Entry<K,V>)getEntry(key);
    3         if (e == null)
    4             return null;
    5         e.recordAccess( this);
    6         return e.value ;
    7 }
    7.是否包含
     1 /**
     2      * Returns <tt>true</tt> if this map maps one or more keys to the
     3      * specified value.
     4      *
     5      * @param value value whose presence in this map is to be tested
     6      * @return <tt> true</tt> if this map maps one or more keys to the
     7      *         specified value
     8      */
     9     public boolean containsValue(Object value) {
    10         // Overridden to take advantage of faster iterator
    11         // 遍历双向链表,查找指定的value
    12         if (value==null) { 
    13             for (Entry e = header .after; e != header; e = e.after )
    14                 if (e.value ==null)
    15                     return true;
    16         } else {
    17             for (Entry e = header .after; e != header; e = e.after )
    18                 if (value.equals(e.value ))
    19                     return true;
    20         }
    21         return false;
    22  }
      LinkedHashMap对containsValue进行了重写,我们在HashMap中说过,HashMap的containsValue需要遍历整个hash表,这样是十分低效的。而LinkedHashMap中重写后,不再遍历hash表,而是遍历其维护的双向链表,这样在效率上难道就有所改善吗?我们分析下:hash表是由数组+单向链表组成,而由于使用hash算法,可能会导致散列不均匀,甚至数组的有些项是没有元素的(没有hash出对应的散列值),而LinkedHashMap的双向链表呢,是不存在空项的,所以LinkedHashMap的containsValue比HashMap的containsValue效率要好一些。
     
    8.遍历
     
     
         LinkedHashMap分析的也就差不多了,要理解LinkedHashMap一定要先对HashMap的实现有所理解,因为它是HashMap的一个子类。我们明白了,LinkedHashMap的两个主要作用,一个是可以实现FIFO、LUR策略的Cache功能,另一个就是提高了HashMap遍历的效率(其他功能的效率有所降低的。。。),当然LinkedHashMap的遍历会在Set中统一讲解
     
         在最后,让我们简单基于LInkedHashMap实现一个Cache功能吧,go!
     1 import java.util.LinkedHashMap;
     2 import java.util.Map;
     3 
     4 public class MyLocalCache extends LinkedHashMap<String, Object> {
     5 
     6         private static final long serialVersionUID = 7182816356402068265L;
     7 
     8         private static final int DEFAULT_MAX_CAPACITY = 1024;
     9 
    10         private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    11 
    12         private int maxCapacity;
    13 
    14         public enum Policy {
    15                FIFO, LRU
    16        }
    17 
    18         public MyLocalCache(Policy policy) {
    19                super(DEFAULT_MAX_CAPACITY, DEFAULT_LOAD_FACTOR, Policy.LRU .equals(policy));
    20                this.maxCapacity = DEFAULT_MAX_CAPACITY;
    21        }
    22 
    23         public MyLocalCache(int capacity, Policy policy) {
    24                super(capacity, DEFAULT_LOAD_FACTOR, Policy. LRU.equals(policy));
    25                this.maxCapacity = capacity;
    26        }
    27 
    28         @Override
    29         protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
    30                return this.size() > maxCapacity;
    31        }
    32        
    33         public static void main(String[] args) {
    34               MyLocalCache cache = new MyLocalCache(5, Policy.LRU);
    35               cache.put( "k1", "v1" );
    36               cache.put( "k2", "v2" );
    37               cache.put( "k3", "v3" );
    38               cache.put( "k4", "v4" );
    39               cache.put( "k5", "v5" );
    40               cache.put( "k6", "v6" );
    41               
    42               System. out.println("size=" + cache.size());
    43               
    44               System. out.println("----------------------" );
    45                for (Map.Entry<String, Object> entry : cache.entrySet()) {   
    46                 System. out.println(entry.getValue());   
    47             }
    48               
    49               System. out.println("----------------------" );
    50               
    51               System. out.println("k3=" + cache.get("k3"));
    52               
    53               System. out.println("----------------------" );
    54                for (Map.Entry<String, Object> entry : cache.entrySet()) {   
    55                 System. out.println(entry.getValue());   
    56             }
    57        }
    58 
    59 }

    参见:

    给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析

    给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器

  • 相关阅读:
    AOP的实现原理——动态代理
    反射
    代理模式
    sprig——jar包
    spring事务管理
    [c++基础]3/5原则--拷贝构造函数+拷贝赋值操作符
    [c++面试准备]--vector对象是如何增长的
    c++面试须知
    redhat--1
    kvm配置虚拟机[待整理]
  • 原文地址:https://www.cnblogs.com/tstd/p/5059589.html
Copyright © 2011-2022 走看看