目录
一.概述
LinkedHashMap是HashMap的子类,关于HashMap可以看下前面的章节:java基础进阶篇 HashMap
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
二.特点
- 非线程安全
- LinkedHashMap 内部保证顺序; 分插入顺序和访问排序两种, 如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。 HashMap不保证插入顺序.
- LinkedHashMap存取数据,还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序。
- LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。
- LinkedHashMap的插入顺序和访问顺序可以由开发者自己决定.
三.应用场合
HashMap是无序的,当我们希望有顺序(保证插入顺序)地去存储key-value时,就需要使用LinkedHashMap了.
使用HashMap:
Map<Integer, String> hashMap = new HashMap<Integer, String>();
hashMap.put(1, "a1");
hashMap.put(3, "a3");
hashMap.put(2, "a2");
hashMap.put(0, "a0");
Set<Entry<Integer, String>> set = hashMap.entrySet();
Iterator<Entry<Integer, String>> iterator = set.iterator();
while (iterator.hasNext()) {
Entry entry = iterator.next();
Integer key = (Integer) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
输出结果:
key:0,value:a0
key:1,value:a1
key:2,value:a2
key:3,value:a3
HashMap 给key做了排序, 不能保证插入顺序, 当有插入顺序的需求时, 就轮到LinkedHashMap 登场了.
使用LinkedHashMap:
Map<Integer, String> hashMap = new LinkedHashMap<Integer, String>();
hashMap.put(1, "a1");
hashMap.put(3, "a3");
hashMap.put(2, "a2");
hashMap.put(0, "a0");
Set<Entry<Integer, String>> set = hashMap.entrySet();
Iterator<Entry<Integer, String>> iterator = set.iterator();
while (iterator.hasNext()) {
Entry entry = iterator.next();
Integer key = (Integer) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
输出结果:
key:1,value:a1
key:3,value:a3
key:2,value:a2
key:0,value:a0
完美保证了插入时的顺序.
四.构造方法
LinkedHashMap继承了HashMap,所以它们有很多相似的地方。
在这儿我们只看参数为空 和修改排序模式的两种构造方法.
1.参数为空
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the default initial capacity (16) and load factor (0.75).
*/
public LinkedHashMap() {
super();
accessOrder = false;
}
默认初始容量是16, 负载因子是0.75. 这里调用了super() 即HashMap的构造方法.
accessOrder设置为false,表示不是访问顺序而是插入顺序存储的,这也是默认值,表示LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的。LinkedHashMap也提供了可以设置accessOrder的构造方法.
2.accessOrder
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
通过例子来看下参数accessOrder 在插入顺序和访问顺序上的作用.
true的情况:
Map<Integer, String> hashMap = new LinkedHashMap<Integer, String>(16,0.75f,true);
hashMap.put(1, "a1");
hashMap.put(3, "a3");
hashMap.put(2, "a2");
hashMap.put(0, "a0");
Set<Entry<Integer, String>> set = hashMap.entrySet();
Iterator<Entry<Integer, String>> iterator = set.iterator();
while (iterator.hasNext()) {
Entry entry = iterator.next();
Integer key = (Integer) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
输出结果:
key:1,value:a1
key:3,value:a3
key:2,value:a2
key:0,value:a0
accessOrder 等于true: 保证了插入顺序. 前面的例子,默认为false的情况, 保证了访问顺序即对key进行了排序.
五.源码结构分析
LinkedHashMap 实现了Map<K,V>接口。其内部还维护了一个双向链表.
默认情况,遍历时的顺序是按照插入节点的顺序。这也是其与HashMap最大的区别。
也可以在构造时传入accessOrder参数,使得其遍历顺序按照访问的顺序输出。
LinkedHashMap的实现主要分两部分,一部分是哈希表,另外一部分是链表。
双向链表左右箭头代表前后指针. 每个节点都包含前指针和后指针.
下面源码部分可以看出, 每个节点都定义了before, after;用于维护双向链表间的节点顺序.
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);
}
}
类里有两个成员变量head tail
,分别指向内部双向链表的表头、表尾。
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
LinkedHashMap 没有重写put 方法, 但是重写了put方法中的newNode()
方法.newNode()
会在HashMap
的putVal()
方法里被调用,putVal()
方法会在批量插入数据putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
或者插入单个数据public V put(K key, V value)
时被调用。
LinkedHashMap重写了
newNode(),在每次构建新节点时,通过linkNodeLast(p);将新节点链接在内部双向链表的尾部。
在构建新节点时,构建的是LinkedHashMap.Entry
不再是Node
.
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;
}
将新增的节点链接在双向链表的尾部.
// link at the end of list
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;
}
}
LinkedHashMap 还重写了三个方法:
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
我们主要看下afterNodeAccess 方法, 该方法是在 accessOrder = true 并且 插入的当前节点不等于尾节点时,该方法才会生效。并且该方法的作用是将插入的节点变为尾节点,后面在get方法中也会调用.
// 把当前节点放到双向链表的尾部
void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//当 accessOrder = true 并且当前节点不等于尾节点tail。这里将last节点赋值为tail节点
if (accessOrder && (last = tail) != e) {
//记录当前节点的上一个节点b和下一个节点a
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//释放当前节点和后一个节点的关系
p.after = null;
//如果当前节点的前一个节点为null
if (b == null)
//头节点=当前节点的下一个节点
head = a;
else
//否则b的后节点指向a
b.after = a;
//如果a != null
if (a != null)
//a的前一个节点指向b
a.before = b;
else
//b设为尾节点
last = b;
//如果尾节点为null
if (last == null)
//头节点设为p
head = p;
else {
//否则将p放到双向链表的最后
p.before = last;
last.after = p;
}
//将尾节点设为p
tail = p;
//LinkedHashMap对象操作次数+1,用于快速失败校验
++modCount;
}
}
总结就是: 在每次插入数据,或者访问、修改数据时,会增加节点、调整链表的节点顺序。以决定迭代时输出的顺序。
六.常见问题
1.如何实现的元素有序?
在HashMap的基础上,对每一个节点添加向前向后指针,这样所有的节点形成了双向链表,自然就是有序的.
2.如何保证顺序的正确以及同步
通过重写的一些关键的方法,在元素发生增删改查等行为时,除了在Hash桶上进行操作,也对链表进行相应的更新,以此来保证顺序的正确.
3.如何实现两种顺序(插入顺序或者访问顺序)?
通过内部的标识位accessOrder(可以手动传入构造方法参数来控制)来记录当前LinkedHashMap是以什么为序,之后再处理元素时通过读取accessOrder的值来控制链表的顺序.
4.为什么重写containsValue()而不重写containsKey()?
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
containsKey()是通过hash值直接计算出该key对应的数组下标,之后在该hash桶的链表上进行查找相同的key.
containsValue()是对table进行遍历,对其中的每一个hash桶的所有值进行遍历,去寻找相同的value.
而在LinkedHashMap中, 其主干利用了哈希表即链表, 需要把传入的key 转化成哈希值再转化成索引, 很低效.
就这个问题补充下:
数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
哈希表综合了数组和链表的特点, 内部结构如下:
可以理解为带链表的数组, 哈希表是由数组+链表组成的, 长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢? 一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。
首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
下面是HashMap 的结构, 也可以理解成单向链表结构,(个人自定义的名称, 便于理解勿喷)
而LinkedHashMap 的双向链表结构如下:(作者自涂鸦)
红色箭头表示节点Entry 之间是双向的, 回到主题, 为什么LinkedHashMap不重写containsKey()?
比如我们拥有 1 处 的key 和value, 从1 -> 2, 需要经历3个步骤
1.把key 转换成hash 值
2.把hash值转换成索引,方便在主干查找到对应的数组下标.(描述不准确,是这个意思)
3.没完呢, 还得根据key 或者是转化后的hash去查找所在下标处的Entry.
三个步骤等于回到了ArrayList 的原始数组时代.
再次重申,便于理解,高手请指教勿喷.
七.常用方法
常用方法请参照前面的章节HashMap.