LRUCache原理及HashMap LinkedHashMap内部实现原理
2015-11-27 17:11
555 查看
LRUCache概述
在开发android应用时,加载图片我们都会加上二级缓存,一级是内存缓存,二级是硬盘缓存。在内存缓存中我们一般采用SDK提供给我们的LRUCahce。LRU(least recently uses最近最少使用)是一种页面置换算法,最近最少使用的被替换掉。LRU算法是以时间轴为依据进行替换,而不是使用频率为依据替换。
常用的页面置换算法还有:
LFU(least frequently used 最不经常使用)是以单位时间内使用频率为依据进行替换,使用频率低的被替换掉。
FIFO(first in fist out 先进先出)按顺序进行替换。
LRUCache实现原理
先看一下LruCache的构造方法public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap<K, V>(0, 0.75f, true); }
可以看到LruCache内部维护了一个LinkedHashMap,LinkedHashMap继承自HashMap,所以我们先来研究下HashMap的实现原理。
HashMap的实现原理
还是先看下构造方法public HashMap(int capacity) { if (capacity < 0) { throw new IllegalArgumentException("Capacity: " + capacity); } if (capacity == 0) { @SuppressWarnings("unchecked") HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE; table = tab; threshold = -1; // Forces first put() to replace EMPTY_TABLE return; } if (capacity < MINIMUM_CAPACITY) { capacity = MINIMUM_CAPACITY; } else if (capacity > MAXIMUM_CAPACITY) { capacity = MAXIMUM_CAPACITY; } else { capacity = Collections.roundUpToPowerOfTwo(capacity); } makeTable(capacity); }
其中
Collections.roundUpToPowerOfTwo(capacity);方法是得到一个不小于所给数的2^n的数,就是说假如传入参数10,那么返回不小于10的2的n次方数就是16。具体的可以看这里
看下
makeTable方法
private HashMapEntry<K, V>[] makeTable(int newCapacity) { @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity]; table = newTable; threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity return newTable; }
可以看到,
HashMap的构造方法中生成了一个大小为2^n的
HashMapEntry的数组。我们看下
HashMapEntry的结构
static class HashMapEntry<K, V> implements Entry<K, V> { final K key; V value; final int hash; HashMapEntry<K, V> next;
其中有
map中的
key、value还有根据
key得到的
hash值和它的后继引用
next。其中
key value都是
Object。
既然是基于
Map的,那么我们看下
HashMap的
put方法和
get方法:
@Override public V put(K key, V value) { if (key == null) { //如果key为null调用该方法处理,需要注意的是HashMap允许key和value为null return putValueForNullKey(value); } //根据key得到hash值 int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; //根据hash值和内部维护数组的长度得到索引 int index = hash & (tab.length - 1); //如果index处的e不为null,通过循环不断遍历e的下一个元素 for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) { // 找到指定 key 与需要放入的 key 相等(hash 值相同 // 并且key的值相等)说明map中已经存在该数据,那么执行替换 if (e.hash == hash && key.equals(e.key)) { //该方法是空实现,注意LinkedHashMap中会用到 preModify(e); V oldValue = e.value; e.value = value; return oldValue; } } // No entry for (non-null) key is present; create one //index处的Entry为null,说明此处还没有数据Entry modCount++; if (size++ > threshold) { //大于数组的长度,扩容 大小x2 tab = doubleCapacity(); index = hash & (tab.length - 1); } addNewEntry(key, value, hash, index); return null; }
需要说明的是这句
int index = hash & (tab.length - 1);
首先
HashMap采用一种所谓的
Hash 算法来决定每个元素的存储位置。 Java中
Object提供一个
hashCode()方法,也就是说每个对象都有
hashCode()方法得到一个唯一的
hash值。
上面得到index的方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置——而 HashMap 底层数组的长度总是 2 的 n 次方,当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。
也就是说该方法返回的值
index总是在0–table.length之间循环。也就是说虽然key的hash值不同,但是他们在
HashMap中的存储位置是一样的。你可能有点疑惑,我们马上就会找到答案。
那么
put方法会把
key-value放入
HashMap,首先根据
key的
hash值得到在
HashMap中的存储位置,如果该位置上有数据,并且key的值相等,那么直接执行修改
value的操作。如果不想的会执行
addNewEntry(key, value, hash, index);方法。这个方法是干什么的呢,我们来看下
void addNewEntry(K key, V value, int hash, int index) { table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]); }
代码很简单,生成一个新的
Entry,并且把
key、value、hash相应赋值,然后最重要的是把
HashMap中
index位置的
Entry的引用付给了新
Entry的
next变量。也就是说在这里形成了一个Entry链,并且新的
Entry位于
Entry链的头部,然后把
index的位置上的值更改成了新生成的
Entry。
HashMap的
get方法跟
put方法的逻辑基本是一致的,感兴趣的读者可以自己查阅。
那么分析到这里我们也就理解了
HashMap的基本结构,用一张图表示
可以看到
HashMap中维护了一个数组,并且先用数组进行定位查找,然后用链表维护数组中同一位置上的
Entry。所以说
HashMap效率高不是没有道理的。
LinkedHashMap的实现原理
同样的先看下构造方法:public LinkedHashMap( int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); header = new LinkedEntry<K, V>(); this.accessOrder = accessOrder; }
在LruCache中是这样使用的:
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
注意
accessOrder的值为true,
首先调用父类也就是
HashMap的构造方法生成一个数组,然后new了一个
LinkedEntry做为
header,看下
LinkedEntry结构
static class LinkedEntry<K, V> extends HashMapEntry<K, V> { LinkedEntry<K, V> nxt; LinkedEntry<K, V> prv; /** Create the header entry */ LinkedEntry() { super(null, null, 0, null); nxt = prv = this; } /** Create a normal entry */ LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next, LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) { super(key, value, hash, next); this.nxt = nxt; this.prv = prv; } }
可以看到它继承自
HashMap中的
HashMapEntry,那么其中的数据就有: key、value、hash、next、nxt、prv。
可以看到
LinkedHashMap在
HashMap的基础上又增加了双向链表。也就是说LinkedHashMap的结构是数组+单向链表+双向链表。
那么这个双向链表是做什么用的呢。我们继续分析。
还记得
HashMap中的
preModify方法吗,
LinkedHashMap实现了该方法用于
put时替换
index位置已经存在并且
key值相等的节点:
@Override void preModify(HashMapEntry<K, V> e) { if (accessOrder) { // accessOrder为true makeTail((LinkedEntry<K, V>) e); } }
private void makeTail(LinkedEntry<K, V> e) { // Unlink e e.prv.nxt = e.nxt; e.nxt.prv = e.prv; // Relink e as tail LinkedEntry<K, V> header = this.header; LinkedEntry<K, V> oldTail = header.prv; e.nxt = header; e.prv = oldTail; oldTail.nxt = header.prv = e; modCount++; }
可以看到首先把e从双向链表中移除,然后放到链表的最尾端。可以看到其中的链表不仅是双向的而且是循环的。联系到
HashMap中的
put中的的操作,这一步只是修改
value值并且更改在双向循环链表中的位置。
再看下
LinkedHashMap中的
addNewEntry方法:
@Override void addNewEntry(K key, V value, int hash, int index) { LinkedEntry<K, V> header = this.header; // Remove eldest entry if instructed to do so. LinkedEntry<K, V> eldest = header.nxt; if (eldest != header && removeEldestEntry(eldest)) { remove(eldest.key); } // Create new entry, link it on to list, and put it into table LinkedEntry<K, V> oldTail = header.prv; LinkedEntry<K, V> newTail = new LinkedEntry<K,V>( key, value, hash, table[index], header, oldTail); table[index] = oldTail.nxt = header.prv = newTail; }
做的事情是生成一个新的
LinkedEntry,把它的引用放到双向循环列表的尾部,并且把它的引用赋值到数组中相应位置上的值。
再看下
get方法
@Override public V get(Object key) { /* * This method is overridden to eliminate the need for a polymorphic * invocation in superclass at the expense of code duplication. */ if (key == null) { HashMapEntry<K, V> e = entryForNullKey; if (e == null) return null; if (accessOrder) makeTail((LinkedEntry<K, V>) e); return e.value; } int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { if (accessOrder) makeTail((LinkedEntry<K, V>) e); return e.value; } } return null; }
逻辑跟
HashMap基本一样,根据
index在数组中定位,然后在单向列表中查找。也就是不管我们在
LinkedHashMap中执行
get方法或者
put方法,最后操作的那个
entry都是插到双向链表的尾部。那么头部的
nxt的那个
entry也就是最久未使用的
entry依据这个特性我们就可以实现
LRU实现缓存。
依据看一张图
然而分析道这里实际上并没有用到双向循环列表,那么这个双向循环列表到底是干什么用的呢?
我们看下LruCache中的put方法
public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } trimToSize(maxSize); return previous; }
public void trimToSize(int maxSize) { while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (size <= maxSize || map.isEmpty()) { break; } Map.Entry<K, V> toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } }
最后如果当前大小大于我们设定的最大缓存大小那么执行
trimToSize方法中根据
LinkedHashMap的
entrySet得到
iterator,然后执行
next方法,最后执行的是LinkedHashMap中的这些代码
LinkedEntry<K, V> next = header.nxt; LinkedEntry<K, V> lastReturned = null; int expectedModCount = modCount; public final boolean hasNext() { return next != header; } final LinkedEntry<K, V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); LinkedEntry<K, V> e = next; if (e == header) throw new NoSuchElementException(); next = e.nxt; return lastReturned = e; }
可以看到返回的是header.nxt,也就是双向循环链表中的第一个entry,那么也就是最久未使用的那个
entry。
总结
到此LruCache的基本原理我们都分析清楚了,并且梳理了HashMap和LinkedHashMap的内部实现原理,虽然我们在开发项目的时候对基本数据结构的内部原理不太关注,但是我们也应该学习其内部原理。毕竟基础才是最重要的。说到HashMap又想到了androidSDK中的SparseArray可以替代key为Integer类型的HashMap,感性却的可以看这篇文章相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories