LinkedHashMap,链表和哈希的合体进化

Posted 毛奇志

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LinkedHashMap,链表和哈希的合体进化相关的知识,希望对你有一定的参考价值。

一、前言

二、LinkedHashMap概述

HashMap 是 Java Collection Framework 的重要成员,也是Map族(如下图所示)中我们最为常用的一种。不过遗憾的是,HashMap是无序的,也就是说,迭代HashMap所得到的元素顺序并不是它们最初放置到HashMap的顺序。HashMap的这一缺点往往会造成诸多不便,因为在有些场景中,我们确需要用到一个可以保持插入顺序的Map。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个子类LinkedHashMap。虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。

值得注意的是,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap和保持访问顺序的LinkedHashMap两种类型,其中,LinkedHashMap的默认实现是按插入顺序排序的。LinkedHashMap类继承体系,如下图:

本质上,HashMap和双向链表合二为一即是LinkedHashMap。所谓LinkedHashMap,其落脚点在HashMap,因此更准确地说,它是一个将所有Entry节点链入一个双向链表双向链表的HashMap。在LinkedHashMapMap中,所有put进来的Entry都保存在如下面第一个图所示的哈希表中,但由于它又额外定义了一个以head为头结点的双向链表(如下面第二个图所示),因此对于每次put进来Entry,除了将其保存到哈希表中对应的位置上之外,还会将其插入到双向链表的尾部,如下图:


对于上面两个图的解释:这里的LinkedHashMap一共包括六个节点,实线单向箭头表示单链表的next指针,虚线双向箭头表示双链表的before-after指针。去掉虚线双向箭头和每个Entry的before-after指针,就是一个HashMap,所以,LinkedHashMap = HashMap + LinkedList,即LinkedHashSet是链表和哈希的合体进化。

HashMap和双向链表的密切配合和分工合作造就了LinkedHashMap。特别需要注意的是,next用于维护HashMap各个桶中的Entry链,before、after用于维护LinkedHashMap的双向链表,虽然它们的作用对象都是Entry,但是各自分离。其中,HashMap与LinkedHashMap的Entry结构示意图如下图所示:

可以看到,由于LinkedHashMap是HashMap的子类,所以LinkedHashMap自然会拥有HashMap的所有特性。比如,LinkedHashMap也最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。LinkedHashMap 也是 Map 的一个非同步的实现,和HashMap一样,所以多线程下也是不安全的。此外,LinkedHashMap还可以用来实现LRU (Least recently used, 最近最少使用)算法。

三、LinkedHashMap在JDK中的定义

3.1 类结构定义

LinkedHashMap继承于HashMap,其在JDK中的定义为:

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

3.2 成员变量定义

与HashMap相比,LinkedHashMap增加了两个属性用于保证迭代顺序,分别是 双向链表头结点header 和 标志位accessOrder (值为true时,表示按照访问顺序迭代;值为false时,表示按照插入顺序迭代)。

// 双向链表的表头元素
private transient Entry<K,V> header;  

//true表示按照访问顺序迭代,false时表示按照插入顺序 
private final boolean accessOrder;  

3.3 成员方法定义

LinkedHashMap中并增加没有额外方法,即LinkedHashMap与HashMap在操作上大致相同,只是在实现细节上略有不同罢了。

3.4 基本元素Entry

LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了Entry。LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。特别需要注意的是,next用于维护HashMap各个桶中Entry的连接顺序,before、after用于维护Entry插入的先后顺序的,源代码如下:

private static class Entry<K,V> extends HashMap.Entry<K,V> 

    Entry<K,V> before, after;

    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) 
        super(hash, key, value, next);
    
    ...

形象地,HashMap与LinkedHashMap的Entry结构示意图如下图所示:

JDK1.7中,HashMap的基本元素是Entry,JDK1.8中,HashMap的基本元素是Node。

3.5 LinkedHashMap的数据结构

本质上,LinkedHashMap = HashMap + 双向链表,也就是说,HashMap和双向链表合二为一即是LinkedHashMap。也可以这样理解,LinkedHashMap 在不对HashMap做任何改变的基础上,给HashMap的任意两个节点间加了两条连线(before指针和after指针),使这些节点形成一个双向链表。

在LinkedHashMapMap中,所有put进来的Entry都保存在HashMap中,但由于它又额外定义了一个以head为头结点的空的双向链表,因此对于每次put进来Entry还会将其插入到双向链表的尾部。

四、LinkedHashMap的构造函数

LinkedHashMap 一共提供了五个构造函数,它们都是在HashMap的构造函数的基础上实现的,除了默认空参数构造方法,下面这个构造函数包含了大部分其他构造方法使用的参数,就不一一列举了。

4.1 指定容量初始化

LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) 该构造函数意在构造一个指定初始容量和指定负载因子的具有指定迭代顺序的LinkedHashMap,其源码如下:

public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) 
   super(initialCapacity, loadFactor); // super调用父类HashMap对应的构造函数
   this.accessOrder = accessOrder; // 迭代顺序的默认值

解释:super调用父类HashMap对应的构造函数,初始容量和负载因子是影响HashMap性能的两个重要参数。同样地,它们也是影响LinkedHashMap性能的两个重要参数。此外,LinkedHashMap 增加了双向链表头结点header和标志位accessOrder两个属性用于保证迭代顺序。

4.2 指定Map初始化

LinkedHashMap(Map<? extends K, ? extends V> m) 该构造函数意在构造一个与指定 Map 具有相同映射的 LinkedHashMap,其初始容量不小于16 (具体依赖于指定Map的大小),负载因子是 0.75,是 Java Collection Framework 规范推荐提供的,其源码如下:

public LinkedHashMap(Map<? extends K, ? extends V> m) 
    super(m);       // 调用HashMap对应的构造函数
    accessOrder = false;    // 迭代顺序的默认值

4.3 init()方法

从上面的五种构造函数我们可以看出,无论采用何种方式创建LinkedHashMap,其都会调用HashMap相应的构造函数。事实上,不管调用HashMap的哪个构造函数,HashMap的构造函数都会在最后调用一个init()方法进行初始化,只不过这个方法在HashMap中是一个空实现,而在LinkedHashMap中重写了它用于初始化它所维护的双向链表。HashMap的参数为空的构造函数以及init方法的源码如下:

public HashMap() 
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();


void init() 

在LinkedHashMap中,它重写了init方法以便初始化双向列表,源码如下:

LinkedHashMap.java

void init() 
   header = new Entry<K,V>(-1, null, null, null);
   header.before = header.after = header;

因此,我们在创建LinkedHashMap的同时就会不知不觉地对双向链表进行初始化。

五、LinkedHashMap的快速存取

我们知道,在HashMap中最常用的两个操作就是:put(Key,Value) 和 get(Key)。同样地,在 LinkedHashMap 中最常用的也是这两个操作。

(1) 对于put(Key,Value)方法而言,LinkedHashMap完全继承了HashMap的 put(Key,Value) 方法,只是对put(Key,Value)方法所调用的recordAccess方法和addEntry方法进行了重写;
(2) 对于get(Key)方法而言,LinkedHashMap则直接对它进行了重写。

5.1 LinkedHashMap的存储实现

LinkedHashMap的存储实现通过put(key, vlaue)来完成,LinkedHashMap没有对 put(key,vlaue) 方法进行任何直接的修改,完全继承了HashMap的 put(Key,Value) 方法,其源码如下:

public V put(K key, V value) 

    //当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置 
    if (key == null)
        return putForNullKey(value); 

    //根据key的hashCode计算hash值
    int hash = hash(key.hashCode());           

    //计算该键值对在数组中的存储位置(哪个桶)
    int i = indexFor(hash, table.length);              

    //在table的第i个桶上进行迭代,寻找 key 保存的位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next)       
        Object k;
        //判断该条链上是否存在hash值相同且key值相等的映射,若存在,则直接覆盖 value,并返回旧value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this); // LinkedHashMap重写了Entry中的recordAccess方法--- (1)    
            return oldValue;    // 返回旧值
        
    

    modCount++; //修改次数增加1,快速失败机制

    //原Map中无该映射,将该添加至该链的链头
    addEntry(hash, key, value, i);  // LinkedHashMap重写了HashMap中的createEntry方法 ---- (2)    
    return null;

上述源码反映了LinkedHashMap与HashMap保存数据的过程。特别地,在LinkedHashMap中,它对addEntry方法和Entry的recordAccess方法进行了重写。下面我们对比地看一下LinkedHashMap 和HashMap的addEntry方法的具体实现:

//  LinkedHashMap中的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex)    

    //创建新的Entry,并插入到LinkedHashMap中  
    createEntry(hash, key, value, bucketIndex);  // 重写了HashMap中的createEntry方法

    //双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
    Entry<K,V> eldest = header.after;  
    //如果有必要,则删除掉该近期最少使用的节点,  
    //这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。  
    if (removeEldestEntry(eldest))   
        removeEntryForKey(eldest.key);  
     else   
        //扩容到原来的2倍  
        if (size >= threshold)  
            resize(2 * table.length);  
      
 

----------------------------------------------------------------------

// HashMap中的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) 
    //获取bucketIndex处的Entry
    Entry<K,V> e = table[bucketIndex];

    //将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

    //若HashMap中元素的个数超过极限了,则容量扩大两倍
    if (size++ >= threshold)
        resize(2 * table.length);

由于LinkedHashMap本身维护了插入的先后顺序,因此其可以用来做缓存,14~19行的操作就是用来支持LRU算法的,这里暂时不用去关心它。此外,在LinkedHashMap的addEntry方法中,它重写了HashMap中的createEntry方法,我们接着看一下createEntry方法:

void createEntry(int hash, K key, V value, int bucketIndex)  
    // 向哈希表中插入Entry,这点与HashMap中相同 
    //创建新的Entry并将其链入到数组对应桶的链表的头结点处, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     

    //在每次向哈希表插入Entry的同时,都会将其插入到双向链表的尾部,  
    //这样就按照Entry插入LinkedHashMap的先后顺序来迭代元素(LinkedHashMap根据双向链表重写了迭代器)
    //同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,也符合LRU算法的实现  
    e.addBefore(header);  
    size++;  
  

由以上源码我们可以知道,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。其中,addBefore方法本质上是一个双向链表的插入操作,其源码如下:

//在双向链表中,将当前的Entry插入到existingEntry(header)的前面  
private void addBefore(Entry<K,V> existingEntry)   
    after  = existingEntry;  
    before = existingEntry.before;  
    before.after = this;  
    after.before = this;  
  

到此为止,我们分析了在LinkedHashMap中put一条键值对的完整过程。

总的来说,相比HashMap而言,LinkedHashMap在向哈希表添加一个键值对的同时,也会将其链入到它所维护的双向链表中,以便设定迭代顺序。

5.2 LinkedHashMap的扩容操作

在HashMap中,我们知道随着HashMap中元素的数量越来越多,发生碰撞的概率将越来越大,所产生的子链长度就会越来越长,这样势必会影响HashMap的存取速度。为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理, 在LinkedHashMap集合框架中,这个扩容操作由resize()方法来完成,在put()操作中调用,至于扩容临界点,就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)。

但是,扩容是一个非常耗时的过程,因为它需要重新计算这些元素在新table数组中的位置并进行复制处理。所以,如果我们能够提前预知HashMap中元素的个数,那么在构造HashMap时预设元素的个数能够有效的提高HashMap的性能。同样的问题也存在于LinkedHashMap中,因为LinkedHashMap本来就是一个HashMap,只是它还将所有Entry节点链入到了一个双向链表中。LinkedHashMap完全继承了HashMap的resize()方法,只是对它所调用的transfer方法进行了重写。我们先看resize()方法源码:

void resize(int newCapacity) 
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;

    // 若 oldCapacity 已达到最大值,直接将 threshold 设为 Integer.MAX_VALUE
    if (oldCapacity == MAXIMUM_CAPACITY)   
        threshold = Integer.MAX_VALUE;
        return;             // 直接返回
    

    // 否则,创建一个更大的数组
    Entry[] newTable = new Entry[newCapacity];

    //将每条Entry重新哈希到新的数组中
    transfer(newTable);  //LinkedHashMap对它所调用的transfer方法进行了重写

    table = newTable;
    threshold = (int)(newCapacity * loadFactor);  // 重新设定 threshold

从上面代码中我们可以看出,Map扩容操作的核心在于重哈希。所谓重哈希是指重新计算原HashMap中的元素在新table数组中的位置并进行复制处理的过程。鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对重哈希过程(transfer方法)进行了重写,源码如下:

void transfer(HashMap.Entry[] newTable) 
    int newCapacity = newTable.length;
    // 与HashMap相比,借助于双向链表的特点进行重哈希使得代码更加简洁
    for (Entry<K,V> e = header.after; e != header; e = e.after) 
        int index = indexFor(e.hash, newCapacity);   // 计算每个Entry所在的桶
        // 将其链入桶中的链表
        e.next = newTable[index];
        newTable[index] = e;   
    

如上述源码所示,LinkedHashMap借助于自身维护的双向链表轻松地实现了重哈希操作。

5.3 LinkedHashMap的读取实现

LinkedHashMap的读取实现由get()方法来完成,这个get()方法是重写了父类HashMap的,因为每个Entry多了before和after两个元素,所以不能直接继承父类的get()拿来用,需要重写一下,源码如下:

public V get(Object key) 
    // 根据key获取对应的Entry,若没有这样的Entry,则返回null
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null)      // 若不存在这样的Entry,直接返回
        return null;
    e.recordAccess(this);
    return e.value;


// HashMap 中的方法
final Entry<K,V> getEntry(Object key) 
    if (size == 0) 
        return null;
    

    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) 
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    
    return null;

在LinkedHashMap的get方法中,通过HashMap中的getEntry方法获取Entry对象。注意这里的recordAccess(this)方法,如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做;如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将e移到链表的末尾处。调用LinkedHashMap的get(Object key)方法后,若返回值是 NULL,则也存在如下两种可能:

(1) 该key对应的值就是null;
(2) HashMap 中不存在该key。

5.4 LinkedHashMap存取小结

LinkedHashMap的存取过程基本与HashMap基本类似,只是在细节实现上稍有不同,这是由
LinkedHashMap本身的特性所决定的,因为它要额外维护一个双向链表用于保持迭代顺序。

第一,在插入put()操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。

第二,在扩容resize()操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写。

第三,在读取get()操作上,LinkedHashMap中重写了HashMap中的get方法,通过HashMap中的getEntry方法获取Entry对象。在此基础上,进一步获取指定键对应的值。

六、LinkedHashMap与LRU算法

LinkedHashMap的存取实现与HashMap大体相同,但是,LinkedHashMap区别于HashMap最大的一个不同点是,前者是有序的,而后者是无序的。为此,LinkedHashMap增加了两个属性用于保证顺序,分别是双向链表头结点header和标志位accessOrder。其中,header是LinkedHashMap所维护的双向链表的头结点,而accessOrder用于决定具体的迭代顺序。

当accessOrder标志位为true时,表示双向链表中的元素按照访问的先后顺序排列,可以看到,虽然Entry插入链表的顺序依然是按照其put到LinkedHashMap中的顺序,但put和get方法均有调用recordAccess方法(put方法在key相同时会调用)。

recordAccess方法判断accessOrder是否为true

(1) 当标志位accessOrder的值为true时,则将当前访问的Entry(包括put进来的Entry和get出来的Entry)移到双向链表的尾部,如下:

① get方法通过调用recordAccess方法来实现,将当前访问的Entry移到双向链表的尾部;
② put方法在覆盖已有key的情况下,也是通过调用recordAccess方法来实现,将当前访问的Entry移到双向链表的尾部;
③ put方法在在插入新的Entry时,则是通过createEntry中的addBefore方法来实现,将当前访问的Entry移到双向链表的尾部。

(2) 当标志位accessOrder的值为false时,表示双向链表中的元素按照Entry插入LinkedHashMap到中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序。因此,当标志位accessOrder的值为false时,虽然也会调用recordAccess方法,但不做任何操作。

LRU,英文是Least Recently Used,即最近最少使用算法,在LinkedHashMap类中,当标志位accessOrder的值为true时,则将当前访问的Entry(包括put进来的Entry和get出来的Entry)移到双向链表的尾部,这样多次操作之后,链表前面的就是最近最少使用的了,下面看看具体是怎样完成这个LRU的。

6.1 put操作与标志位accessOrder

// 将key/value添加到LinkedHashMap中      
public V put(K key, V value)       
    // 若key为null,则将该键值对添加到table[0]中。      
    if (key == null)      
        return putForNullKey(value);      
    // 若key不为null,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。      
    int hash = hash(key.hashCode());      
    int i = indexFor(hash, table.length);      
    for (Entry<K,V> e = table[i]; e != null; e = e.next)       
        Object k;      
        // 若key对已经存在,则用新的value取代旧的value     
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))       
            V oldValue = e.value;      
            e.value = value;      
            e.recordAccess(this);      
            return oldValue;      
              
          

    // 若key不存在,则将key/value键值对添加到table中      
    modCount++;    
    //将key/value键值对添加到table[i]处    
    addEntry(hash, key, value, i);      
    return null;      
      

从上述源码我们可以看到,当要put进来的Entry的key在哈希表中已经存在时,会调用Entry的recordAccess方法;当要put进来的Entry的key在哈希表中不存在时,则会调用addEntry方法将新的Entry插入到对应桶的单链表的头部。那么,这个recordAccess方法具体干了什么,且看源码:

void recordAccess(HashMap<K,V> m)   
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
    //如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部  
    //如果是按照插入的先后顺序排序,则不做任何事情  
    if (lm.accessOrder)   
        lm.modCount++;  
        //移除当前访问的Entry  
        remove();  
        //将当前访问的Entry插入到链表的尾部  
        addBefore(lm.header);  
        
   

LinkedHashMap重写了HashMap中的recordAccess方法(HashMap中该方法为空),当调用父类的put方法时,在发现key已经存在时,会调用该方法;当调用自己的get方法时,也会调用到该方法。

解释“当调用父类的put方法时”
因为LinkedHashMap的put操作是直接调用父类HashMap的put方法,所以这里这么说,其实也就是调用LinkedHashMap自己的put方法的意思。

recordAccess()方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部。也就是说,当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾;当accessOrder为默认值false时,从源码中可以看出recordAccess方法什么也不会做。现在,我们再看一下put()中调用的addEntry()方法:

// LinkedHashMap中的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) 
    //创建新的Entry,并插入到LinkedHashMap中  
    createEntry(hash, key, value, bucketIndex);  // 重写了HashMap中的createEntry方法

    //双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
    Entry<K,V> eldest = header.after;  
    //如果有必要,则删除掉该近期最少使用的节点,  
    //这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。  
    if (removeEldestEntry(eldest))   
        removeEntryForKey(eldest.key);  
     else   
        //扩容到原来的2倍  
        if (size >= threshold)  
            resize(2 * table.length);  
      
 

void createEntry(int hash, K key, V value, int bucketIndex)  
    // 向哈希表中插入Entry,这点与HashMap中相同 
    //创建新的Entry并将其链入到数组对应桶的链表的头结点处, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     

    //在每次向哈希表插入Entry的同时,都会将其插入到双向链表的尾部,  
    //这样就按照Entry插入LinkedHashMap的先后顺序来迭代元素(LinkedHashMap根据双向链表重写了迭代器)
    //同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,也符合LRU算法的实现  
    e.addBefore(header);  
    size++;  

同样是将新的Entry链入到table中对应桶中的单链表中,但可以在createEntry方法中看出,同时也会把新put进来的Entry插入到了双向链表的尾部。
  
从插入顺序的层面来说,新的Entry插入到双向链表的尾部可以实现按照插入的先后顺序来迭代Entry,而从访问顺序的层面来说,新put进来的Entry又是最近访问的Entry,也应该将其放在双向链表的尾部。在上面的addEntry方法中还调用了removeEldestEntry方法,该方法源码如下:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) 
    return false;

removeEldestEntry()方法是用来被重写的,一般地,如果用LinkedHashMap实现LRU算法,就要重写该方法。比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中putEntry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)。

6.2 get操作与标志位accessOrder

在LinkedHashMap中进行读取操作时,一样也会调用recordAccess方法,如下:

public V get(Object key) 
    // 根据key获取对应的Entry,若没有这样的Entry,则返回null
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null)      // 若不存在这样的Entry,直接返回
        return null;
    e.recordAccess(this);
    return e.value;

同样,这里的recordAccess也是,如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部(因为put和get都属于访问操作);如果是按照插入的先后顺序排序,则recordAccess()不做任何事情。

void recordAccess(HashMap<K,V> m)   
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
    //如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部  
    //如果是按照插入的先后顺序排序,则不做任何事情  
    if (lm.accessOrder)   
        lm.modCount++;  
        //移除当前访问的Entry  
        remove();  
        //将当前访问的Entry插入到链表的尾部  
        addBefore(lm.header);  
        
   

6.3 LinkedListMap与LRU算法小结

使用LinkedHashMap实现LRU的必要前提是将accessOrder标志位设为true以便开启按访问顺序排序的模式。我们可以看到,无论是put方法还是get方法,都会导致目标Entry成为最近访问的Entry,因此就把该Entry加入到了双向链表的末尾:

(1) get方法通过调用recordAccess方法来实现;
(2) put方法在覆盖已有key的情况下,也是通过调用recordAccess方法来实现;
(3) put方法在在插入新的Entry时,则是通过createEntry中的addBefore方法来实现。

这样,我们便把最近使用的Entry放入到了双向链表的后面。多次操作后,双向链表前面的Entry便是最近没有使用的,这样当节点个数满的时候,删除最前面的Entry(head后面的那个Entry)即可,因为它就是最近最少使用的Entry,完美。

6.4 使用LinkedHashMap实现LRU算法

如下所示,这里使用LinkedHashMap实现一个符合LRU算法的数据结构,该结构最多可以缓存6个元素,但元素个数超过6个,会自动删除最近最久没有被使用的元素,如下所示:

public class LRU<K,V> extends LinkedHashMap<K, V> implements Map<K, V>

    private static final long serialVersionUID = 1L;

    public LRU(int initialCapacity,
             float loadFactor,
                        boolean accessOrder) 
        super(initialCapacity, loadFactor, accessOrder);
    

    /** 
     * @description 重写LinkedHashMap中的removeEldestEntry方法,当LRU中元素个数超过6个时,
     *              删除最不经常使用的元素
     * @author rico       
     * @created 2017年5月12日 上午11:32:51      
     * @param eldest
     * @return     
     * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)     
     */  
    @Override
    protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) 
        // TODO Auto-generated method stub
        if(size() > 6)
            return true;
        
        return false;
    

    public static void main(String[] args) 

        LRU<Character, Integer> lru = new LRU<Character, Integer>(
                16, 0.75f, true);

        String s = "abcdefghijkl";
        for (int i = 0; i < s.length(); i++) 
            lru.put(s.charAt(<

以上是关于LinkedHashMap,链表和哈希的合体进化的主要内容,如果未能解决你的问题,请参考以下文章

LinkedHashMap,链表和哈希的合体进化

LinkedHashMap

LinkedHashMap

LinkedHashMapt深入学习

Map集合

Java基础Map