文章目录
LinkedHashMap
一、LinkedHashMap简介
1.1 LinkedHashMap的继承关系
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
图示:
从继承关系上看,LinkedHashMap继承了HashMap的特性,并且在其基础上扩展了一些新特性(元素的有序性)。
- 2、LinkedHashMap的底层结构
说LinkedHashMap之前,先说HashMap。HashMap是无序的,也就是说,迭代HashMap所得到的元素顺序并不是它们最初添加到HashMap的顺序。为了保证迭代元素的顺序与存入容器的顺序一致,诞生了HashMap的子类------LinkedHashMap。
由于LinkedHashMap是HashMap的子类,所以LinkedHashMap自然会拥有HashMap的所有特性。比如,LinkedHashMap也最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。
本质上,HashMap和双向链表合二为一即是LinkedHashMap。更准确地说,它是一个将所有Entry节点链入一个双向链表双向链表的HashMap。
关键的地方来了,在HashMap有一些空方法,比如:
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
LinkedHashMap重写了这些方法,这些方法被用来保持列表的有序。
关于LinkedHashMap和HashMap的差异,借用网上两张图来说明。
- HashMap的结构:
- LinkedHashMap的结构:
1.2 LinkedHashMap特点
- 1、由于继承HashMap类,所以默认初始容量是16,加载因子是0.75。
- 2、线程不安全。
- 3、具有fail-fast的特征。
- 4、底层使用双向链表,可以保存元素的插入顺序,顺序有两种方式:一种是按照插入顺序排序,一种按照访问做排序。默认以插入顺序排序。
- 5、key和value允许为null,key重复时,新value覆盖旧value。
- 6、可以用来实现LRU算法。
- 7、LinkedHashMap与HashMap的存取数据操作基本是一致的,只是增加了双向链表保证数据的有序性。
- 8、LinkedHashMap继承HashMap,基于HashMap+双向链表实现。(HashMap是数组+链表+红黑树实现的)。
二、LinkedHashMap常见方法
- 1、构造具有默认初始容量(16)和负载因子(0.75)的LinkedHashMap实例
public LinkedHashMap()
- 2、构造具有指定初始容量和默认负载因子(0.75)的LinkedHashMap实例
public LinkedHashMap(int initialCapacity)
- 3、构造具有指定初始容量和负载因子的LinkedHashMap实例
public LinkedHashMap(int initialCapacity, float loadFactor)
- 4、构造一个空的 LinkedHashMap实例,具有指定的初始容量,负载因子和订购模式(accessOrder)。accessOrder为false时,基于插入顺序;accessOrder为true时,基于访问顺序
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
- 5、清空元素
public void clear()
- 6、判断LinkedHashMap是否包含某个value
public boolean containsValue(Object value)
- 7、返回LinkedHashMap中的Entry形成的Set
public Set<Map.Entry<K,V>> entrySet()
- 8、对LinkedHashMap中的每个元素执行特定操作
public void forEach(BiConsumer<? super K, ? super V> action)
- 9、获取某个key对应的value
public V get(Object key)
- 10、获取某个key对应的value,该key不存在时,返回默认值
public V getOrDefault(Object key, V defaultValue)
- 11、返回LinkedHashMap中的key形成的Set
public Set<K> keySet()
- 12、返回value形成的集合
public Collection<V> values()
三、LinkedHashMap源码解析
3.1 Entry
LinkedHashMap中存储的节点:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
可以看出,LinkedHashMap中的节点Entry,相比于HashMap中的节点Node,多了指向前后位置的节点before和after,用于维护双向链表(即保持元素有序)。
HashMap与LinkedHashMap的Entry结构示意图:
LinkedHashMap中的变量:
private static final long serialVersionUID = 3801124242820219131L;
//头结点
transient LinkedHashMap.Entry<K,V> head;
//尾节点
transient LinkedHashMap.Entry<K,V> tail;
//这个变量决定链表元素的存储方式,false按照存储顺序存储,true表示按照访问顺序
//存储(将最近访问的元素移动到尾部),该变量和LRU算法相关
final boolean accessOrder;
3.2 构造方法
- 1、LinkedHashMap(int initialCapacity, float loadFactor)
public LinkedHashMap(int initialCapacity, float loadFactor) {
//指定初始容量与负载因子
super(initialCapacity, loadFactor);
//默认访问顺序标识为false,表示按照存储顺序存储
accessOrder = false;
}
- 2、LinkedHashMap(int initialCapacity)
public LinkedHashMap(int initialCapacity) {
//指定初始容量,负载因子为0.75
super(initialCapacity);
//默认访问顺序标识为false,表示按照存储顺序存储
accessOrder = false;
}
- 3、LinkedHashMap()
public LinkedHashMap() {
//默认初始容量(16)和负载因子(0.75)
super();
//默认访问顺序标识为false,表示按照存储顺序存储
accessOrder = false;
}
- 5、LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
//指定初始容量与负载因子
super(initialCapacity, loadFactor);
//指定默认访问顺序标识
this.accessOrder = accessOrder;
}
3.3 是否包含某个value
public boolean containsValue(Object value) {
//从向前后进行遍历
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}
3.4 获取元素
- 1、get(Object key)
public V get(Object key) {
Node<K,V> e;
//第一步是直接使用Hashmap中的函数getNode方法,获取value,如果为null,返回null
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
//将添加的元素移动到链表的尾端
afterNodeAccess(e);
return e.value;
}
//getNode方法来自于其父类HashMap
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果此key对应的hash桶不为空执行以下
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//如果需要查询的是此哈希桶的第一个元素,直接返回
return first;
if ((e = first.next) != null) {
//如果是红黑树的结果,就需要按照红黑树的方式查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历整个链表,找到key对应的value并且返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
//将访问的这个元素移动到双向链表的尾端,并且将他的后面的元素向前移动
void afterNodeAccess(Node<K,V> e) {
//last为原链表的尾节点
LinkedHashMapEntry<K,V> last;
//如果accessOrder为true,并且尾端元素不是需要访问的元素
if (accessOrder && (last = tail) != e) {
//将节点e强制转换成linkedHashMapEntry,b为这个节点的前一个
//节点,a为它的后一个节点
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//p为最后一个元素,那么他的后置节点必定是空
p.after = null;
//b为e的前置元素,如果b为空,说明此元素必定是链表的第一个元素,更新之后
//链表的头结点已经变成尾节点,那么原链表的第二个节点就要变为头结点
if (b == null)
head = a;
else
//如果b不是空,那么b的后置节点就由p变为p的后置节点
b.after = a;
//如果p的后置节点不为空,那么更新后置节点a的前置节点为b
if (a != null)
a.before = b;
else
//如果p的后置节点为空,那么p就是尾节点,那么更新last的节点为p的前置节点
last = b;
//如果原来的尾节点为空,那么原链表就只有一个元素
if (last == null)
head = p;
else {
//更新当前节点p的前置节点为 原尾节点last, last的后置节点是p
p.before = last;
last.after = p;
}
//p为最新的尾节点
tail = p;
++modCount;
}
}
这里需要注意两点,一是调用次函数之后,访问的这个元素会移动到双向链表的尾端,二是在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询(get)访问数据,也会导致fail-fast,因为迭代的顺序已经改变。
- 2、getOrDefault
//实现与get方法基本类似,不过方法最后的返回值是默认值
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
//将添加的元素移动到链表的尾端
afterNodeAccess(e);
return e.value;
}
3.5 清空LinkedHashMap
public void clear() {
super.clear();
//将头尾节点都置为null
head = tail = null;
}
//HashMap的clear方法
public void clear() {
Node<K,V>[] tab;
modCount++;
//数组中所有元素都置为null
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
3.6 获取key组成的Set
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
//在这里linkedHashMap重写了此函数。
ks = new LinkedKeySet();
keySet = ks;
}
return ks;
}
final class LinkedKeySet extends AbstractSet<K> {
//链表元素个数
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
//遍历的目的是获取Iterator
public final Iterator<K> iterator() {
return new LinkedKeyIterator();
}
//这个直接使用的hashmap的containsKey函数,最终使用的是getNode,前文已经讲过就不在强调
public final boolean contains(Object o) { return containsKey(o); }
//remove最后也是调用了removeNode,前文已经讲过。就不在强调
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
}
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode().getKey(); }
}
3.7 获取value组成的Collection
public Collection<V> values() {
Collection<V> vs;
return (vs = values) == null ? (values = new LinkedValues()) : vs;
}
final class LinkedValues extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
public final Iterator<V> iterator() {
return new LinkedValueIterator();
}
//linkedHashMap重写了contains函数
public final boolean contains(Object o) { return containsValue(o); }
//省略了一些方法
}
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
3.8 获取entry组成的Set
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
//之后的遍历都是通过Iterator来进行
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
//通过getnode方法来获取节点
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
3.9 LinkedHashMap中的核心迭代器
abstract class LinkedHashIterator {
//下一个节点
LinkedHashMapEntry<K,V> next;
//当前节点
LinkedHashMapEntry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
//初始化的时候将next指向双向链表的头结点
next = head;
expectedModCount = modCount;
//current为空
current = null;
}
public final boolean hasNext() {
return next != null;
}
//nextNode方法就是我们用到的next方法,
//迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出
final LinkedHashMapEntry<K,V> nextNode() {
//记录要返回的e
LinkedHashMapEntry<K,V> e = next;
//fail-fast判断
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果返回的是空,则抛出异常
if (e == null)
throw new NoSuchElementException();
//更新当前节点为e
current = e;
//更新下一个节点是e的后置节点
next = e.after;
return e;
}
//删除方法,就直接使用了hashmap的remove
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
LinkedHashMap只提供了单向访问,即按照插入的顺序从头到尾进行访问,不能像LinkedList那样进行双向访问。
在新增节点时,已经维护了元素之间的插入顺序了,所以在迭代访问时只需要不断的访问当前节点的下一个节点即可。
3.10 添加元素
由于LinkedHashMap是HashMap的子类,添加元素是通过调用父类的putVal来完成的。在HashMap的putVal方法中,有调用newNode(hash, key, value, null)
方法来创建节点的操作。
LinkedHashMap重写的newNode方法:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 作用:将新建的节点添加到维护的双向链表上去
// 方式:往链表的尾部进行添加
linkNodeLast(p);
return p;
}
//在双向链表的尾部添加新建的节点
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
// p为新的需要追加的结点
tail = p;
// 如果last为null.则表示现在链表为空。新new出来的p元素就是链表的头结点
if (last == null)
head = p;
// 否则就是链表中已存在结点的情况:往尾部添加即可
else {
// 把新追加p的结点的前驱结点设置之前的尾部结点
// 把之前的尾部结点的后驱结点设为新追加的p结点
p.before = last;
last.after = p;
}
}
可以看出LinkedHashMap的在新建一个结点的时候,做了两件事:
结合构造方法来看的话:LinkedHashMap初始化时,accessOrder为false,就会按照插入顺序提供访问,插入方法使用的是父类HashMap的put方法,不过覆写了put方法,执行中调用的是newNode/newTreeNode和afterNodeAccess 方法。
LinkedHashMap通过新增头节点、尾节点,给每个节点增加before、after 属性。每次新增时,都把节点追加到尾节点,这样就可以保证新增节点是按照顺序插入到链表中的。
LinkedHashMap的没有自己的put方法的实现,而是使用父类HashMap的put方法:在正常新增之后,调用afterNodeAccess(e)和 afterNodeInsertion(evict)让LinkedHashMap自己做后续处理:
//回调函数,新节点插入之后回调 , 根据evict判断是否需要删除最老插入的节点。
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
//LinkedHashMap 默认返回false 则不删除节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//移动访问的节点到链表末端(该方法在get小节已详细介绍过)
void afterNodeAccess(Node<K,V> e) {
//省略代码....
}
//LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回true
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
3.11 删除元素
LinkedHashMap删除元素时,也是通过调用父类的方法来实现的。LinkedHashMap在删除元素时,会调用到自己重写的afterNodeRemoval方法:
//在删除节点e时,同步将e从双向链表上删除
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//待删除节点 p 的前置后置节点都置空
p.before = p.after = null;
//如果前置节点是null,则现在的头结点应该是后置节点a
if (b == null)
head = a;
else//否则将前置节点b的后置节点指向a
b.after = a;
//同理如果后置节点时null ,则尾节点应是b
if (a == null)
tail = b;
else//否则更新后置节点a的前置节点为b
a.before = b;
}