老马说编程
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]