Map 综述(二):彻头彻尾理解 LinkedHashMap
图解LinkedHashMap原理
1. 概述
在刷leetcode的时候碰到了一个LRU
题目,最开始自己是拿LinkdedList+HashMap
做的,做完看题解发现Java
为我们提供了LinkedHashMap
,可以很好的支持LRU
算法;
HashMap
是无序的,内部使用Hash
数组+链表+红黑树实现,因为元素存储在链表的位置通过Hash
计算得到,在迭代遍历的顺序自然和存入时的顺序不同,HashMap
的这一缺点往往会造成诸多不便,因为在有些场景中,我们确需要用到一个可以保持插入顺序的Map
;
HashMap
的子类LinkedHashMap
解决了这个问题,虽然它增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap
分为:
- 保持插入顺序的
LinkedHashMap
- 保持访问顺序的
LinkedHashMap
其中LinkedHashMap
的默认实现是按插入顺序排序的
2. 基本使用
LinkedHashMap
继承了HashMap
,所以它们有很多相似的地方,它也是提供了key-value
的存储方式,并提供了put
和get
方法来进行数据存取,其实和HashMap
的使用差不多,其实在使用LinkedHashMap
更关心的是顺序存取,可以看到LinkedHashMap
提供了多个构造方法,我们先看空参的构造方法
public LinkedHashMap() {
// 调用HashMap的构造方法,其实就是初始化Entry[] table
super();
// 这里是指是否基于访问排序,默认为false
accessOrder = false;
}
首先调用了父类HashMap
的构造方法,其实就是根据初始容量、负载因子去初始化Entry[] table
然后把accessOrder
标志位设置为false
,这跟存储的顺序有关了,前面说过LinkedHashMap
存储数据的顺序分为两种:插入顺序和访问顺序
accessOrder
设置为false
,表示是插入顺序存储,这也是默认值,表示LinkedHashMap
中存储的顺序是按照调用put
方法插入的顺序进行排序的
LinkedHashMap
也提供了可以设置accessOrder
的构造方法
/**
* Constructs an empty <tt>LinkedHashMap</tt> instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor); // 调用HashMap对应的构造函数
this.accessOrder = accessOrder; // 迭代顺序的默认值
}
// 第三个参数用于指定accessOrder值
Map<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
linkedHashMap.put("name1", "josan1");
linkedHashMap.put("name2", "josan2");
linkedHashMap.put("name3", "josan3");
System.out.println("开始时顺序:");
//name1 name2 name3
Set<Entry<String, String>> set = linkedHashMap.entrySet();
Iterator<Entry<String, String>> iterator = set.iterator();
while(iterator.hasNext()) {
Entry entry = iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
System.out.println("通过get方法,导致key为name1对应的Entry到表尾");
linkedHashMap.get("name1");
Set<Entry<String, String>> set2 = linkedHashMap.entrySet();
Iterator<Entry<String, String>> iterator2 = set2.iterator();
//name2 name3 name1
while(iterator2.hasNext()) {
Entry entry = iterator2.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
3. LinkedHashMap
的基本实现
本质上,LinkedHashMap = HashMap + 双向链表
,LinkedHashMap
在不对HashMap
做任何改变的基础上,给HashMap
的任意两个节点间加了两条连线(before
指针和after
指针),使这些节点形成一个双向链表。在LinkedHashMapMap
中,所有put
进来的Entry
都保存在HashMap
中,但由于它又额外定义了一个以head
为头结点的空的双向链表,因此对于每次put
进来Entry还会将其插入到双向链表的尾部
3.1 基本定义
我们看一下他的类定义和成员变量
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
/**
* The head of the doubly linked list.
*/
private transient Entry<K,V> header; // 双向链表的表头元素
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
private final boolean accessOrder; //true表示按照访问顺序迭代,false时表示按照插入顺序
}
保存的Entry
的定义如下:
/**
* LinkedHashMap entry.
*/
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
他继承了HashMap.Entry
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
在HashMap.Entry
的基础上新增了before
和after
两个字段,他们是为双向链表服务的,特别需要注意的是,next
用于维护HashMap
各个桶中Entry
的连接顺序,before
、after
用于维护Entry
插入的先后顺序的
3.2 构造方法
我们再回到它的构造方法,随便看一个
public LinkedHashMap() {
// 调用HashMap的构造方法,其实就是初始化Entry[] table
super();
// 这里是指是否基于访问排序,默认为false
accessOrder = false;
}
你会发现他先调用了父类HashMap
的构造方法,对于其他几种形式的构造方法也是如此,看一下HashMap
的构造方法
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
/**
* Initialization hook for subclasses. This method is called
* in all constructors and pseudo-constructors (clone, readObject)
* after HashMap has been initialized but before any entries have
* been inserted. (In the absence of this method, readObject would
* require explicit knowledge of subclasses.)
*/
void init() {
}
HashMap
的构造函数都会在最后调用一个init()
方法进行初始化,只不过这个方法在HashMap
中是一个空实现,而在LinkedHashMap
中重写了它用于初始化它所维护的双向链表
/**
* Called by superclass constructors and pseudoconstructors (clone,
* readObject) before any entries are inserted into the map. Initializes
* the chain.
*/
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}
这样就初始化了一个LinkedHashMap
,也可以说初始化了一个HashMap
和一个双向链表
3.3 put
方法
LinkedHashMap
没有重写put
方法,所以还是调用HashMap
得到put
方法,如下:
public V put(K key, V value) {
// 对key为null的处理
if (key == null)
return putForNullKey(value);
// 计算hash
int hash = hash(key);
// 得到在table中的index
int i = indexFor(hash, table.length);
// 遍历table[index],是否key已经存在,存在则替换,并返回旧值
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++;
// 如果key之前在table中不存在,则调用addEntry,LinkedHashMap重写了该方法
addEntry(hash, key, value, i);
return null;
}
LinkedHashMap
中,它对addEntry
方法和Entry
的recordAccess
方法进行了重写
void addEntry(int hash, K key, V value, int bucketIndex) {
// 调用父类的addEntry,增加一个Entry到HashMap中
super.addEntry(hash, key, value, bucketIndex);
//双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
Entry<K,V> eldest = header.after;
//如果有必要,则删除掉该近期最少使用的节点,
//这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
这里调用了父类HashMap的addEntry方法,如下:
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);
}
// LinkedHashMap进行了重写
createEntry(hash, key, value, bucketIndex);
}
前面是扩容相关的代码,在上一篇HashMap解析中已经讲过了。这里主要看createEntry方法,LinkedHashMap进行了重写
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
// e就是新创建了Entry,会加入到table[bucketIndex]的表头
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
// 把新创建的Entry,加入到双向链表中
e.addBefore(header);
size++;
}
由以上源码我们可以知道,在LinkedHashMap
中向哈希表中插入新Entry
的同时,还会通过Entry
的addBefore
方法将其链入到双向链表中。其中,addBefore
方法本质上是一个双向链表的插入操作,其源码如下
//在双向链表中,将当前的Entry插入到existingEntry(header)的前面
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
从这里就可以看出,当put
元素时,不但要把它加入到HashMap
中去,还要加入到双向链表中
3.4 双向链表的重排序
当key
如果已经存在时,则进行更新Entry
的value
,就是HashMap
的put
方法中的如下代码:
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;
}
}
主要看e.recordAccess(this)
,这个方法跟访问顺序有关,HashMap
是无序的,所以在HashMap.Entry
的recordAccess
方法是空实现,但是LinkedHashMap
是有序的,LinkedHashMap.Entry
对recordAccess
方法进行了重写
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
// 如果LinkedHashMap的accessOrder为true,则进行重排序
// 比如前面提到LruCache中使用到的LinkedHashMap的accessOrder属性就为true
if (lm.accessOrder) {
lm.modCount++;
// 把更新的Entry从双向链表中移除
remove();
// 再把更新的Entry加入到双向链表的表尾
addBefore(lm.header);
}
}
在LinkedHashMap
中,只有accessOrder
为true
,即是访问顺序模式,才会put
时对更新的Entry
进行重新排序,而如果是插入顺序模式时,不会重新排序,这里的排序跟在HashMap
中存储没有关系,只是指在双向链表中的顺序
3.5 get
方法
LinkedHashMap
有对get
方法进行了重写,如下:
public V get(Object key) {
// 调用genEntry得到Entry
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
// 如果LinkedHashMap是访问顺序的,则get时,也需要重新排序
e.recordAccess(this);
return e.value;
}
先是调用了getEntry
方法,通过key
得到Entry
;后面调用了LinkedHashMap.Entry
的recordAccess
方法重新排序,把get
的Entry
移动到双向链表的表尾
3.6 扩容
LinkedHashMap扩容时,数据的再散列和HashMap是不一样的。
HashMap是先遍历旧table,再遍历旧table中每个元素的单向链表,取得Entry以后,重新计算hash值,然后存放到新table的对应位置。
LinkedHashMap是遍历的双向链表,取得每一个Entry,然后重新计算hash值,然后存放到新table的对应位置。
从遍历的效率来说,遍历双向链表的效率要高于遍历table,因为遍历双向链表是N次(N为元素个数);而遍历table是N+table的空余个数(N为元素个数)
4. LinkedHashMap
与 LRU
(Least recently used
,最近最少使用)算法
到此为止,我们已经分析完了LinkedHashMap
的存取实现,在addEntry
方法中还调用了removeEldestEntry
方法,该方法源码如下
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
}
该方法是用来被重写的,一般地,如果用LinkedHashmap
实现LRU
算法,就要重写该方法。比如可以将该方法覆写为如果设定的内存已满,则返回true
,这样当再次向LinkedHashMap
中putEntry
时,在调用的addEntry
方法中便会将近期最少使用的节点删除掉(header
后的那个节点)
使用LinkedHashMap
实现一个符合LRU
算法的数据结构,该结构最多可以缓存6个元素,但元素多余六个时,会自动删除最近最久没有被使用的元素,如下所示:
public class LRU<K,V> extends LinkedHashMap<K, V> implements Map<K, V>{
private static final long serialVersionUID = 1L;
public LRU(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor, accessOrder);
}
/**
* @description 重写LinkedHashMap中的removeEldestEntry方法,当LRU中元素多余6个时,
* 删除最不经常使用的元素
* @author rico
* @created 2017年5月12日 上午11:32:51
* @param eldest
* @return
* @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
*/
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
// TODO Auto-generated method stub
if(size() > 6){
return true;
}
return false;
}
public static void main(String[] args) {
LRU<Character, Integer> lru = new LRU<Character, Integer>(
16, 0.75f, true);
String s = "abcdefghijkl";
for (int i = 0; i < s.length(); i++) {
lru.put(s.charAt(i), i);
}
System.out.println("LRU中key为h的Entry的值为: " + lru.get('h'));
System.out.println("LRU的大小 :" + lru.size());
System.out.println("LRU :" + lru);
}
}