PS: 要先了解HashMap的实现原理HashMap源码分析
一、简单介绍
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
可以看到LinkedHashMap继承了HashMap,其实际是在HashMap基础上,把数据节点连成一个双向链表,遍历的时候按链表顺序遍历。
小总结
默认的LinkedHashMap 的遍历会按照插入的顺序遍历出来,HashMap则不保证顺序。
注意上面是默认的情况,LinkedHashMap中还有个accessorder成员标志,默认是false,当为true时,每get一个元素,都会把这个元素放在链表最后,即遍历的时候就变成最后被遍历出来。
二、源码分析
//通过继承HashMap.Node类,新增两个成员,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的Node类,新增了两个成员before,after,用于实现链表结构。
类成员
//指向链表的头结点
transient LinkedHashMap.Entry<K,V> head;
//指向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
//为false时,遍历会按插入的顺序遍历
//为true时,每一次get操作,都会把获得的节点放到链表尾
//默认为false
final boolean accessOrder;
接下来就是分析从增删查来读LinkedHashMap源码。
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
这三个方法是父类HashMap留给子类实现的方法,分别在get、put、remove方法完成后调用
1. 新增操作
一开始笔者想找put方法,但发现其并没有重写父类的put方法,转去找Entry类在哪里使用到。找到以下两个方法。
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;
}
//红黑树结构时用到
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);//连接到链表尾部
return p;
}
上面两个方法是重写父类的,父类HashMap的put方法中,如果是一个当前不存在的key,就会根据不同结构分别调用上面两个方法来创建新节点,这样就能把父类HashMap里的节点换成LinkHashMap中的通过“改装”的节点了。
接下来看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;//旧的尾节点下一个节点指向新的尾节点
}
}
就是把新增的节点放在链表最后,如果链表一开始为空,那就赋值给头结点
再看看afterNodeInsertion()方法
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
由于removeEldestEntry()方法总是返回false,所以该方法等于没有做任何操作。
小总结
LinkedHashMap的新增操作通过父类HashMap来完成,它则是通过定以义一个Entry继承父类Node的,然后利用Entry实现了链表的结构。
2. 查操作
2.1 get操作
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);//调整链表
return e.value;
}
getNode()方法在父类HashMap中实现了,LinkedHashMap重用其来查询数据,然后根据accessOrder的值来决定是否需要调整链表.
下面看afterNodeAccess()方法的做法
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
//进入条件:accessOrder为true,且尾节点不等于e
LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e,
b = p.before, a = p.after;
//p就是节点e,b是节点的前节点,a是e的后节点
p.after = null;//先把p的节点赋值为null
if (b == null)//说明p是头结点
head = a;
else
b.after = a;//把p的前节点连上p的后节点
if (a != null)//这里肯定不为null吧??
a.before = b;//把p的后节点连上p的前节点
else
last = b;
if (last == null)
head = p;
else {
p.before = last;//把p的前节点指向尾节点
last.after = p;//把旧尾节点的后节点指向p
}
tail = p;//尾节点赋值为p
++modCount;
}
}
afterNodeAccess()方法做的就是,把传入节点e,移动到链表尾部。
2.2 遍历操作
来看看其遍历如何实现,挑entrySet方法来说,先回顾下遍历写法
Iterator it = map.entrySet().iterator();
while(it.hasNext())
it.next();
追踪其源码如下
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() {
return nextNode();
}
}
可以看到map.entrySet().iterator()这一句代码返回的是LinkedEntryIterator对象。而其next()方法,则是调用了nextNode()方法,该方法在父类LinkedHashIterator中实现了
LinkedHashIterator() {
next = head;//next一开始指向链表头结点
expectedModCount = modCount;
current = null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
//next一开始是指向head(即指向链表头)
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)//快速失败
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;//next指向下一个节点
return e;
}
可以看到遍历如前文所说,是遍历链表。
3. 删除操作
LinkedHashMap没有重写remove方法,而是重写了afterNodeRemoval方法
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e,
b = p.before, a = p.after;
//把p的前后指向赋值为null
p.before = p.after = null;
if (b == null)//说明是头结点
head = a;
else
b.after = a;//p原来的前节点指向p原来的后节点
if (a == null)//说明p是尾节点
tail = b;
else
a.before = b;//p原来的后节点指向p原来的前节点
}
LinkedHashMap重用了HashMap的remove方法,然后在afterNodeRemoval方法中删除链表中相应的节点
三、实现LRU算法(leetcode的一道题)
利用LinkedHashMap的数据结构特性,可以简便地实现LRU算法。
class LRUCache {
private LinkedHashMap<Integer,Integer> data;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
//这里要指定第三个参数为true
data=new LinkedHashMap<>(capacity,1,true);
}
public int get(int key) {
//get()方法会自动调整链表
Integer o = data.get(key);
return o!=null?o:-1;
}
public void put(int key, int value) {
//这里要调用一次get方法,一来可以检查当前key是否存在
//二来可以调整其在链表位置的位置
if(data.get(key)==null&&data.size()==capacity){
data.remove(data.keySet().iterator().next());
}
data.put(key,value);
}
}
这里只展示了实现插入整数,可改进为泛型使其更加通用。