LinkedHashMap继承了HashaMap,它拥有HashMap的所有特性。LinkedHashMap是LinkedList与HashMap的结合体。通过双向链表来保持迭代顺序,正是因为这个特性,LinkedHashMap提供了空的一个方法,重写此方法可以很好的实现Lru算法。
LinkedHashMap
LinkedHashMap默认是按照插入顺序进行排序的,它的本质还是HashMap和双向链表的结合体,它是一个将所有节点链入双向链表的HashMap。所有的put进来的节点在插入到哈希表中后,由于又定义了一个双向链表,其节点还会插入到双向链表的尾部。LinkedHashMap的特性和HashMap相同,比如最多允许一个键为空,多个value为空。它也是线程不安全的。
LinkedHashMap属性
LinkedHashMap多了三个属性,分别是双向链表的头节点和尾节点,标志符accessOrder。
// 双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
// 双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 默认是false,代表按照插入顺序迭代,如果为true,代表按照访问顺序迭代
final boolean accessOrder;
LinkedHashMap重新定义了Entry,增加了befaore,after,用于节点插入的先后顺序,next是用来保存HashMap桶中节点的后续节点。
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);
}
}
LinkedHashMap构造方法
LinkedHashMap一共有5个构造方法,是在HashMap的基础上实现的。
// 构造指定容量大小和负载因子的LinkedHashMap
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
// 构造指定初始容量大小和默认负载因子的LinkedHashMap
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
// 默认无参的构造函数
public LinkedHashMap() {
super(); // 调用父类构造函数
accessOrder = false; // 默认为false,表示按插入顺序迭代
}
// 初始化负载因子,把m中的元素都添加到HashMap中
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
// 构造一个指定容量大小和负载因子,指定迭代顺序的LinkedHashMap
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
put方法
LinkedHashMap的put继承了HashMap的put方法,只不过重写了里面的afterNodeAccess和afterNodeInsertion方法,在HasnMap中已经分析了put方法,这里直接看一下LinkedHashMap自己重写的方法。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p = // LinkedHashMap是调用自己的Entry类
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { // 把新添加的节点追加到双向链表的链尾
LinkedHashMap.Entry<K,V> last = tail; // 将链表节点赋给last
tail = p; // 把当前节点作为链尾节点
if (last == null) // 如果链尾为空,代表链表为空,这是第一个添加的节点,则直接把当前节点=head = last
head = p;
else { // 链表不为空,直接把当前作为链尾节点
p.before = last; // 把last作为当前节点的前节点
last.after = p; // 当前节点作为last的后节点,这样通过改变节点的引用就把当前节点作为链尾
}
}
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) { // 如果是TreeNode节点也是相同的操作
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
void afterNodeInsertion(boolean evict) { // 插入节点后把最老的节点删除,但是removeEldestEntry方法总是返回false,
LinkedHashMap.Entry<K,V> first; // 并不会删除节点,如果想实现,让子类去重写该方法,给定一个条件来控制删除最老的节点
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) { // 把最近访问的节点放到链尾
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) { // accessOrder如果为true,并且当前的节点不是尾节点
LinkedHashMap.Entry<K,V> p = // 找到当前节点的前节点和后节点
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null; // 把当前节点的后节点置为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;
}
}
当accessOrder为true的时候,会调用这个方法,把当前节点放到链尾。
get方法
get方法调用了HashMap的getNode方法,如果没有找到该节点,直接返回null,否则,对accessOrder进行判断,如果accessOrder为true,表示按照访问顺序排序,调用afterNodeAccess方法,把当前节点放到链尾。
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;
}
remove方法
remove方法在HashMap中已经删除了节点,接下来LinkedHashMap重写afteNodeRemoval方法来移除双向链表中的节点void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p = // p为要删除的节点
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null; // 把p的前后节点都置为空,偏于GC
if (b == null) //如果p的前节点为空,p的后节点直接作为头节点
head = a;
else // 如果p前节点不为空,p的后节点变为p的前节点的后节点
b.after = a;
if (a == null) // 如果p的后节点为空,p的前节点作为尾节点
tail = b;
else // 如果p的后节点不为空,p的前节点变为p的后节点的前节点
a.before = b;
}
Lru算法
当使用LinkedHashMap实现LRU算法时,需要调用其构造方法把accessOrder设为true,这样就开启了访问顺序排序的模式。put方法或是get方法,put方法是把key相同的节点放到链尾,get方法需要把accessOrder设为true,才能把刚找到的节点放到链尾,多次put或get后,这样头节点就是最长没被访问的节点,当节点个数满了时候,直接删除最少使用的节点即头节点即可。下面是一个实现LRU的demo
public static void main(String[] args) {
LRU<String, Integer> lru = new LRU<>(16, 0.75f, true);
lru.put("java", 1);
lru.put("python", 2);
lru.put("golang", 3);
lru.put("c++", 4);
lru.put("node.js", 5);
System.out.println("LRU的大小:"+lru.size());
System.out.println("LRU:"+lru);
}
public static class LRU<K, V> extends LinkedHashMap<K, V> implements Map<K, V>{
public LRU(int cap,float loadFactor,boolean accessOrder) {
super(cap, loadFactor, accessOrder);
}
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
if (size() > 3) {
return true;
}
return false;
}
}
运行结果为
LinkedHashMap虽然不如HashMap使用频率高,但也是一个重要的实现,我们还是应该对这种实现思想好好学习的。