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

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的容量有关






























你可能感兴趣的:(java源码学习:1.8)