LinkedHashMap

Map 综述(二):彻头彻尾理解 LinkedHashMap
图解LinkedHashMap原理

1. 概述

在刷leetcode的时候碰到了一个LRU题目,最开始自己是拿LinkdedList+HashMap做的,做完看题解发现Java为我们提供了LinkedHashMap,可以很好的支持LRU算法;

HashMap是无序的,内部使用Hash数组+链表+红黑树实现,因为元素存储在链表的位置通过Hash计算得到,在迭代遍历的顺序自然和存入时的顺序不同,HashMap的这一缺点往往会造成诸多不便,因为在有些场景中,我们确需要用到一个可以保持插入顺序的Map;

HashMap的子类LinkedHashMap解决了这个问题,虽然它增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:

  • 保持插入顺序的LinkedHashMap
  • 保持访问顺序的LinkedHashMap

其中LinkedHashMap的默认实现是按插入顺序排序的
在这里插入图片描述

2. 基本使用

LinkedHashMap继承了HashMap,所以它们有很多相似的地方,它也是提供了key-value的存储方式,并提供了putget方法来进行数据存取,其实和HashMap的使用差不多,其实在使用LinkedHashMap更关心的是顺序存取,可以看到LinkedHashMap提供了多个构造方法,我们先看空参的构造方法

public LinkedHashMap() {
    // 调用HashMap的构造方法,其实就是初始化Entry[] table
    super();
    // 这里是指是否基于访问排序,默认为false
    accessOrder = false;
}

首先调用了父类HashMap的构造方法,其实就是根据初始容量、负载因子去初始化Entry[] table

然后把accessOrder标志位设置为false,这跟存储的顺序有关了,前面说过LinkedHashMap存储数据的顺序分为两种:插入顺序和访问顺序

accessOrder设置为false,表示是插入顺序存储,这也是默认值,表示LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的

LinkedHashMap也提供了可以设置accessOrder的构造方法

/**
 * Constructs an empty <tt>LinkedHashMap</tt> instance with the
 * specified initial capacity, load factor and ordering mode.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @param  accessOrder     the ordering mode - <tt>true</tt> for
 *         access-order, <tt>false</tt> for insertion-order
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public LinkedHashMap(int initialCapacity,
         float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);   // 调用HashMap对应的构造函数
    this.accessOrder = accessOrder;    // 迭代顺序的默认值
}
// 第三个参数用于指定accessOrder值
Map<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
linkedHashMap.put("name1", "josan1");
linkedHashMap.put("name2", "josan2");
linkedHashMap.put("name3", "josan3");
System.out.println("开始时顺序:");
//name1 name2 name3
Set<Entry<String, String>> set = linkedHashMap.entrySet();
Iterator<Entry<String, String>> iterator = set.iterator();
while(iterator.hasNext()) {
    Entry entry = iterator.next();
    String key = (String) entry.getKey();
    String value = (String) entry.getValue();
    System.out.println("key:" + key + ",value:" + value);
}
System.out.println("通过get方法,导致key为name1对应的Entry到表尾");
linkedHashMap.get("name1");
Set<Entry<String, String>> set2 = linkedHashMap.entrySet();
Iterator<Entry<String, String>> iterator2 = set2.iterator();
//name2 name3 name1
while(iterator2.hasNext()) {
    Entry entry = iterator2.next();
    String key = (String) entry.getKey();
    String value = (String) entry.getValue();
    System.out.println("key:" + key + ",value:" + value);
}

3. LinkedHashMap的基本实现

本质上,LinkedHashMap = HashMap + 双向链表LinkedHashMap 在不对HashMap做任何改变的基础上,给HashMap的任意两个节点间加了两条连线(before指针和after指针),使这些节点形成一个双向链表。在LinkedHashMapMap中,所有put进来的Entry都保存在HashMap中,但由于它又额外定义了一个以head为头结点的空的双向链表,因此对于每次put进来Entry还会将其插入到双向链表的尾部

在这里插入图片描述

3.1 基本定义

我们看一下他的类定义和成员变量

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
	    /**
     * The head of the doubly linked list.
     */
    private transient Entry<K,V> header;  // 双向链表的表头元素

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    private final boolean accessOrder;  //true表示按照访问顺序迭代,false时表示按照插入顺序 
}

保存的Entry的定义如下:

/**
 * LinkedHashMap entry.
 */
private static class Entry<K,V> extends HashMap.Entry<K,V> {
    // These fields comprise the doubly linked list used for iteration.
    Entry<K,V> before, after;

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

他继承了HashMap.Entry

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
}

HashMap.Entry的基础上新增了beforeafter两个字段,他们是为双向链表服务的,特别需要注意的是,next用于维护HashMap各个桶中Entry的连接顺序,beforeafter用于维护Entry插入的先后顺序的

3.2 构造方法

我们再回到它的构造方法,随便看一个

public LinkedHashMap() {
    // 调用HashMap的构造方法,其实就是初始化Entry[] table
    super();
    // 这里是指是否基于访问排序,默认为false
    accessOrder = false;
}

你会发现他先调用了父类HashMap的构造方法,对于其他几种形式的构造方法也是如此,看一下HashMap的构造方法

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

   /**
     * Initialization hook for subclasses. This method is called
     * in all constructors and pseudo-constructors (clone, readObject)
     * after HashMap has been initialized but before any entries have
     * been inserted.  (In the absence of this method, readObject would
     * require explicit knowledge of subclasses.)
     */
    void init() {
    }

HashMap的构造函数都会在最后调用一个init()方法进行初始化,只不过这个方法在HashMap中是一个空实现,而在LinkedHashMap中重写了它用于初始化它所维护的双向链表

/**
 * Called by superclass constructors and pseudoconstructors (clone,
 * readObject) before any entries are inserted into the map.  Initializes
 * the chain.
 */
void init() {
    header = new Entry<K,V>(-1, null, null, null);
    header.before = header.after = header;
}

这样就初始化了一个LinkedHashMap,也可以说初始化了一个HashMap和一个双向链表

3.3 put方法

LinkedHashMap没有重写put方法,所以还是调用HashMap得到put方法,如下:

public V put(K key, V value) {
    // 对key为null的处理
    if (key == null)
        return putForNullKey(value);
    // 计算hash
    int hash = hash(key);
    // 得到在table中的index
    int i = indexFor(hash, table.length);
    // 遍历table[index],是否key已经存在,存在则替换,并返回旧值
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    
    modCount++;
    // 如果key之前在table中不存在,则调用addEntry,LinkedHashMap重写了该方法
    addEntry(hash, key, value, i);
    return null;
}

LinkedHashMap中,它对addEntry方法和EntryrecordAccess方法进行了重写

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 调用父类的addEntry,增加一个Entry到HashMap中
    super.addEntry(hash, key, value, bucketIndex);
    //双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
    Entry<K,V> eldest = header.after;
    //如果有必要,则删除掉该近期最少使用的节点,  
    //这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。  
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    }
}

这里调用了父类HashMap的addEntry方法,如下:

    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 扩容相关
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        // LinkedHashMap进行了重写
        createEntry(hash, key, value, bucketIndex);
    }

前面是扩容相关的代码,在上一篇HashMap解析中已经讲过了。这里主要看createEntry方法,LinkedHashMap进行了重写

void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMap.Entry<K,V> old = table[bucketIndex];
    // e就是新创建了Entry,会加入到table[bucketIndex]的表头
    Entry<K,V> e = new Entry<>(hash, key, value, old);
    table[bucketIndex] = e;
    // 把新创建的Entry,加入到双向链表中
    e.addBefore(header);
    size++;
}

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

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

从这里就可以看出,当put元素时,不但要把它加入到HashMap中去,还要加入到双向链表中

3.4 双向链表的重排序

key如果已经存在时,则进行更新Entryvalue,就是HashMapput方法中的如下代码:

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        // 重排序
        e.recordAccess(this);
        return oldValue;
    }
}

主要看e.recordAccess(this),这个方法跟访问顺序有关,HashMap是无序的,所以在HashMap.EntryrecordAccess方法是空实现,但是LinkedHashMap是有序的,LinkedHashMap.EntryrecordAccess方法进行了重写

void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    // 如果LinkedHashMap的accessOrder为true,则进行重排序
    // 比如前面提到LruCache中使用到的LinkedHashMap的accessOrder属性就为true
    if (lm.accessOrder) {
        lm.modCount++;
        // 把更新的Entry从双向链表中移除
        remove();
        // 再把更新的Entry加入到双向链表的表尾
        addBefore(lm.header);
    }
}

LinkedHashMap中,只有accessOrdertrue,即是访问顺序模式,才会put时对更新的Entry进行重新排序,而如果是插入顺序模式时,不会重新排序,这里的排序跟在HashMap中存储没有关系,只是指在双向链表中的顺序

3.5 get方法

LinkedHashMap有对get方法进行了重写,如下:

public V get(Object key) {
    // 调用genEntry得到Entry
    Entry<K,V> e = (Entry<K,V>)getEntry(key);
    if (e == null)
        return null;
    // 如果LinkedHashMap是访问顺序的,则get时,也需要重新排序
    e.recordAccess(this);
    return e.value;
}

先是调用了getEntry方法,通过key得到Entry;后面调用了LinkedHashMap.EntryrecordAccess方法重新排序,把getEntry移动到双向链表的表尾

3.6 扩容

LinkedHashMap扩容时,数据的再散列和HashMap是不一样的。

HashMap是先遍历旧table,再遍历旧table中每个元素的单向链表,取得Entry以后,重新计算hash值,然后存放到新table的对应位置。

LinkedHashMap是遍历的双向链表,取得每一个Entry,然后重新计算hash值,然后存放到新table的对应位置。

从遍历的效率来说,遍历双向链表的效率要高于遍历table,因为遍历双向链表是N次(N为元素个数);而遍历table是N+table的空余个数(N为元素个数)

4. LinkedHashMapLRU(Least recently used,最近最少使用)算法

到此为止,我们已经分析完了LinkedHashMap的存取实现,在addEntry方法中还调用了removeEldestEntry方法,该方法源码如下

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

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

使用LinkedHashMap实现一个符合LRU算法的数据结构,该结构最多可以缓存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(i), i);
        }
        System.out.println("LRU中key为h的Entry的值为: " + lru.get('h'));
        System.out.println("LRU的大小 :" + lru.size());
        System.out.println("LRU :" + lru);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值