1 LinkedHashMap(jdk1.7之前)
我们知道Map
其底层数据存储是一个hash
表(数组+单向链表)。接下来我们看一下另一个LinkedHashMap
,它是HashMap
的一个子类,他在HashMap
的基础上维持了一个双向链表(hash表
+双向链表
),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO
),或者是最近最少使用(LRU
)的顺序。
来具体看下LinkedHashMap
的实现。
1.1 定义
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
从定义可以看到LinkedHashMap
继承于HashMap
,且实现了Map
接口。这也就意味着HashMap
的一些优秀因素可以被继承下来,比如hash
寻址,使用链表解决hash
冲突等实现的快速查找,对于HashMap
中一些效率较低的内容,比如容器扩容过程,遍历方式,LinkedHashMap
是否做了一些优化呢。继续看代码吧。
1.2 底层存储
LinkedHashMap
是基于HashMap
,并在其基础上维持了一个双向链表,也就是说LinkedHashMap
是一个hash
表(数组+单向链表) +双向链表的实现,到底实现方式是怎么样的,来看一下:
/** * 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.
*
* @serial11 */
private final boolean accessOrder;
看到了一个无比熟悉的属性header
,它在LinkedList
中出现过,英文注释很明确,是双向链表的头结点对不对。
再看accessOrder
这个属性,true
表示最近较少使用顺序,false
表示插入顺序。当然你说怎么没看到数组呢,别忘了LinkedHashMap
继承于HashMap
再来看下Entry
这个节点类和HashMap
中的有什么不同。
/** * LinkedHashMap entry.
*/
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
// 双向链表的上一个节点before和下一个节点after
Entry<K,V> before, after ;
// 构造方法直接调用父类HashMap的构造方法(super)
Entry( int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
/** * 从链表中删除当前节点的方法
*/
private void remove() {
// 改变当前节点前后两个节点的引用关系,当前节点没有被引用后,gc可以回收
// 将上一个节点的after指向下一个节点
before.after = after;
// 将下一个节点的before指向前一个节点
after.before = before;
}
/** * 在指定的节点前加入一个节点到链表中(也就是加入到链表尾部)
*/
private void addBefore(Entry<K,V> existingEntry) {
// 下面改变自己对前后的指向
// 将当前节点的after指向给定的节点(加入到existingEntry前面嘛)
after = existingEntry;
// 将当前节点的before指向给定节点的上一个节点33 before = existingEntry.before ;
// 下面改变前后最自己的指向
// 上一个节点的after指向自己
before.after = this;
// 下一个几点的before指向自己
after.before = this;
}
// 当向Map中获取查询元素或修改元素(put相同key)的时候调用这个方法
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
// 如果accessOrder为true,也就是使用最近较少使用顺序
if (lm.accessOrder ) {
lm. modCount++;
// 先删除,再添加,也就相当于移动了
// 删除当前元素
remove();
// 将当前元素加入到header前(也就是链表尾部)
addBefore(lm. header);
}
}
// 当从Map中删除元素的时候调动这个方法
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
可以看到Entry
继承了HashMap
中的Entry
,但是LinkedHashMap
中的Entry
多了两个属性指向上一个节点的before
和指向下一个节点的after
,也正是这两个属性组成了一个双向链表,Entry
还有一个继承下来的next
属性,这个next
是单向链表中用来指向下一个节点的,怎么回事嘛,怎么又是单向链表又是双向链表呢,其实想的没错,这里的节点即是Hash
表中的单向链表中的一个节点,它又是LinkedHashMap
维护的双向链表中的一个节点,是不是瞬间觉得高大上了
注:黑色箭头指向表示单向链表的next
指向,红色箭头指向表示双向链表的before
指向,蓝色箭头指向表示双向链表的after
指向。另外LinkedHashMap
种还有一个header
节点是不保存数据的,这里没有画出来。
从上图可以看出LinkedHashMap
仍然是一个Hash
表,底层由一个数组组成,而数组的每一项都是个单向链表,由next
指向下一个节点。但是LinkedHashMap
所不同的是,在节点中多了两个属性before
和after
,由这两个属性组成了一个双向循环链表
,而由这个双向链表维持着Map
容器中元素的顺序。看下Entry
中的recordRemoval
方法,该方法将在节点被删除时候调用,Hash
表中链表节点被正常删除后,调用该方法修正由于节点被删除后双向链表的前后指向关系,从这一点来看,LinkedHashMap比HashMap的add、remove、set等操作要慢一些(因为要维护双向链表 )。
1.3 构造方法
/** * 构造一个指定初始容量和加载因子的LinkedHashMap,默认accessOrder为false
*/
public LinkedHashMap( int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
/** * 构造一个指定初始容量的LinkedHashMap,默认accessOrder为false
*/
public LinkedHashMap( int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
/** * 构造一个使用默认初始容量(16)和默认加载因子(0.75)的LinkedHashMap,默认accessOrder为false
*/
public LinkedHashMap() {
super();
accessOrder = false;
}
/** * 构造一个指定map的LinkedHashMap,所创建LinkedHashMap使用默认加载因子(0.75)和足以容纳指定map的初始容量,默认accessOrder为false 。
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
/** * 构造一个指定初始容量、加载因子和accessOrder的LinkedHashMap
*/
public LinkedHashMap( int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
40 this.accessOrder = accessOrder;
41 }
构造方法很简单基本都是调用父类HashMap
的构造方法(super
),只有一个区别就是对于accessOrder
的设定,上面的构造参数中多数都是将accessOrder
默认设置为false
,只有一个构造方法留了一个出口可以设置accessOrder
参数。看完了构造方法,发现一个问题,头部节点header
的初始化跑哪里去了
回忆一下,看看HashMap的构造方法:
/** * Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[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() {
init()
在HashMap
中是一个空方法,也就是给子类留的一个回调函数,我们来看下LinkedHashMap
对init()
方法的实现
/** * Called by superclass constructors and pseudoconstructors (clone,
* readObject) before any entries are inserted into the map. Initializes
* the chain.
*/
void init() {
// 初始化话header,将hash设置为-1,key、value、next设置为null
header = new Entry<K,V>(-1, null, null, null);
// header的before和after都指向header自身
header.before = header. after = header ;
1.4 增加
LinkedHashMap
没有重写put
方法,只是重写了HashMap
中被put
方法调用的addEntry
/** * 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) {
// 调用createEntry方法创建一个新的节点
createEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed, else grow capacity if appropriate
// 取出header后的第一个节点(因为header不保存数据,所以取header后的第一个节点)
Entry<K,V> eldest = header.after ;
// 判断是容量不够了是要删除第一个节点还是需要扩容
if (removeEldestEntry(eldest)) {
// 删除第一个节点(可实现FIFO、LRU策略的Cache)
removeEntryForKey(eldest. key);
} else {
// 和HashMap一样进行扩容
if (size >= threshold)
resize(2 * table.length );
}
}
/** * 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) {
// 下面三行代码的逻辑是,创建一个新节点放到单向链表的头部
// 取出数组bucketIndex位置的旧节点
HashMap.Entry<K,V> old = table[bucketIndex];
// 创建一个新的节点,并将next指向旧节点
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
// 将新创建的节点放到数组的bucketIndex位置
table[bucketIndex] = e;
// 维护双向链表,将新节点添加在双向链表header前面(链表尾部)
e.addBefore( header);
// 计数器size加1
size++;
}
/** * 默认返回false,也就是不会进行元素删除了。如果想实现cache功能,只需重写该方法
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
可以看到,在添加方法上,比HashMap
中多了两个逻辑,一个是当Map
容量不足后判断是删除第一个元素,还是进行扩容,另一个是维护双向链表。而在判断是否删除元素的时候,我们发现removeEldestEntry
这个方法竟然是永远返回false
,原来想要实现Cache
功能,需要自己继承LinkedHashMap
然后重写removeEldestEntry
方法,这里默认提供的是容器的功能。
1.5 删除
LinkedHashMap
没有重写remove
方法,只是在实现了Entry
类的recordRemoval
方法,该方法是HashMap
的提供的一个回调方法,在HashMap
的remove
方法进行回调,而LinkedHashMap
中recordRemoval
的主要当然是要维护双向链表了
1.6 查找
LinkedHashMap
重写了get
方法,但是确复用了HashMap
中的getEntry
方法,LinkedHashMap
是在get
方法中指加入了调用recoreAccess
方法的逻辑,recoreAccess
方法的目的当然也是维护双向链表了,具体逻辑返回上面去看下Entry
类的recoreAccess
方法吧
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 ;
}
1.7 是否包含
/** * Returns <tt>true</tt> if this map maps one or more keys to the
* specified value.
*
* @param value value whose presence in this map is to be tested
* @return <tt> true</tt> if this map maps one or more keys to the
* specified value
*/ public boolean containsValue(Object value) {
// Overridden to take advantage of faster iterator
// 遍历双向链表,查找指定的value
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;
}
LinkedHashMap
对containsValue
进行了重写,HashMap
的containsValue
需要遍历整个hash
表,这样是十分低效的。而LinkedHashMap
中重写后,不再遍历hash
表,而是遍历其维护的双向链表,这样在效率上难道就有所改善吗?我们分析下:hash
表是由数组+单向链表组成,而由于使用hash
算法,可能会导致散列不均匀,甚至数组的有些项是没有元素的(没有hash
出对应的散列值),而LinkedHashMap
的双向链表呢,是不存在空项的,所以LinkedHashMap
的containsValue
比HashMap
的containsValue
效率要好一些。
1.8 cache功能
在最后,让我们简单基于LInkedHashMap
实现一个Cache功能
import java.util.LinkedHashMap;
import java.util.Map;
public class MyLocalCache extends LinkedHashMap<String, Object> {
private static final long serialVersionUID = 7182816356402068265L;
private static final int DEFAULT_MAX_CAPACITY = 1024;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private int maxCapacity;
public enum Policy {
FIFO, LRU
}
public MyLocalCache(Policy policy) {
super(DEFAULT_MAX_CAPACITY, DEFAULT_LOAD_FACTOR, Policy.LRU .equals(policy));
this.maxCapacity = DEFAULT_MAX_CAPACITY;
}
public MyLocalCache(int capacity, Policy policy) {
super(capacity, DEFAULT_LOAD_FACTOR, Policy. LRU.equals(policy));
this.maxCapacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return this.size() > maxCapacity;
}
public static void main(String[] args) {
MyLocalCache cache = new MyLocalCache(5, Policy.LRU);
cache.put( "k1", "v1" );
cache.put( "k2", "v2" );
cache.put( "k3", "v3" );
cache.put( "k4", "v4" );
cache.put( "k5", "v5" );
cache.put( "k6", "v6" );
System. out.println("size=" + cache.size());
System. out.println("----------------------" );
for (Map.Entry<String, Object> entry : cache.entrySet()) {
System. out.println(entry.getValue());
}
System. out.println("----------------------" );
System. out.println("k3=" + cache.get("k3"));
System. out.println("----------------------" );
for (Map.Entry<String, Object> entry : cache.entrySet()) {
System. out.println(entry.getValue());
}
}
}
网友评论