同步操作将从 icanci/Java-Review 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
本篇源码基于以下JDK版本解析:
C:\Users\icanci>java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
众所周知,HashMap 提供的访问,是无序的。而在一些业务场景下,我们希望能够提供有序访问的 HashMap 。那么此时,我们就有两种选择:
LinkedHashMap ,在 HashMap 的基础之上,提供了顺序访问的特性。而这里的顺序,包括两种:
LinkedHashMap 实现的接口、继承的类,如下图所示:
在开始看LinkedHashMap的属性之前,我们先看一下 **[Java 集合框架 - 哈希表 HashMap]**的HashMap的Node子类图
在图中,我们可以看到 LinkedHashMap 实现了自定义的节点 Entry ,一个支持指向前后节点的 Node 子类。代码如下:
// LinkedHashMap.java
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);
}
}
before
属性,指向前一个节点。after
属性,指向后一个节点。
通过 before
+ after
属性,我们就可以形成一个以 Entry 为节点的链表
既然LinkedHashMap是LinkedList+Hash Map的组合,那必然有头尾节点两兄弟,所以属性如下:
// LinkedHashMap.java
/**
* 头节点。
*
* 越老的节点,放在越前面。所以头节点,指向链表的开头
*
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 尾节点
*
* 越新的节点,放在越后面。所以尾节点,指向链表的结尾
*
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 是否按照访问的顺序。
*
* true :按照 key-value 的访问顺序进行访问。
* false :按照 key-value 的插入顺序进行访问。
*
* The iteration ordering method for this linked hash map: {@code true}
* for access-order, {@code false} for insertion-order.
*
* @serial
*/
final boolean accessOrder;
仔细看下每个属性的注释。
head
+ tail
属性,形成 LinkedHashMap 的双向链表。而访问的顺序,就是 head => tail
的过程。
accessOrder
属性,决定了 LinkedHashMap 的顺序。也就是说
true
时,当 Entry 节点被访问时,放置到链表的结尾,被 tail
指向。false
时,当 Entry 节点被添加时,放置到链表的结尾,被 tail
指向。如果插入的 key 对应的 Entry 节点已经存在,也会被放到结尾。也就是下面这张图:
LinkedHashMap 一共有 5 个构造方法,其中四个和 HashMap 相同,只是多初始化 accessOrder = false
。所以,默认使用插入顺序进行访问。
另外一个 #LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
构造方法,允许自定义 accessOrder
属性。代码如下
// LinkedHashMap.java
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
在插入 key-value 键值对时,例如说 #put(K key, V value)
方法,如果不存在对应的节点,则会调用 #newNode(int hash, K key, V value, Node e)
方法,创建节点。
因为 LinkedHashMap 自定义了 Entry 节点,所以必然需要重写该方法。代码如下
// LinkedHashMap.java
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// <1> 创建 Entry 节点
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<>(hash, key, value, e);
// <2> 添加到结尾
linkNodeLast(p);
// 返回
return p;
}
<1>
处,创建 Entry 节点。虽然此处传入 e
作为 Entry.next
属性,指向下一个节点。但是实际上,#put(K key, V value)
方法中,传入的 e = null
。
<2>
处,调用 #linkNodeLast(LinkedHashMap.Entry p)
方法,添加到结尾。代码如下:
// LinkedHashMap.java
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
// 记录原尾节点到 last 中
LinkedHashMap.Entry<K,V> last = tail;
// 设置 tail 指向 p ,变更新的尾节点
tail = p;
// 如果原尾节点 last 为空,说明 head 也为空,所以 head 也指向 p
if (last == null)
head = p;
// last <=> p ,相互指向
else {
p.before = last;
last.after = p;
}
}
这样,符合越新的节点,放到链表的越后面
在 HashMap 的读取、添加、删除时,分别提供了 #afterNodeAccess(Node e)
、#afterNodeInsertion(boolean evict)
、#afterNodeRemoval(Node e)
回调方法。这样,LinkedHashMap 可以通过它们实现自定义拓展逻辑。
在 accessOrder
属性为 true
时,当 Entry 节点被访问时,放置到链表的结尾,被 tail
指向。所以 #afterNodeAccess(Node e)
方法的代码如下:
// LinkedHashMap.java
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// accessOrder 判断必须是满足按访问顺序。
// (last = tail) != e 将 tail 赋值给 last ,并且判断是否 e 已经是队尾。如果是队尾,就不用处理了。
if (accessOrder && (last = tail) != e) {
// 将 e 赋值给 p 【因为要 Node 类型转换成 Entry 类型】
// 同时 b、a 分别是 e 的前后节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 第一步,将 p 从链表中移除
p.after = null;
// 处理 b 的下一个节点指向 a
if (b == null)
head = a;
else
b.after = a;
// 处理 a 的前一个节点指向 b
if (a != null)
a.before = b;
else
last = b;
// 第二步,将 p 添加到链表的尾巴。实际这里的代码,和 linkNodeLast 是一致的。
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
// tail 指向 p ,实际就是 e 。
tail = p;
// 增加修改次数
++modCount;
}
}
链表的操作看起来比较繁琐,实际一共分成两步:1)第一步,将 p
从链表中移除;2)将 p
添加到链表的尾巴。
因为 HashMap 提供的 #get(Object key)
和 #getOrDefault(Object key, V defaultValue)
方法,并未调用 #afterNodeAccess(Node e)
方法,这在按照读取顺序访问显然不行,所以 LinkedHashMap 重写这两方法的代码,如下:
// LinkedHashMap.java
public V get(Object key) {
// 获得 key 对应的 Node
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果访问到,回调节点被访问
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
public V getOrDefault(Object key, V defaultValue) {
// 获得 key 对应的 Node
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
// 如果访问到,回调节点被访问
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
在开始看 #afterNodeInsertion(boolean evict)
方法之前,我们先来看看如何基于 LinkedHashMap 实现 LRU 算法的缓存。代码如下:
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 传递进来最多能缓存多少数据
*
* @param cacheSize 缓存大小
*/
public LRUCache(int cacheSize) {
// true 表示让 LinkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
return size() > CACHE_SIZE;
}
}
为什么能够这么实现呢?我们在 #afterNodeInsertion(boolean evict)
方法中来理解。代码如下:
// LinkedHashMap.java
// evict 翻译为驱逐,表示是否允许移除元素
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// first = head 记录当前头节点。因为移除从头开始,最老
// <1> removeEldestEntry(first) 判断是否满足移除最老节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
// <2> 移除指定节点
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
<1>
处,调用 #removeEldestEntry(Map.Entry eldest)
方法,判断是否移除最老节点。代码如下
// LinkedHashMap.java
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
<2>
处,如果满足条件,则调用 #removeNode(...)
方法,移除最老的节点。
在节点被移除时,LinkedHashMap 需要将节点也从链表中移除,所以重写 #afterNodeRemoval(Node e)
方法,实现该逻辑。代码如下:
// LinkedHashMap.java
void afterNodeRemoval(Node<K,V> e) { // unlink
// 将 e 赋值给 p 【因为要 Node 类型转换成 Entry 类型】
// 同时 b、a 分别是 e 的前后节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 将 p 从链表中移除
p.before = p.after = null;
// 处理 b 的下一个节点指向 a
if (b == null)
head = a;
else
b.after = a;
// 处理 a 的前一个节点指向 b
if (a == null)
tail = b;
else
a.before = b;
}
因为 LinkedHashMap 需要满足按顺序访问,所以需要重写 HashMap 提供的好多方法,例如说本小节我们看到的几个。
#keysToArray(T[] a)
方法,转换出 key 数组顺序返回。代码如下
// LinkedHashMap.java
@Override
final <T> T[] keysToArray(T[] a) {
Object[] r = a;
int idx = 0;
// 通过 head 顺序遍历,从头到尾
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
r[idx++] = e.key;
}
return a;
}
要小心,必须保证 a
放得下 LinkedHashMap 所有的元素。
#valuesToArray(T[] a)
方法,转换出 value 数组顺序返回。代码如下
// LinkedHashMap.java
@Override
final <T> T[] valuesToArray(T[] a) {
Object[] r = a;
int idx = 0;
// 通过 head 顺序遍历,从头到尾
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
r[idx++] = e.value;
}
return a;
}
#keySet()
方法,获得 key Set 。代码如下
// LinkedHashMap.java
public Set<K> keySet() {
// 获得 keySet 缓存
Set<K> ks = keySet;
// 如果不存在,则进行创建
if (ks == null) {
ks = new LinkedKeySet(); // LinkedKeySet 是 LinkedHashMap 自定义的
keySet = ks;
}
return ks;
}
其中, LinkedKeySet 是 LinkedHashMap 自定义的 Set 实现类。代码如下:
// LinkedHashMap.java
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(); // <X>
}
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 Object[] toArray() {
return keysToArray(new Object[size]);
}
public <T> T[] toArray(T[] a) {
return keysToArray(prepareArray(a));
}
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);
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
其内部,调用的都是 LinkedHashMap 提供的方法
#values()
方法,获得 value Collection 。代码如下:
// LinkedHashMap.java
public Collection<V> values() {
// 获得 values 缓存
Collection<V> vs = values;
// 如果不存在,则进行创建
if (vs == null) {
vs = new LinkedValues(); // LinkedValues 是 LinkedHashMap 自定义的
values = vs;
}
return vs;
}
其中, LinkedValues 是 LinkedHashMap 自定义的 Collection 实现类。代码如下
// LinkedHashMap.java
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(); // <X>
}
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return Spliterators.spliterator(this, Spliterator.SIZED |
Spliterator.ORDERED);
}
public Object[] toArray() {
return valuesToArray(new Object[size]);
}
public <T> T[] toArray(T[] a) {
return valuesToArray(prepareArray(a));
}
public final void forEach(Consumer<? super V> 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.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
其内部,调用的都是 LinkedHashMap 提供的方法。
#entrySet()
方法,获得 key-value Set 。代码如下:
// LinkedHashMap.java
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
// LinkedEntrySet 是 LinkedHashMap 自定义的
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
其中, LinkedEntrySet 是 LinkedHashMap 自定义的 Set 实现类。代码如下:
// LinkedHashMap.java
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator(); // <X>
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return Spliterators.spliterator(this, Spliterator.SIZED |
Spliterator.ORDERED |
Spliterator.DISTINCT);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> 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);
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
其内部,调用的都是 LinkedHashMap 提供的方法。
在上面的代码中,实际标记了三处 <X>
标记,分别是 LinkedKeyIterator、LinkedValueIterator、LinkedEntryIterator ,用于迭代遍历 key、value、Entry 。而它们都继承了 LinkedHashIterator 抽象类,代码如下:
// LinkedHashMap.java
abstract class LinkedHashIterator {
/**
* 下一个节点
*/
LinkedHashMap.Entry<K,V> next;
/**
* 当前节点
*/
LinkedHashMap.Entry<K,V> current;
/**
* 修改次数
*/
int expectedModCount;
LinkedHashIterator() {
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;
// 如果发生了修改,抛出 ConcurrentModificationException 异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 如果 e 为空,说明没有下一个节点,则抛出 NoSuchElementException 异常
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();
// 如果发生了修改,抛出 ConcurrentModificationException 异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 标记 current 为空,因为被移除了
current = null;
// 移除节点
removeNode(p.hash, p.key, null, false, false);
// 修改 expectedModCount 次数
expectedModCount = modCount;
}
}
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
// key
public final K next() { return nextNode().getKey(); }
}
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<V> {
// value
public final V next() { return nextNode().value; }
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
// Entry
public final Map.Entry<K,V> next() { return nextNode(); }
}
#clear()
方法,清空 LinkedHashMap 。代码如下:
// LinkedHashMap.java
public void clear() {
// 清空
super.clear();
// 标记 head 和 tail 为 null
head = tail = null;
}
需要额外清空 head
、tail
在序列化时,会调用到 #internalWriteEntries(java.io.ObjectOutputStream s)
方法,重写代码如下:
// LinkedHashMap.java
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
// 通过 head 顺序遍历,从头到尾
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
// 写入 key
s.writeObject(e.key);
// 写入 value
s.writeObject(e.value);
}
}
在反序列化时,会调用 #reinitialize()
方法,重写代码如下:
// LinkedHashMap.java
void reinitialize() {
// 调用父方法,初始化
super.reinitialize();
// 标记 head 和 tail 为 null
head = tail = null;
}
查找值时,会调用 #containsValue(Object value)
方法,重写代码如下:
// LinkedHashMap.java
public boolean containsValue(Object value) {
// 通过 head 顺序遍历,从头到尾
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;
}
如下几个方法,是 LinkedHashMap 重写和红黑树相关的几个方法,胖友可以自己瞅瞅:
#replacementNode(Node p, Node next)
#newTreeNode(int hash, K key, V value, Node next)
#replacementTreeNode(Node p, Node next)
#transferLinks(LinkedHashMap.Entry src, LinkedHashMap.Entry dst)
下面,我们来对 ArrayList 做一个简单的小结:
accessOrder = false
时,按照 key-value 的插入顺序进行访问。accessOrder = true
时,按照 key-value 的读取顺序进行访问。#keysToArray(T[] a)
等遍历相关的方法。下一篇:Java 集合框架 - 哈希集合 HashSet
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。