~ 前言
如果之前没有了解过HashMap的源码,请先看 java之HashMap详解。如果已经了解过了,那么今天这篇文章非常简单。
后续讲解内容为源码实现,这里使用的是JDK8的版本。
LinkedHashMap
LinkedHashMap是HashMap的一个子类,它的用法与父类HashMap相同。不过LinkedHashMap比HashMap多了一个特点,那就是可以按照添加元素时的顺序来进行遍历。并且通过名字 Linked 我们可以猜到它是一个链表结构,但是又要与HashMap结合,这个就有点懵了,我们慢慢来探索吧。
首先认识一下很重要的几个成员与节点的定义。
// 头节点,里面内容为空,访问顺序下是最年轻的
transient LinkedHashMap.Entry<K,V> head;
// 尾节点,里面内容为空,访问顺序下是最年轻的
transient LinkedHashMap.Entry<K,V> tail;
// 访问顺序
// true可以按最近访问顺序遍历,最近访问的优先读
final boolean accessOrder;
// 添加LinkedHashMap节点独有的定义(双向指针: before,after)
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);
}
}
然后我们来看一下它的关系图。
通过关系图我们能清楚的看到它的实现:
- HashMap,拥有HashMap的所有功能
- cloneable,可以克隆
- serializable,可以被序列化
初始化
简单分析一下:
// 都是调用HashMap进行初始化
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
// 开启访问顺序参数 this.accessOrder = accessOrder;
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
~ 基本方法解析
put
会直接会调用到HashMap的方法,所以效果与HashMap一样,但是到最后会有回调方法的调用,而回调方法实现在LinkedHashMap中。
那我们先来看看HashMap是在put方法什么情况下调用到LinkedHashMap的回调方法。
// 情况一
// 修改数据定位到的插槽已经存在一个或以上的节点
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 回调方法
afterNodeAccess(e);
return oldValue;
}
// 情况二
// 插入新节点
++modCount;
if (++size > threshold)
resize();
// 回调方法
afterNodeInsertion(evict);
return null;
那么我们根据以上HashMap的代码片段分别分析一下两种情况的回调方法作用。
// 开启访问顺序前提下,将节点放到最后(尾巴表示最年轻)并维护指针
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
// 是否开启访问顺序,尾节点不是当前修改的节点
// 都符合,将新插入节点放到链表尾部
if (accessOrder && (last = tail) != e) {
// p = 刚修改的节点
// b = before, a = after
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 既然要移动到最后那么下一个节点就是空
p.after = null;
// 当前节点是头节点,就把头节点指向下一个节点
//【head】->【this】-> 【after】 ===》 【head】->【after】
if (b == null)
head = a;
// 不是头节点将上一个节点指向下一个节点
//【head】->【before】->【this】-> 【after】 ===》【head】->【before】->【after】
else
b.after = a;
// 下一个节点不是尾节点,将它上一个指针指向before
//【before】<-【after】
if (a != null)
a.before = b;
// 是尾节点了
// 【before】<-【last】
else
last = b;
// 到这里就已经找到尾节点了,走else逻辑
if (last == null)
head = p;
// 不是,将修改节点放到最后
else {
p.before = last;
last.after = p;
}
// 将尾节点指向修改节点
tail = p;
++modCount;
}
}
// 删除没有访问过节点,最老的
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
// evict在调用put方法时为true
// first = head 拿到最老的节点
// removeEldestEntry(first) 默认返回false
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
// 条件符合就删除这个节点
removeNode(hash(key), key, null, false, true);
}
}
这里我们已经把两种回调都看完了,注释标注的十分明了。
- 在第二个回调方法中我们看到有一段逻辑判断中调用了某个方法,并默认返回false,
protected boolean removeEldestEntry(Map.Entry<K,V> eldest)
,那么默认都返回false了那么后面的逻辑就都不走了呀。那写这个方法有什么意义呢?
首先我们看到方法用了 “protected“ 这个关键字,说明它是可以被我们自定义实现继承的(源码中一般 protected 标注的方法我们都是可以继承实现,留给我们扩展),那么我们实现这个方法的话就可以让它返回true,就可以继续执行后面的逻辑了。
我们知道HashMap可以扩容的非常大,并且没有对数据的内容大小限制之类的,就说明了HashMap存储的数据在宏观上是无限的,这样就会有内存溢出的风险。LinkedHashMap还维护了一个链表的结构,这样看是不是就很像一个缓存呢?而缓存为了处理老化数据和内存溢出的风险都会采取一些措施,比如lru等算法。所以我们实现这个方法可以实现一些lru的数据淘汰策略。
ok,我们两个方法都分析完成了,也知道了LinkedHashMap在HashMap的数据结构上对节点之间维护了一个另外的链表。是不是非常简单呢。
remove
这里和put()方法一样会调用HashMap的实现,最后进行回调方法的调用,而回调方法实现在LinkedHashMap中。
按例先看一下HashMap是如何调用到回调方法的。
// 找了了要删除的节点,根据节点的结构进行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
// 删除完后开始执行回调方法
afterNodeRemoval(node);
return node;
}
接着就是回调方法
void afterNodeRemoval(Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 将前后指针全部断开
p.before = p.after = null;
// 然后根据节点判断是否是头或者尾
// 如果不是,前后两个节点互相指向对方
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
上面的回调就是将需要移除的节点从链表移除。
get
public V get(Object key) {
Node<K,V> e;
// 调用HashMap拿节点
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果开启顺序访问就将访问到的值放到最后
// 意味着是最年轻的
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
非常简单
keySet
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(); }
// 这里是重点我们直接看这个方法
public final Iterator<K> iterator() {
return new LinkedKeyIterator();
}
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return Spliterators.spliterator(this, Spliterator.SIZED |
Spliterator.ORDERED |
Spliterator.DISTINCT);
}
public final void forEach(Consumer<? super K> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
// 遍历
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.key);
// modcount 判断前后变化
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
这里的 iterator 是重点,我们直接看它的代码。
// 有继承我们直接先看继承的类
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode().getKey(); }
}
abstract class LinkedHashIterator {
// 这三个变量的ArrayList里面的基本一样,可以理解为一样
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
// next指向头节点
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
// 遍历
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
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;
// 里面会回调afterNodeRemoval() 来维护链表
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
迭代器的初始化中,next = head;
这个代码说明我们遍历的是LinkedHashMap维护的链表
最后
没错到这里就将一些基础的源码看完了,对于树节点我们都放在后面看。
如果有些同学很难理解其中的代码,就需要先看一下前面的文章(HashMap)。
最后补一张LinkedHashMap的简单示意图,紫色箭头线就是LinkedHashMap自己维护的特殊指针。