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

来源:互联网 发布:python编写桌面程序 编辑:程序博客网 时间:2024/05/29 07:03
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 一样,它可以为基本操作(addcontains 和 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-bindingfail-fast, and additionally report Spliterator.ORDERED.
此类是 Java Collections Framework 的成员。

2、源码学习

首先定义了自己的Entry<K,V>类,继承了HashMap.Node类,比HashMap.Node类多了指向前后节点的before和after节点。注意,HashMap中的TreeNode类是直接继承的这个Entry类
static classEntry<K,V>extendsHashMap.Node<K,V> {
Entry<K,V>before,after;
Entry(inthash,Kkey,Vvalue,Node<K,V> next) {
super(hash,key,value,next);
}
}

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

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

方法linkNodeLast定义了将Entry追加到队列尾部的方法:如果目前队列是空,则队列头和尾都设置为传入的p,若队列不为空,则将p放到队尾,原来的队尾的after节点指向p
private voidlinkNodeLast(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 voidtransferLinks(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,以删除原链表
voidreinitialize() {
super.reinitialize();
head=tail=null;
}

重写了HashMap的newNode方法,在这里创建了LinkedHashMap.Entry类,替换了原表的Node类,并且在本类中维护了队列(调用了linkNodeLast方法)。到这里,put方法创建的普通节点已经可以维护到队列中
Node<K,V>newNode(inthash,Kkey,Vvalue,Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
newLinkedHashMap.Entry<K,V>(hash,key,value,e);
linkNodeLast(p);
returnp;
}

重写了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 =
newLinkedHashMap.Entry<K,V>(q.hash,q.key,q.value,next);
transferLinks(q,t);
returnt;
}

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

重写了afterNodeRemoval方法,删除一个节点后,将LinkedHashMap队列中的这个节点去掉。也就是将这个节点的前后节点都置为null,而将这个节点原来的前后节点相连(如果这个节点本身是头或尾节点则做一些特殊处理。
voidafterNodeRemoval(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,这样不会清除最近访问过的数据)。
voidafterNodeInsertion(booleanevict) {// possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if(evict && (first =head) !=null&& removeEldestEntry(first)) {
Kkey = 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放到队列尾
voidafterNodeAccess(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方法,以保证在序列化时写入流的数据也是有序的
voidinternalWriteEntries(java.io.ObjectOutputStream s)throwsIOException {
for(LinkedHashMap.Entry<K,V> e = head;e !=null;e = e.after) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}

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

重写了containsValue方法,在这个版本中遍历集合使用的是本身维护的队列,这样耗时只和真实存储的数据的大小有关系,而和集合容量无关
public booleancontainsValue(Object value) {
for(LinkedHashMap.Entry<K,V> e = head;e !=null;e = e.after) {
Vv = e.value;
if(v == value || (value !=null&& value.equals(v)))
return true;
}
return false;
}

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

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

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

LinkedKeySet中LinkedKeyIterator,LinkedValues中的LinkedValueIterator都继承自LinkedHashIterator,从LinkedHashIterator的核心方法nextNode中可以看出,它在迭代时使用的是LinkedHashMap的队列而非table。
finalLinkedHashMap.Entry<K,V>nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if(modCount!=expectedModCount)
throw newConcurrentModificationException();
if(e ==null)
throw newNoSuchElementException();
current= e;
next= e.after;
returne;
}

在这些LinkedKeySet、LinkedValues、LinkedEntrySet以及LinkedHashMap本身的forEach方法中,都是直接遍历队列,也就是耗时和map存储的数据多少有关,而不像HashMap中是和map的容量有关






























阅读全文
0 0