LinkedHashMap学习笔记
前几天看到一篇关于面试问题的博客,里面提到LinkedHashMap和TreeMap区别于HashMap的地方有一点就是是否有序,当时不甚明白,因为觉得LinkedHashMap用的是链表结构的,不知道是如何实现有序的,所以去看了看源码和一些文章。
基本结构
LinkedHashMap继承自HashMap,内部类Entry也是继承自HashMap.Node<K,V>,但是里面定义了两个独有的元素:Entry<K,V> before, after;
,有序就是通过这两个元素来实现。
实现方式
LinkedHashMap实现的基本思想就是对多态
的使用。它许多方法都是使用的HashMap中已有的方法,通过对方法中调用的子方法进行复写,通过动态绑定,实现方法重用。
1.内部属性
在我所看的1.8版本的jdk中,定义了一个静态节点元素Entry,以及这个链表的头元素head和尾元素tail,这两个元素是用来方便对链表进行操作的。
此外还有一个特有的属性final boolean accessOrder
,这是一个用来确定LinkedHashMap的顺序的值,若初始化时确定为false代表按照插入顺序,true表示按照最后操作顺序。在调用构造方法进行初始化时,如果没有指定这个值,默认都是为false。而且构造方法都是调用父类的构造方法来实现的。
2.有序实现
我们在此先不讨论accessOrder为true的情况,单说按照插入顺序访问的实现方式。
刚才有提到Entry节点比父类中的多了两个指针,before和after,看到这两个指针时大概就能想到实现按照插入顺序的访问应该是在插入节点时记录他的before和after两个值。知道了它的大概方式,我们来找它的具体实现。
观察源码可以发现LinkedHashMap中关于元素存取,值重写了get方法,并没有重写put方法。之所以会重写get方法是因为要实现按照元素最后访问顺序的有序方式。我们来到父类HashMap中,当桶中相应位置没有元素时:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
当hash值对应位置有元素存在时,且新元素的key不跟其他元素相同时,在链表末尾将新元素加入:
Node<K,V> e; K k;
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
观察代码发现新节点的加入使用的是newNode方法,这个方法在子类中是重写了的,所以会调用子类中的newNode方法,所以我们又回到了LinkedHashMap中。
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;
}
这个方法会new一个Entry,通过linkNodeLast将其顺序确定下来,然后返回一个Entry类型的元素。所以其实有序的实现是放在了linkNodeLast这个方法中。
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;
}
}
这个方法用之前定义的tail来操作,新建一个last元素保存tail的引用,将传入的元素设置为最后一个,然后确定两个元素之间的指向问题。这样就将插入的顺序记录了下来。
另外,accessOrder=true的情况也是对这个指针的指向的修改,通过将最后访问的节点改到tail的位置,本文不作深入的探讨。
3.迭代器遍历
LinkedHashMap内部定义了一个内部抽象类,LinkedHashIterator,里面定义了相关的实现。
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;
}
遍历时使用after来改变指向,就能够按照顺序来访问了。它的子类有LinkedKeyIterator,LinkedValueIterator,LinkedEntryIterator三个,都实现了Iterator接口,使用时能够自动根据数据类型动态绑定相应的方法进行遍历的实现。