LinkedHashMap

老马说编程

LinkedHashMap是HashMap的子类,但内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于这个双向链表中。

public class LinkedHashMap  extends HashMap  implements Map{
//双向链表的头
    private transient Entry header;

    private final boolean accessOrder;//按访问顺序
}

LinkedHashMap支持两种顺序

插入顺序

Map seqMap = new LinkedHashMap<>();

seqMap.put("c", 100);
seqMap.put("d", 200);
seqMap.put("a", 500);
seqMap.put("d", 300);

for(Entry entry : seqMap.entrySet()){
  System.out.println(entry.getKey()+" "+entry.getValue());
}

键是按照"c", "d", "a"的顺序插入的,修改"d"的值不会修改顺序,所以输出为:

c 100
d 300
a 500

用于: 希望保持原来的顺序
接受一些键值对作为输入,处理,然后输出,输出时希望保持原来的顺序。
在添加到Map前,键已经通过其他方式排好序了,这时,就没有必要使用TreeMap了,毕竟TreeMap的开销要大一些。

  • 一个配置文件,其中有一些键值对形式的配置项,但其中有一些键是重复的,希望保留最后一个值,但还是按原来的键顺序输出
  • 一个购物车,键为购买项目,值为购买数量,按用户添加的顺序保存。
  • 在从数据库查询数据放到内存时,可以使用SQL的order by语句让数据库对数据排序。

访问顺序

访问指get/put操作,其对应的键值对会移到链表末尾,

刚用过的节点.addBefore(header);放到header前面就是队尾
最末尾的是最近访问的,最开始的最久没被访问的

只有一个构造方法,可以指定按访问顺序,如下所示:

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder)//如果为true,就是访问顺序。

Map accessMap = new LinkedHashMap<>(16, 0.75f, true);

accessMap.put("c", 100);
accessMap.put("d", 200);
accessMap.put("a", 500);
accessMap.get("c");
accessMap.put("d", 300);

for(Entry entry : accessMap.entrySet()){
    System.out.println(entry.getKey()+" "+entry.getValue());
}

每次访问都会将该键值对移到末尾,所以输出为:

a 500
c 100
d 300

使用例子:

LRU缓存

全称是Least Recently Used,最近最少使用

默认情况下,LinkedHashMap没有对容量做限制,但它有一个protected方法:

protected boolean removeEldestEntry(Map.Entry eldest) {//参数是最久没被访问的键值对
    return false;//如果这个方法返回true,则这个最久的键值对就会被删除
}

子类可以重写该方法,在满足一定条件的情况,返回true。

下面就是一个简单的LRU缓存的实现:

public class LRUCache extends LinkedHashMap {
    private int maxEntries;
    
    public LRUCache(int maxEntries){
        super(16, 0.75f, true);
        this.maxEntries = maxEntries;
    }
    
    @Override
    protected boolean removeEldestEntry(Entry eldest) {
        return size() > maxEntries;
    }
}    

使用自制LRU:

LRUCache cache = new LRUCache<>(3);//限定缓存容量为3
//添加了4个键值对

cache.put("a", "abstract");
cache.put("b", "basic");//最久没被访问的键是"b",会被删除
cache.put("c", "call");
cache.get("a");

cache.put("d", "call");
System.out.println(cache);

输出为:

{c=call, a=abstract, d=call}

源码

public class LinkedHashMap  extends HashMap  implements Map{
//双向链表的头 头不存数据 永远是它 新值用它来确定位置,插它前面
    private transient final Entry header;

    private final boolean accessOrder;//按访问顺序
}

header的类型是这个内部类 双向链表

private static class Entry extends HashMap.Entry {
    Entry before, after;

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

    //从链表中移除
    private void remove() {
        before.after = after;
        after.before = before;
    }

//把本节点 放到existingEntry的前面
    private void addBefore(Entry existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

    @Override//在HashMap中实现为空
//hashMao.put实现中,如果键已经存在了 会调用
//LinkedHashMap重写了get方法中 会调用
    void recordAccess(HashMap m) {
        LinkedHashMap lm = (LinkedHashMap)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);//移到链表的末尾
        }
    }

    @Override//在HashMap中实现为空
    void recordRemoval(HashMap m) {
        remove();//节点从链表中也移除
    }
}

构造方法准备header

在HashMap的构造方法中,会调用init方法,是留给子类重写的,
LinkedHashMap的重写, 就是准备好head

@Override  //构造方法中调用
void init() {
    header = new Entry<>(-1, null, null, null);
    header.before = header.after = header;
}

put

如果是新key, 除了hashMap中是数组加这个节点,双向链表里面也要加,加到队尾,如果LinkedHashMap子类实现大小限制,就在这会删除最老数据
如果是原来就有的key, 除了hashMap中的操作, 还要把节点移动到队尾.通过调用双向链表里面的recordAccess方法

hashMap.put 如果是新的键,会调用addEntry方法添加节点, 整个方法在LinkedHashMap中重写,

先调用hashMap的addEntry,里面会调createEntry
removeEldestEntry加了一个钩子, LinkedHashMap的子类要是想删除老元素,保存数量的话就在这删

@Override
   void addEntry(int hash, K key, V value, int bucketIndex) {

        //先调用父类,里面会调用createEntry创建节点,
        super.addEntry(hash, key, value, bucketIndex);

       //满足条件就删除最老的(最前面的)元素
        Entry eldest = header.after;
        if (removeEldestEntry(eldest)) {//这个默认都是false 可以继承LinkedHashMap重写
            //删除最老的元素
            removeEntryForKey(eldest.key);
        }
    }

super.addEntry中调用的hashMap.createEntry也在LinkedHashMap中重写了

void createEntry(int hash, K key, V value, int bucketIndex) {

//数组中加上
    HashMap.Entry old = table[bucketIndex];
    Entry e = new Entry<>(hash, key, value, old);
    table[bucketIndex] = e;

//主要是增加拿这句:  链表中把这个新节点加到header的前面(就是双向链表的最后,是最新的数
    e.addBefore(header);

    size++;
}

例子:只有一个元素hello → 1

Map countMap = new LinkedHashMap<>();
countMap.put("hello", 1);

get
LinkedHashMap中整个重写了

public V get(Object key) {

    Entry e = (Entry)getEntry(key);
    if (e == null)
        return null;

//为了加了这么一句, 调整了顺序,把这节点放到队尾
    e.recordAccess(this);
    return e.value;
}

containsValue
比hashMap要快 ,因为只要遍历真的有值的节点. 遍历双向链表,而不是整个数组

public boolean containsValue(Object value) {
    // Overridden to take advantage of faster iterator
    if (value==null) {
        for (Entry e = header.after; e != header; e = e.after)
            if (e.value==null)
                return true;
    } else {
        for (Entry e = header.after; e != header; e = e.after)
            if (value.equals(e.value))
                return true;
    }
    return false;
}

LinkedHashSet

Map接口的实现类都有一个对应的Set接口的实现类,比如HashMap有HashSet
map的val不用, 只用key 就行了

public class LinkedHashSetextends HashSet
    implements Set, 
Cloneable, java.io.Serializable {
}

LinkedHashMap对应的set是LinkedHashSet
内部的Map的实现类是LinkedHashMap,可以保持插入顺序

Set set = new LinkedHashSet<>();
set.add("b");
set.add("c");
set.add("a");
set.add("c");

System.out.println(set);

输出为:

[b, c, a]

你可能感兴趣的:(LinkedHashMap)