java-map之LinkedHashMap
1.1概述
在使用HashMap的时候,可能会遇到需要按照当时put的顺序来进行哈希表的遍历。通过上篇对HashMap的了解,我们知道HashMap中不存在保存顺序的机制。本篇文章要介绍的LinkedHashMap专为此特性而生。在LinkedHashMap中可以保持两种顺序,分别是插入顺序和访问顺序,这个是可以在LinkedHashMap的初始化方法中进行指定的。相对于访问顺序,按照插入顺序进行编排被使用到的场景更多一些,所以默认是按照插入顺序进行编排。
1.2结构
所以该结构其实就是用双向链表加hashmap实现的,
1.3特点
- key和value都允许为空
- key重复会覆盖,value可以重复
- 有序的
- LinkedHashMap是非线程安全的
1.4详解
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
可以看到,LinkedHashMap继承了HashMap,实现了Map接口。
在属性上它比HashMap多了两个属性:
//链表的头结点
private transient Entry<K,V> header;
//该属性指取得键值对的方式,是个布尔值,false表示插入顺序,true表示访问顺序,也就是访问次数.
private final boolean accessOrder;
LinkedHashMap有五个构造器:
//用默认的初始容量和负载因子构建一个LinkedHashMap,取出键值对的方式是插入顺序
public LinkedHashMap() {
super();
accessOrder = false;
}
//构造一个指定初始容量的LinkedHashMap,取得键值对的顺序
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//构造一个指定初始容量和负载因子,按照插入顺序的LinkedHashMap
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//根据给定的初始容量,负载因子和键值对迭代顺序构建一个LinkedHashMap
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//通过给定的map创建一个LinkedHashMap,负载因子是默认值,迭代方式是插入顺序.
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
其实主要区别还是在基本数据结构:
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);
}
LinkedHashMap的Entry类继承了HashMap的Entry,并在此基础上进行了扩展,它拥有以下属性:
K key;
V value;
Entry<K, V> next;
int hash;
Entry<K, V> before;
NEtry<K, V> after;
LinkedHashMap的初始化实际上是先调用HashMap的初始化,最后调用LinkedHashMap的init()使得header初始化。
void init() {
header = new Entry<>(-1, null, null, null);
header.before = header.after = header;
}
在header中,hash值为-1,其他都为null,也就是说这个header不在数组table中,其实它就是用来指示开源元素、标记结束元素的.header的目的就是为了记录第一个插入的元素是谁,在遍历的时候能够找到第一个元素
LinkedHashMap保存元素只是重写了写了父类put方法逻辑中调用的子方法addEntry()和createEntry()。
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++;
//在这里调用的是LinkedHashMap重写后的addEntry方法,这之前的和HashMap一样
addEntry(hash, key, value, i);
return null;
}
在最后一步调用LinkedHashMap的addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
//调用HashMap的addEntry,在map部分添加元素。
super.addEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
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方法.
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++;
}
其实以上的操作和HashMap的操作没有什么不同,都是把新添加的节点放在了table[bucketIndex]位置上,差别在于LinkedHashMap还做了addBefore操作,而addBefore方法的目的就是让新的Entry和原链表生成一个双向链表.
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
LinkedHashMap获得元素:
public V get(Object key) {
//调用父类的getEntry方法获取元素
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
//该方法用来记录访问顺序.
e.recordAccess(this);
return e.value;
}
可以看到,在get方法中,它是调用了父类的getEntry方法来获取到元素,之后再调用自己的recordAccess()方法:
void recordAccess(HashMap<K,V> m) {
//因为在之前是转型为父类对象来获取entry的,所以这里要转回LinkedHashMap,判断获取数据的方式.
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//当LinkedHashMap按照访问来排序时
if (lm.accessOrder) {
lm.modCount++;
//移除当前节点
remove();
//将当前节点插入到头节点前面,即最后面.
addBefore(lm.header);
}
}
调用父类的getEntry方法获取节点数据以后,再判断当前排序模式accessOrder,如果accessOrder是true,即按照访问顺序排序,那就将当前节点从链表中移除,然后再将当前节点插入到链表的尾部.
1.5应用
实现LRU算法:
public class LRUCache extends LinkedHashMap
{
public LRUCache(int maxSize)
{
super(maxSize, 0.75F, true);
maxElements = maxSize;
}
protected boolean removeEldestEntry(java.util.Map.Entry eldest)
{
return size() > maxElements;
}
private static final long serialVersionUID = 1L;
protected int maxElements;
}
需要重写removeEldestEntry()方法,原来默认的该方法返回值为false,现在我们需要在容量满时移除最不常用的元素。
当然,也可以自定义实现该算法,和上面这种继承方式不一样的的地方在于LRUCache类的节点中包含的是三个成员变量,一个最大容量,一个双向链表,一个map集合。然后在LRUCache的增删改查中封装双向链表和map集合的api函数。