java-map之LinkedHashMap

1.1概述

在使用HashMap的时候,可能会遇到需要按照当时put的顺序来进行哈希表的遍历。通过上篇对HashMap的了解,我们知道HashMap中不存在保存顺序的机制。本篇文章要介绍的LinkedHashMap专为此特性而生。在LinkedHashMap中可以保持两种顺序,分别是插入顺序和访问顺序,这个是可以在LinkedHashMap的初始化方法中进行指定的。相对于访问顺序,按照插入顺序进行编排被使用到的场景更多一些,所以默认是按照插入顺序进行编排。

1.2结构


所以该结构其实就是用双向链表加hashmap实现的,

1.3特点

  • key和value都允许为空
  • key重复会覆盖,value可以重复
  • 有序的
  • LinkedHashMap是非线程安全的

1.4详解

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

可以看到,LinkedHashMap继承了HashMap,实现了Map接口。
在属性上它比HashMap多了两个属性:

//链表的头结点
private transient Entry<K,V> header;
//该属性指取得键值对的方式,是个布尔值,false表示插入顺序,true表示访问顺序,也就是访问次数.
private final boolean accessOrder;

LinkedHashMap有五个构造器:

   	//用默认的初始容量和负载因子构建一个LinkedHashMap,取出键值对的方式是插入顺序
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    //构造一个指定初始容量的LinkedHashMap,取得键值对的顺序
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    //构造一个指定初始容量和负载因子,按照插入顺序的LinkedHashMap
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    //根据给定的初始容量,负载因子和键值对迭代顺序构建一个LinkedHashMap
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

    //通过给定的map创建一个LinkedHashMap,负载因子是默认值,迭代方式是插入顺序.
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
    }

其实主要区别还是在基本数据结构:

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

LinkedHashMap的Entry类继承了HashMap的Entry,并在此基础上进行了扩展,它拥有以下属性:

K key;
V value;
Entry<K, V> next;
int hash;
Entry<K, V> before;
NEtry<K, V> after;

LinkedHashMap的初始化实际上是先调用HashMap的初始化,最后调用LinkedHashMap的init()使得header初始化。

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

在header中,hash值为-1,其他都为null,也就是说这个header不在数组table中,其实它就是用来指示开源元素、标记结束元素的.header的目的就是为了记录第一个插入的元素是谁,在遍历的时候能够找到第一个元素
LinkedHashMap保存元素只是重写了写了父类put方法逻辑中调用的子方法addEntry()和createEntry()。

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        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++;
        //在这里调用的是LinkedHashMap重写后的addEntry方法,这之前的和HashMap一样
        addEntry(hash, key, value, i);
        return null;
    }

在最后一步调用LinkedHashMap的addEntry方法:

    void addEntry(int hash, K key, V value, int bucketIndex) {		
        //调用HashMap的addEntry,在map部分添加元素。
        super.addEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }

    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方法.
        createEntry(hash, key, value, bucketIndex);
    }

LinkedHashMap自己的createEntry()方法:

    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
    }

其实以上的操作和HashMap的操作没有什么不同,都是把新添加的节点放在了table[bucketIndex]位置上,差别在于LinkedHashMap还做了addBefore操作,而addBefore方法的目的就是让新的Entry和原链表生成一个双向链表.

        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

LinkedHashMap获得元素:

    public V get(Object key) {
        //调用父类的getEntry方法获取元素
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        //该方法用来记录访问顺序.
        e.recordAccess(this);
        return e.value;
    }

可以看到,在get方法中,它是调用了父类的getEntry方法来获取到元素,之后再调用自己的recordAccess()方法:

        void recordAccess(HashMap<K,V> m) {
            //因为在之前是转型为父类对象来获取entry的,所以这里要转回LinkedHashMap,判断获取数据的方式.
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            //当LinkedHashMap按照访问来排序时
            if (lm.accessOrder) {
                lm.modCount++;
                //移除当前节点
                remove();
                //将当前节点插入到头节点前面,即最后面.
                addBefore(lm.header);
            }
        }

调用父类的getEntry方法获取节点数据以后,再判断当前排序模式accessOrder,如果accessOrder是true,即按照访问顺序排序,那就将当前节点从链表中移除,然后再将当前节点插入到链表的尾部.

1.5应用

实现LRU算法:

public class LRUCache extends LinkedHashMap
{
    public LRUCache(int maxSize)
    {
        super(maxSize, 0.75F, true);
        maxElements = maxSize;
    }

    protected boolean removeEldestEntry(java.util.Map.Entry eldest)
    {
        return size() > maxElements;
    }

    private static final long serialVersionUID = 1L;
    protected int maxElements;
}

需要重写removeEldestEntry()方法,原来默认的该方法返回值为false,现在我们需要在容量满时移除最不常用的元素。

当然,也可以自定义实现该算法,和上面这种继承方式不一样的的地方在于LRUCache类的节点中包含的是三个成员变量,一个最大容量,一个双向链表,一个map集合。然后在LRUCache的增删改查中封装双向链表和map集合的api函数。

posted @ 2020-07-28 14:07  大嘤熊  阅读(257)  评论(0编辑  收藏  举报