前言
项目上遇到过这样一个问题,需要对map集合中的对象按顺序进行存储。当时也没想那么多,直接一顿乱造,手写了个MyGoodMap,此类在继承了TreeMap的基础上,改写了比较器计算方式,使其变成始终一边倒的tree,很是高兴
最近偶然的机会,碰到了LinkedHashMap这个名词。看到这个词的第一眼,有种莫名的嘲讽在心中回荡,莫非…,我赶忙查阅了一下,没错,正是我要的类:可以按照顺序存储并查询的Map。二话不说,直接把MyGoodMap这个败类替了下来(太丢我的脸)
今天决定研究一下,LinkedHashMap底层如何保证有序
HashMap的无序(微讲解便于后面理解,可选择跳过)
在研究之前,有必要了解一下HashMap底层是如何进行存储,如何查询遍历的
HashMap顾名思义按照hash算法进行散列存储对象的
HashMap散列方式:
散列值 = 要添加的键的hashCode % map大小
HashMap中的对象,底层都存储在一个Node<K,V>[]数组中(Node<K,V>封装了要存储的键值对,其实底层继承了Map.Entry,明白了吧),而上面得到的散列值,就对应了数组的下标,而要添加的键值呢,就存储在对应当前数组下标对应的Node单项链表中(下面解释为什么成了链表)。比如散列值=5,那么当前键值则存储在node[5]中
public class HashMap{
...
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; //指向下一个node
...
}
}
数组中不是存的Node吗,怎么成了链表?原来,每个node对象都存在一个next属性(上图),不断指向下一个散列值相同的node,数组中实际存储的是链表中第一项,这样保证了所有相同散列值的node都能对应数组下标
我们来看put操作:
public V put(K key, V value) {
//调用下方代码
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//存储所有对象的底层数组
Node<K,V>[] tab;
//当前要添加的对象(封装了键和值)
Node<K,V> p;
//1.判断当前 散列值 对应数组下标的内容是否存在
if ((p = tab[i = (n - 1) & hash]) == null)
//若不存在,则创建新node,作为当前下标的第一项(此处是接下来的重点!!!)
tab[i] = newNode(hash, key, value, null);
else {
...
for (int binCount = 0; ; ++binCount) {
//2.如果数组中当前下标位置存在对象,
// 则将当前对象置于当前下标位置对应链表的尾部
// 此处,不断遍历node下一个指向对象,直到获取到最后一个
if ((e = p.next) == null) {
//添加至尾部(下面的重点!!!)
p.next = newNode(hash, key, value, null);
...
}
...
p = e;
}
...
}
...
}
根据上面代码,每次添加新的键值时,都会创建一个新的node,添加至node[]数组中(无论是作为真正存储的对象,也就是链表头部head,或是head.next关联的node)
看到这你可能有些乱(没关系),这乱七八糟的存储方式,是如何进行查询的呢?我们看一下底层迭代器如何遍历:
public void forEach(BiConsumer<? super K, ? super V> action) {
...
//1.遍历数组中的每一项(注意每一项对应了一个链表)
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
...
}
还是很乱?就是很乱,以上遍历毫无顺序,完全根据散列位置进行遍历(看不懂没关系,知道乱就行),接下来咱们进入正题
LinkedHashMap的有序
LinkedHashMap继承了HashMap,它对HashMap进行了不大的改动
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
那么LinkedHashMap到底如何实现有序的呢?
首先,对HashMap中的Node进行了拓展,定义了新的内部类Entry<K,V>
public class LinkedHashMap {
//静态内部类
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);
}
}
加入了before,after两个属性,看样子LinkedHashMap打算自身维护一个双向链表
接下来让我们看看Entry对象的创建
//这是HashMap中的newNode方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
//LinkedHashMap中覆盖了原有的newNode方法
@Override
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);
//将当前node(entry)添加至链表尾部
//注意:此处的链表是LinkedHashMap自身维护的双向链表
linkNodeLast(p);
return 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;
}
}
由此可以看出,LinkedHashMap每每在创建Entry时(也就是HashMap中的node),都会通过linkNodeLast方法偷偷维护一下自己的双向链表,毫无疑问,它一定也覆盖了HashMap中的forEach遍历方法:
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
...
//仅遍历自己的双向链表,完全摒弃数组
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.key, e.value);
...
}
再看HashMap原有的forEach:
public void forEach(BiConsumer<? super K, ? super V> action) {
...
//通过数组下标+单向链表遍历
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
...
}
破案,原来LinkedHashMap完全摒弃了原有底层数组维护的遍历方式,仅仅通过自己维护的双向链表进行不断向下遍历
最后
本人第一篇博客,一定存在很多问题,还请指正
173




被折叠的 条评论
为什么被折叠?



