LinkedHashMap
继承HashMap
所以拥有它的全部功能,但在HashMap
基础上新增了双向链表的功能,可以说LinkedHashMap
=HashMap
+双向链表,所以保证了其元素的有序性。默认是元素插入时顺序,支持按元素访问排序,所以可以用作LRU
缓存算法。
1.构造方法
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
accessOrder
表示是否根据访问次数排序,默认是false
,即根据插入顺序排序
调用了父类的构造函数,并初始化了header 节点
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
//LinkedHashMap对此方法进行了重写
init();
}
//初始化了header 节点
void init() {
header = new Entry<>(-1, null, null, null);
header.before = header.after = header;
}
//对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);
}
//省略部分代码
}
从构造方法我们可以看出,LinkedHashMap
在HashMap
的基础上,重写了Entry()
新增了两个属性before
和after
用来支撑双向链表数据结构。注意这两个属性和原属性next
没有任何关系 before
指向上一个插入的元素,after
指向后一个插入的元素。
2.put操作
//此方法是父类HashMap中的方法
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);
//数组中每个元素都是链表,JDK1.8后变成红黑数
//遍历链表,如果比对KEY的hash值,hash码和equals同时相同才表示该元素存在
//如果该元素已经存在集合中,则覆盖老值
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;
//这个是一个在HashMap中是一个空方法,在LinkedHashMap中对
//其进行了重写,后面再详细分析
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
put()
是直接继承的HashMap
的,在linkedHashMap
中并没有定义此方法,但是重写了addEntry(hash, key, value, i);
和recordAccess()
。我们一个个来看
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
//如果要删除双向链表中最后一个元素,将执
//行下面代码,这段代码后面再分析,实现LRU算法的关键所在
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
super.addEntry()
调用的是HashMap
中的方法
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);
}
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++;
}
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
我们这里重点分析addBefore()
,此方法是用来维护双向链表结构的
新增一个元素
新增两个元素
我们会发现header.after
永远指向第一个元素(最先插入的元素) ,header.before
永远指向最后一个元素,而且最后一个元素的after
永远指向头结点,这就是双向链表
我们再来分析addEntry(hash, key, value, i);
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
//如果要删除双向链表中最后一个元素,将执
//行下面代码,这段代码后面再分析,实现LRU算法的关键所在
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
取出header.after
即最早插入的元素,如果removeEldestEntry(eldest)
返回true
则删除这个元素,所以我们可以重写这个方法,根据元素个数是否超过预期返回true
或者false
来实现LRU
算法
前面说到如果一个已经存在的元素再次保存到容器中,如果是按访问次数排序,那么这个元素会被删除,并重新添加到链表的末尾,相当于刷新了这个元素的访问次数,主要执行Entry#recordAccess()
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
//这个删除元素
remove();
//重新添加该元素
addBefore(lm.header);
}
}
3.get操作
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
可以发现执行get()
操作后,如果是按访问顺序排序,那么将该元素删除后,重新添加该元素,即刷新钙元素的访问次数,前面已经分析过了
4.总结
所谓的LRU
算法就是(Least recently used,最近最少使用)。使用一个双向链表,记录插入元素的顺序,默认肯定是先插入的元素最先被淘汰,因为最老。如果被命中,即get()
操作后,删除该元素,重新再插入该元素,相当于更新了使用频率一样。依次类推。
5.附件,自定义LRU
缓存
public class MyLRU<K,V> extends LinkedHashMap<K,V>{
private int limit;
public MyLRU(int limit){
super(limit,0.75f,true);
this.limit = limit;
}
/***
* 重写此方法即可,当元素个数达到上限,返回true,就会删除一个最老元素
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size()>limit;
}
}
public class Main {
public static void main(String[] args) {
MyLRU<Integer,String> lru = new MyLRU<>(3);
lru.put(1,"1");
lru.put(2,"2");
lru.put(3,"3");
lru.get(1);
lru.put(4,"4");
for (Map.Entry entry :lru.entrySet()){
System.out.println(entry.getKey());
}
}
}
输出
3
1
4
因为执行了一次get(1)操作,那么元素1,就会被排到最后,此时容器中元素顺序为2,3,1,再新增元素4的时候,元素2就被被删掉,最终结果就是3,1,4