LinkedHashMap源码解读与实现LRU缓存
LinkedHashMap继承HashMap
自定义全局变量header表示头节点
private transient Entry<K,V> header;
private final boolean accessOrder;
同时LinkedHashMap的自定义内部类Entry也继承了HashMap的Entry,但是新增了两个指针before和after。在哈希表的基础上又构成了双向链接列表。
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);
}
private void remove() {
before.after = after;
after.before = before;
}
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
构造方法:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
可以看到,默认调用了父类HashMap的构造方法,传入loadFactor,默认为 false,代表按照插入顺序进行迭代。可以显式设置为 true,代表以访问顺序进行迭代。
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;
init();
}
可以看到最后一步构造调用了init()方法,在HashMap中方法为空。而在LinkedHashMap中重写了这个方法,进一步实现了对其元素Entry的初始化。
@Override
void init() {
header = new Entry<>(-1, null, null, );
header.before = header.after = header;
}
在HashMap中,其Put方法如下。它的Entry.recordAccess是空方法。
过程如下:先哈希定位到数组中哈希桶位置,然后遍历entry链表,如果hash相同并且key相同,覆盖原value,更新成新value。如果对应哈希桶中没有元素,调用addEntry()方法
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++;
addEntry(hash, key, value, i);
return null;
}
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);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
而LinkedHashMap没有重写Put方法,而是重写了父Entry.recordAccess()方法。还有addEntry(),createEntry()方法
如果accessOrder为真,表示按照访问顺序迭代时,调用addBefore(lm.header)方法,表示将元素放到最后。(双向链表,头结点的before代表最后元素)
注意:这里调用情况是,同一个哈希桶对应的entry链表的调整。
/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
重写Entry.addBefore方法,实现链表的链接
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
重写的addEnty,createEntry方法。主要是重写createEntry方法,通过调用 e.addBefore(header)完成链表的链接。
/**
* This override alters behavior of superclass put method. It causes newly
* allocated entry to get inserted at the end of the linked list and
* removes the eldest entry if appropriate.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
/**
* This override differs from addEntry in that it doesn't resize the
* table or remove the eldest entry.
*/
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++;
}
LinkedHashMap 重写了父类 HashMap 的 get 方法,在每一次get后调用了我们前面分析过的recordAccess方法,将元素放置到最后一个,这样就形成了一个按访问顺序迭代的哈希链表。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。
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;
}
如何通过这种数据结构实现lru缓存?
该哈希映射的迭代顺序就是最后访问其条目的顺序,这种映射很适合构建 LRU 缓存。LinkedHashMap 提供了 removeEldestEntry(Map.Entry<K,V> eldest) 方法。该方法可以提供在每次添加新条目时移除最旧条目的实现程序,默认返回 false,这样,此映射的行为将类似于正常映射,即永远不能移除最旧的元素。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
LinkedHashMap在添加元素的时候会对此进行判断
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
通过重写removeEldestEntry可以实现自定义容量的linkedlist
LRU具体实现代码如下:
public class LRUCache<K, V> {
private static final float hashTableLoadFactor = 0.75f;
private LinkedHashMap<K, V> map;
private int cacheSize;
static Logger logger = LoggerFactory.getLogger(LRUCache.class);
public LRUCache(int cacheSize) {
this.cacheSize = cacheSize;
int hashTableCapacity = (int) Math
.ceil(cacheSize / hashTableLoadFactor) + 1;
map = new LinkedHashMap<K, V>(hashTableCapacity, hashTableLoadFactor, true) {
private static final long serialVersionUID = 1;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > LRUCache.this.cacheSize;
}
};
}
public synchronized V get(K key) {
return map.get(key);
}
public synchronized void put(K key, V value) {
map.put(key, value);
}
public synchronized Collection<Map.Entry<K, V>> getAll() {
return Lists.newArrayList(map.entrySet());
}
public static void main(String[] args) {
LRUCache<String, String> c = new LRUCache<String, String>(3);
c.put("1", "one");
c.put("2", "two");
c.put("3", "three");
c.put("4", "four");
if (c.get("2") == null) {
throw new Error();
}
c.put("5", "five");
c.put("4", "second four");
c.get("5");
for (Map.Entry<String, String> e : c.getAll()) {
logger.info(e.getKey() + " : " + e.getValue());
}
}