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 are
late-binding
,
fail-fast
, and additionally report
Spliterator.ORDERED
.
此类是
Java Collections Framework
的成员。
2、源码学习
首先定义了自己的Entry类,继承了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的容量有关