一、HashMap遍历时为什么不是有序的?
参考上一篇文章java8 HashMap接口实现,HashMap遍历的时候是按照哈希桶数组来遍历的,如果哈希桶内包含多个元素则遍历桶内所有元素后再遍历下一个哈希桶。而元素的插入时是根据key的hash值来随机分布到哈希桶中的,并且同一个桶内的元素由于扩容或者转换成红黑树结构,顺序也会不断发生改变。所以HashMap遍历时顺序是不固定的,跟插入顺序和访问顺序等都无关。那么实际应用场景需要一个维护遍历顺序的Map怎么办呢?有LinkedHashMap.
二、LinkedHashMap概述
LinkedHashMap继承自HashMap,是在HashMap的基础上通过维护一个双向链表来保证以插入或者访问顺序来遍历Map中的元素,可以在此基础上实现LRU Cache。跟HashMap一样允许key/value为null,非线程安全,迭代时修改会快速失败,注意:
1、对插入顺序而言,如果一个key重复插入,该key在插入顺序中是不变的,在访问顺序中是变的。
2、无法通过Map copy = new LinkedHashMap(m),m为任意的Map接口实现类,来获取跟原Map顺序一样的LinkedHashMap
3、LinkedHashMap因为额外的维护了一个双向链表所以性能比HashMap略低,LinkedHashMap迭代的性能跟元素的个数而不是容量成正相关,HashMap迭代的性能跟容量是正相关的。
参考如下用例:
@Test
public void test() throws Exception {
//默认的插入顺序
Map<String,Integer> test=new LinkedHashMap<>();
test.put("1", 1);
test.put("2", 1);
test.put("3", 1);
//重新插入,原有的顺序不变
test.put("2", 1);
test.get("3");
for(String key:test.keySet()){
System.out.println(key);
}
}
@Test
public void test2() throws Exception {
//按照访问顺序遍历,最后被遍历的元素就是最近被访问到的元素
Map<String,Integer> test=new LinkedHashMap<>(64,0.75f,true);
test.put("1", 1);
test.put("2", 1);
test.put("3", 1);
test.get("1");
//重新插入访问顺序会变
test.put("2", 1);
for(String key:test.keySet()){
System.out.println(key);
}
}
类继承关系如下图:
三、数据结构
/**
* 增加两个属性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);
}
}
注意HashMap中的红黑树实现TreeNode是LinkedHashMap.Entry的子类,但是在HashMap中并未处理before,after这两个属性,之所以设计这种继承关系,是为了确保当HashMap的存储结构变成红黑树以后,依然能够维持双向链表。另外必须明确的是,LinkedHashMap中的双向链表关系,HashMap单个哈希桶内的单向链表或者红黑树关系,这三者都是彼此独立的,只是通过类属性的引用关系将不同实例串联起来而已。
四、类属性
/**
* 双向链表的头部
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 双向链表的尾部
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 遍历元素的顺序,如果是true按照访问顺序,如果是false则按照插入顺序
* @serial
*/
final boolean accessOrder;
五、类构造方法
基本跟HashMap一致,只是增加了一个accessOrder的属性而已,该属性默认false,即默认按照插入顺序遍历。
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
//默认情况下accessOrder都为false,即按照插入顺序维护遍历顺序
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
//只有这一种方法可以显示指定按照访问顺序迭代,其他都是默认的按照插入顺序
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
六、双向链表实现
其实现的核心在于重写了下列几个方法:
这个几个方法在HashMap中的作用如下:
newNode方法:插入元素时,构造一个新的单向链表的Node
replacementNode方法: 当由红黑树转换成单向链表时,将原来的TreeNode转换成单向链表Node
newTreeNode方法: 插入元素时,构造一个红黑树节点元素TreeNode
replacementTreeNode方法:当单向链表转换为红黑树时,将原来的单向链表Node转换为TreeNode
afterNodeAccess,afterNodeInsertion,afterNodeRemoval方法:分别表示元素被访问,元素插入,元素移除时的回调方法,在HashMap中是空实现。
改写后的方法如下:
// 插入新元素到链表末尾
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;
}
}
// 将元素src的链式关系同步到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;
}
//将原来的Node替换成LinkedHashMap.Entry,并维护链式关系
//改写后的newNode方法和newTreeNode方法就可以保证每次插入元素都会将该元素移动到双向链表尾部
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;
}
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;
}
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
//注意TreeNode是继承自LinkedHashMap.Entry的
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}
//从链式关系中删除节点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;
//该元素是尾元素
if (a == null)
tail = b;
else
a.before = b;
}
//按需删除最早插入的一个元素
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//removeEldestEntry默认返回false,可以被子类改写,如果实现LRU Cache,可以返回true
//把最老的没有被访问的元素移除掉
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//通过afterNodeAccess方法维护访问顺序,每次访问该元素就将该元素移动到双向链表的末尾
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放在last的后面
p.before = last;
last.after = p;
}
tail = p;
//注意此时modCount会自增
++modCount;
}
}
因为afterNodeAccess方法执行的过程中会修改modCount,所以遍历的时候不能执行会触发afterNodeAccess方法,参考如下用例:
@Test
public void test2() throws Exception {
//按照访问顺序遍历,最后被遍历的元素就是最近被访问到的元素
Map<String,Integer> test=new LinkedHashMap<>(64,0.75f,true);
test.put("1", 1);
test.put("2", 1);
test.put("3", 1);
test.get("1");
//重新插入访问顺序会变
test.put("2", 1);
for(String key:test.keySet()){
System.out.println(key);
//抛出异常
// test.remove("2");
//抛出异常
// test.replace("3",3);
//不抛出异常,因为2已经是双向链表最后一个元素了
test.replace("2",3);
}
}
注意会回调afterNodeAccess方法的方法如下:
对HashIterator的改写保证迭代顺序为双向链表的顺序:
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
//从双向链表的头元素开始遍历
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
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;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}