1、LinkedHashMap类的说明
1.1 解释说明
/**
* 哈希表和链表实现了Map接口,具有可预测的迭代顺序(也就是说LinkedHashMap是有序的)
* 这个工具不同于HashMap因为它在entry节点上多维护了一组双向链表,用来保证entry
* 插入有序。(我们知道在HashMap中每一个桶中的节点用一个单链表或者红黑树维护)
* 这个双向链表保证了entry的迭代顺序(也就是保证entry有序),通常是键插入的顺序。
* 但是要注意,如果一个键是重新插入并不会影响它的顺序(也就是说如果键存在,那么
* 再插入一个相同的Key的时候,它首先会执行m.containsKey(),如果存在则返回true)
*
*
* 这个实现使它的客户端避免了由HashMap和HashTable所提供的未指定的、通常是混乱
* 的排序,并且不会增加类似于TreeMap的成本(这里不是很理解treeMap哪里成本比较高
* 等以后看了treeMap再回来看看)。它还可以生成与原始Map相同的副本,而不管原始Map
* 的实现方式。
* <pre>
* void foo(Map m) {
* Map copy = new LinkedHashMap(m);
* ...
* }
* </pre>
*
*
* 如果一个模块接收输入,复制它,然后又副本决定其返回的顺序。那么这个工具会非常有用。
* 客户端通常喜欢按照他们提交的顺序显示结果。
*
*
* LinkedHashMap(int,float,boolean) 这个构造函数一共一个创建链式HashMap,它的迭代顺序
* 是它的entry最后一次被访问。从最近最少访问到最近最多访问。这种方式的映射特别适用于
* LRU缓存(最近最少使用),调用{@code put}、{@code putIfAbsent}、{@code get}、{@code getOrDefault}、
* {@code compute}、{@code computeIfAbsent}、{@code computeIfPresent}或{@code merge}
* 方法会导致对相应entry的访问(假设在调用完成后存在)。如果值被替换,{@code replace}方法
* 只会导致对entry的访问。{@code putAll}方法为指定映射中的每个映射生成一个entry访问,其顺序
* 是键值映射由指定映射的entry集迭代器提供。没有其他方法生成entry访问。,尤其是对集合
* 视图的操作而不是会影响支持映射的迭代顺序。
*
*
* 可以通过重写removeEldestEntry(Map.Entry)方法,以便在向映射添加映射时强制执行自动删除
* 陈旧的映射。(一般用链表的形式进行存储数据都需要有一个删除的过程,不然内存空间会爆满的)
*
*
* 这个类提供了Map的所有操作(因为它继承了Hashmap且实现了Map接口),且允许空值。跟HashMap一样,
* 它提供固定的时间复杂度的性能对于基本的操作,像add(),contains()还有remove()。假设元素被均匀
* 的分配到每个桶中,则LinkedHashMap的性能可能会比HashMap的性能稍微低一点,因为要维护链表的开销。
* 但是有一个例外,LinkedHashMap迭代集合元素的时候,只跟集合中的元素个数有关,而于桶的多少无关。
* 但是HashMap是跟桶的容量大小成正比的,因为它要从第一个桶一个个找过去。
*
*
* LinkedHashMap也有两个属性影响它的性能,初始容量和装载因子。这个于HashMap一样。但是要注意的是,
* 如果把初始容量定的太高的话,对于LinedHashMap的影响比HashMap低,原因我们在前面已经说过了,那是
* 因为LinkedHashMap的迭代不受初始容量的影响。
*
*
* 这个方法也是非线程安全的。如果有多个线程访问,且至少有一个线程修改了Map,则必须外部自己加锁。
* 这通常是通过在一些自然封装映射的对象上进行同步来实现的。
*
*
* 如果不存在这样的对象,则应该使用{@link Collections#synchronizedMap集合'包装'映射。
* synchronizedMap}方法。这最好在创建时完成,以防止意外地不同步地访问映射:
* Map m = Collections.synchronizedMap(new LinkedHashMap(...));
*
*
* 一个结构性修改是指添加或者删除一个或者多个映射的操作,又或者是在访问hash链表的情况下
* 影响了迭代的顺序。在插入顺序链接哈希映射中,仅更改与映射中已包含的键关联的值不是结构
* 性修改。在访问顺序的链接哈希映射中,仅用get()查询映射是一种结构修改。
*
*
* 跟HashMap一样,迭代器也是采用"fail-fast"来对并发的结构性修改抛出异常,但是也不能用这个
* 来保证并发性的修改,它只是检测bug,最终还是要靠外部的同步来保证并发的修改安全。
1.2 数据结构
跟hashmap相比较主要多了一个双向链表来维护entry,图中的黑色双向线。在JDK1.8中也是采用(数组+单链表+双向链表+红黑树)
2、方法字段
/**
* 在继承了HashMap的节点的基础上,新增了双向链表
*/
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);
}
}
//序列号
private static final long serialVersionUID = 3801124242820219131L;
/**
* 双向链表的头结点
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 双向链表的尾结点
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
*迭代的排序方法,如果是true,则按顺序,
*为false,则不按顺序
* @serial
*/
final boolean accessOrder;
从字段可以发现主要是继承自hashMap
然后增加了一个双向链表和一个指向头尾的指针,主要用来插入和删除的方便。还有一个accessOrder主要用来指定要不要按照顺序插入。
3、关键方法
linkNodeLast(LinkedHashMap.Entry<K,V> 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(LinkedHashMap.Entry<K,V> src, LinkedHashMap.Entry<K,V> dst)
// 其中的dst:destination目的节点,src:source源节点,这个函数的作用
//就是用dst节点替换掉src节点,整个过程就是一个双向链表的替换节点过程
private void transferLinks(LinkedHashMap.Entry<K,V> src,
LinkedHashMap.Entry<K,V> dst) {
LinkedHashMap.Entry<K,V> b = dst.before = src.before;//将dst的befor指针指向srcde before指针所指节点
LinkedHashMap.Entry<K,V> a = dst.after = src.after;//将dst的after指针指向srcde after指针所指节点
if (b == null)//如果前置节点不存在,则当前节点为头结点
head = dst;
else
b.after = dst;
if (a == null)//后置节点不存在,则为尾结点
tail = dst;
else
a.before = dst;
}
这个方法是用来替换节点的,当映射到相同的key,想要替换掉对应的value就调用这个方法,可以看到这个是一个private方法,说明它是在内部调用,提供给我们的是其他方法比如replacementNode
方法,主要的实现如图所示。
afterNodeRemoval(Node<K,V> e)
//删除节点
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;//否则b的after节点链接到a
if (a == null)//没有后置节点
tail = b;//b为尾结点
else
a.before = b;//否则a的before节点链接到b
}
afterNodeInsertion(boolean evict)
//插入节点,如果存在相同key,可能删除第一个旧的节点
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {//removeEldestEntry删除旧节点,默认是false
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
afterNodeAccess(Node<K,V> e)
//访问节点,则会将访问的节点会被移到链表的最后位置,这个方法和afterNodeRemoval(),就可以实现LRU算法
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;
}
}
4、总结
总的来说LinkedHashMap主要是继承自HashMap,HashMap有的LinkedHashMap也都可以实现。最主要的区别在于LinkedHashMap多了双向链表来维护节点的顺序,使节点按插入的时间有序,此外还有一个就是实现了插入新节点的时候能够判断是否要删除旧节点,可以用这个来实现LRU算法。