您的位置:首页 > 编程语言 > Java开发

java1.8 常用集合源码学习:LinkedHashMap

2017-10-21 14:54 387 查看
1、api

Map 接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现与 HashMap 的不同之处在于,它维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。注意,如果在映射中重新插入 键,则插入顺序不受影响。(如果在调用 m.put(k,
v) 前 m.containsKey(k) 返回了 true,则调用时会将键 k 重新插入到映射 m 中。)
此实现可以让客户避免未指定的、由 HashMap(及 Hashtable)所提供的通常为杂乱无章的排序工作,同时无需增加与 TreeMap 相关的成本。使用它可以生成一个与原来顺序相同的映射副本,而与原映射的实现无关:
void foo(Map m) { Map copy = new LinkedHashMap(m); ... }

如果模块通过输入得到一个映射,复制这个映射,然后返回由此副本确定其顺序的结果,这种情况下这项技术特别有用。(客户通常期望返回的内容与其出现的顺序相同。)
提供特殊的构造方法来创建链接哈希映射,该哈希映射的迭代顺序就是最后访问其条目的顺序,从近期访问最少到近期访问最多的顺序(访问顺序)。这种映射很适合构建
LRU 缓存。调用 put 或 get 方法将会访问相应的条目(假定调用完成后它还存在)。putAll 方法以指定映射的条目集迭代器提供的键-值映射关系的顺序,为指定映射的每个映射关系生成一个条目访问。任何其他方法均不生成条目访问。特别是,collection
视图上的操作不 影响底层映射的迭代顺序。
可以重写 removeEldestEntry(Map.Entry) 方法来实施策略,以便在将新映射关系添加到映射时自动移除旧的映射关系。
此类提供所有可选的 Map 操作,并且允许 null 元素。与 HashMap 一样,它可以为基本操作(add、contains 和 remove)提供稳定的性能,假定哈希函数将元素正确分布到桶中。由于增加了维护链接列表的开支,其性能很可能比 HashMap 稍逊一筹,不过这一点例外:LinkedHashMap 的
collection 视图迭代所需时间与映射的大小 成比例。HashMap 迭代时间很可能开支较大,因为它所需要的时间与其容量 成比例。
链接的哈希映射具有两个影响其性能的参数:初始容量和加载因子。它们的定义与 HashMap 极其相似。要注意,为初始容量选择非常高的值对此类的影响比对 HashMap 要小,因为此类的迭代时间不受容量的影响。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射的意外的非同步访问:
Map m = Collections.synchronizedMap(new LinkedHashMap(...));
结构修改是指添加或删除一个或多个映射关系,或者在按访问顺序链接的哈希映射中影响迭代顺序的任何操作。在按插入顺序链接的哈希映射中,仅更改与映射中已包含键关联的值不是结构修改。在按访问顺序链接的哈希映射中,仅利用 get 查询映射不是结构修改。
Collection(由此类的所有 collection 视图方法所返回)的 iterator 方法返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒将来不确定的时间任意发生不确定行为的风险。
注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的方式是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
The spliterators returned by the spliterator method of the collections returned by all of this class's collection view methods
arelate-binding, fail-fast,
and additionally report Spliterator.ORDERED.
此类是 Java
Collections Framework 的成员。

2、源码学习

首先定义了自己的Entry<K,V>类,继承了HashMap.Node类,比HashMap.Node类多了指向前后节点的before和after节点。注意,HashMap中的TreeNode类是直接继承的这个Entry类
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);
}
}

然后定义了双端队列的头和尾
transient
LinkedHashMap.Entry<K,V>
head;
transient
LinkedHashMap.Entry<K,V>
tail;

然后定义了迭代顺序,默认为false,即按照插入顺序,如果初始化时设置为true,则会按照访问顺序迭代(即如果访问了某个键值对,则这个键值对会移动到队尾)
final boolean
accessOrder;

方法linkNodeLast定义了将Entry追加到队列尾部的方法:如果目前队列是空,则队列头和尾都设置为传入的p,若队列不为空,则将p放到队尾,原来的队尾的after节点指向p
private void
linkNodeLast(LinkedHashMap.Entry<K,V>
p) {
LinkedHashMap.Entry<K,V>
last = tail;
tail
= p;
if
(last ==
null)
head
= p;
else
{
p.before
= last;
last.after
= p;
}
}

transferLinks方法将dst放到队列中src的位置上。如果b==null,说明src已经是队列的头部,则将dst直接设置为头部,如果b!=null,则将src.before节点的after节点设置为dst;类似的,如果a==null,则说明src已经是队列的尾部,则将dst直接设置为尾部,如果a!=null,则设置src.after节点的before节点为dst
private void
transferLinks(LinkedHashMap.Entry<K,V>
src,
LinkedHashMap.Entry<K,V>
dst) {
LinkedHashMap.Entry<K,V>
b = dst.before
= src.before;
LinkedHashMap.Entry<K,V>
a = dst.after
= src.after;
if
(b ==
null)
head
= dst;
else
b.after
= dst;
if
(a ==
null)
tail
= dst;
else
a.before
= dst;
}

reinitialize方法除了调用父类方法外,还直接将head和tail节点置位null,以删除原链表
void
reinitialize() {
super.reinitialize();
head
=
tail
=
null;
}

重写了HashMap的newNode方法,在这里创建了LinkedHashMap.Entry类,替换了原表的Node类,并且在本类中维护了队列(调用了linkNodeLast方法)。到这里,put方法创建的普通节点已经可以维护到队列中
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;
}

重写了HashMap的replacementNode方法,目的是在HashMap的TreeNode树转换为普通的Node时在LinkedHashMap的队列中不会丢失他们的前后关系(通过调用transferLinks方法更新队列中的指针)
Node<K,V>
replacementNode(Node<K,V>
p,
Node<K,V>
next) {
LinkedHashMap.Entry<K,V>
q = (LinkedHashMap.Entry<K,V>)p;
LinkedHashMap.Entry<K,V>
t =
new
LinkedHashMap.Entry<K,V>(q.hash,
q.key,
q.value,
next);
transferLinks(q,
t);
return
t;
}

类似的,重写了HashMap的newTreeNode方法旨在创建新的TreeNode时将TreeNode维护到LinkedHashMap的队列中;
类似的,重写了replacementTreeNode方法,目的是在HashMap的普通节点转换为TreeNode节点时在LinkedHashMap的队列中不会丢失他们的前后关系(通过调用transferLinks方法更新队列中的指针)

重写了afterNodeRemoval方法,删除一个节点后,将LinkedHashMap队列中的这个节点去掉。也就是将这个节点的前后节点都置为null,而将这个节点原来的前后节点相连(如果这个节点本身是头或尾节点则做一些特殊处理。
void
afterNodeRemoval(Node<K,V>
e) { // unlink
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;
}

重写了afterNodeInsertion方法,这个方法在有新的Node插入到map中时调用(注意,是全新的key,已有key的update不会调用到这个方法)。这个方法将会判断:不是初始化时的插入(evict),且不是第一次插入(head != null),且需要去掉最老的数据(调用removeEldestEntry判断),如果满足以上条件,则去掉这个map中最先存入的那对键值对。注意removeEldestEntry默认是返回false,也就是不会去掉最老的数据,如果要使用这个LinkedHashMap作为缓存,则可以重写他的removeEldestEntry方法,在size大于一个指定阈值时返回true,即可实现一个内存的定长缓存(如果确实需要使用的话应该考虑把accessOrder也设置成true,这样不会清除最近访问过的数据)。
void
afterNodeInsertion(boolean
evict) {
// possibly remove eldest
LinkedHashMap.Entry<K,V>
first;
if
(evict && (first =
head) !=
null
&& removeEldestEntry(first)) {
K
key = first.key;
removeNode(hash(key),
key, null, false, true);
}
}

这里重写了afterNodeAccess方法,如果accessOrder设置为null,并且访问过(get、update都算)的节点p不是队列尾,则会将访问过的节点p调整到队列尾部。判断逻辑为,如果p没有前节点,则设置p的后节点为队列头,如果p有前节点,则将p的前节点的后节点指向p的后节点,也就是将p的前节点和后节点相连;如果p有后节点,则将p的后节点的前节点指向e的前节点,也就是将p的后节点和前节点相连;如果队列没有尾节点,则将p置位队列头;如果队列有尾节点,则将p放到队列尾
void
afterNodeAccess(Node<K,V>
e) { // move node to last
LinkedHashMap.Entry<K,V>
last;
if
(accessOrder
&& (last =
tail) != e) {
LinkedHashMap.Entry<K,V>
p =
(LinkedHashMap.Entry<K,V>)e,
b = p.before,
a = p.after;
p.after
=
null;
if
(b ==
null)
head
= a;
else
b.after
= a;
if
(a !=
null)
a.before
= b;
else
last = b;
if
(last ==
null)
head
= p;
else
{
p.before
= last;
last.after
= p;
}
tail
= p;
++modCount;
}
}

重写了internalWriteEntries方法,以保证在序列化时写入流的数据也是有序的
void
internalWriteEntries(java.io.ObjectOutputStream s)
throws
IOException {
for
(LinkedHashMap.Entry<K,V>
e = head;
e !=
null;
e = e.after)
{
s.writeObject(e.key);
s.writeObject(e.value);
}
}

默认的构造方法中accessOrder都是设置为false,也就是按插入顺序迭代

重写了containsValue方法,在这个版本中遍历集合使用的是本身维护的队列,这样耗时只和真实存储的数据的大小有关系,而和集合容量无关
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;
}

在get和getOrDefault方法中都加入了accessOrder的判断,如果是true,则在确实命中了key以后,会将对应的键值对移到队列尾

方法removeEldestEntry指定是否需要在插入操作后删除队列的头元素,像上面说的,我们可以重写这个方法以实现一些特别的数据结构,如内存中的定长缓存字典等。
protected boolean
removeEldestEntry(Map.Entry<K,V>
eldest) {
return false;
}

重写的KeySet方法返回的是一个LinkedKeySet,类似的,values方法返回的LinkedValues,entrySet方法返回的LinkedEntrySet。
public
Set<K>
keySet() {
Set<K>
ks = keySet;
if
(ks ==
null) {
ks =
new
LinkedKeySet();
keySet
= ks;
}
return
ks;
}

LinkedKeySet中LinkedKeyIterator,LinkedValues中的LinkedValueIterator都继承自LinkedHashIterator,从LinkedHashIterator的核心方法nextNode中可以看出,它在迭代时使用的是LinkedHashMap的队列而非table。
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;
}

在这些LinkedKeySet、LinkedValues、LinkedEntrySet以及LinkedHashMap本身的forEach方法中,都是直接遍历队列,也就是耗时和map存储的数据多少有关,而不像HashMap中是和map的容量有关
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: