我们总说哈希表(HashMap)是以O(1)的时间插入,然后遍历时是无序的,也就是说,hash过后就不一定插在了数组什么位置了。这样导致我们无法保证获取到的键顺序。
但是LinkedHashMap就不一样,按照我们的理解,里面应该还有一个线性结构,来保存HashMap的插入顺序,以至于遍历时能够获得按插入顺序得到的顺序。
接下来我们就分析一下它的源码。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
//。。。
}
首先,我们可以看出实际上LinkedHashMap是HashMap的子类,那么按照继承的原则,只要看看哪些被修改了即可。
在我的.java文件(我的是jdk11)中,相比于HashMap的2400+行代码,LinkedHashMap的700+行代码显得更容易阅读。
下面是三个增加的字段:
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder;
因为大多数的数组之类的变量已经继承来了,所以本质上需要的head、tail不用说一定是链表的头、位,按照注释上的描述,这里面都是双端链表。
LinkedHashMap包含更多的构造器:
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
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;
}
能够看出大多数都是把构造参数传递给父类的构造器而已。
那么,assessOrder是干什么用的呢?我们先不看源码,做个小实验:
public static void main(String[] args){
LinkedHashMap<String,Integer> map=new LinkedHashMap<>(16,0.75f,true);//默认assessOrder=false
map.put("one",1);
map.put("two",2);
map.put("four",4);
map.put("five",5);
map.put("ten",10);
System.out.println(map);
map.get("five");
System.out.println(map);
map.get("ten");
System.out.println(map);
//{one=1, two=2, four=4, five=5, ten=10}
//{one=1, two=2, four=4, ten=10, five=5}
//{one=1, two=2, four=4, five=5, ten=10}
}
然后等于false时:
public static void main(String[] args){
LinkedHashMap<String,Integer> map=new LinkedHashMap<>(16,0.75f,false);//默认assessOrder=false
map.put("one",1);
map.put("two",2);
map.put("four",4);
map.put("five",5);
map.put("ten",10);
System.out.println(map);
map.get("five");
System.out.println(map);
map.get("ten");
System.out.println(map);
//{one=1, two=2, four=4, five=5, ten=10}
//{one=1, two=2, four=4, five=5, ten=10}
//{one=1, two=2, four=4, five=5, ten=10}
}
哦天哪,好像与某个数据结构很像啊,似乎是。。。LRU?
对!assessOrder=true时,每次用get访问到的键都会被放到链表顺序的最末端,可以造成缓存的效果!
接下来看插入函数
。。。。。我们没有看到put函数!
说明LinkedHashMap的put完全与HashMap的一模一样,看来决定他的有序访问并不是插入时造成的,而是由get方法决定的。
get函数:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)//调用父类get
return null;
if (accessOrder)//开启LRU效果时候的动作——把访问到的结构放到末端
afterNodeAccess(e);
return e.value;
}
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;
}
}
那么假如说最常见的情况:无参数构造,然后多次插入/修改,是如何在遍历时得到一个有序的序列呢?
事实上遍历有序是由迭代器完成的,我们看一下里面的一个子类:
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;
removeNode(p.hash, p.key, null, false, false);
expectedModCount = modCount;
}
}
那么我们不禁奇怪了,最开始head不是空的嘛,如果assessOrder=false那么似乎没有机会获得一个完整的已经连接好的链表
其实这里面连接的过程非常隐秘:
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;
}
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
//覆盖了父类HashMap的newNode方法
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<>(hash, key, value, e);
linkNodeLast(p);//在这个地方调用了上面的方法
return p;
}
所以这个过程是借用子类方法覆盖,创建新的节点时额外的利用指针把这些节点按顺序“串”起来。
所以:
没有用额外空间!
没有用额外空间!
没有用额外空间!
所以删除也不是咱们想象的,要搜索链表什么的,都是简单的指针操作。
迭代器也没有新建一个链表,而是利用
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来迭代保持有序。
所以这个还真的是容易分析呢,也破除了原有的旧观点,这个就看出了继承的强大,使得那么复杂的LikedHashMap继承了HashMap的代码使得代码数量大大减少!