十一、Map 子接口之 LinkedHashMap
- 11.1 LinkedHashMap
- 11.2 插入顺序
- 11.3 访问顺序
11.1 LinkedHashMap
在日常开发中,我们使用频率最高的键值对集合应该就是 HashMap 了,但是它也有不足之处。比如现在我需要一个按照插入顺序来排列的键值对集合,显然 HashMap 就无能为力了。
为了提高查找效率,HashMap 在插入的时候对键做了一次哈希算法,这就导致插入的元素是无序的:
i = (n - 1) & hash;
按照这个公式计算后的值并不是按照 0、1、2、3、4 这样有序的下标将键值对插入到数组当中的,而具有一定的随机性。
LinkedHashMap 继承了 HashMap,可以实现键值对按照顺序插入:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{}
另外,LinkedHashMap 内部又追加了双向链表,来维护元素的插入顺序。其中的 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);
}
}
11.2 插入顺序
在 HashMap 中,无论 null 值在第几次被插入,它都会插入到 HashMap 的第一位:
/**
* @author QHJ
* @date 2022/11/1 10:44
* @description: HashMapTest
*/
public class LinkedHashMapTest {
public static void main(String[] args) {
Map<String, String> hashMap = new HashMap<>();
hashMap.put("1", "青花椒");
hashMap.put("2", "张三");
hashMap.put("3", "李四");
hashMap.put(null, null);
for (String key : hashMap.keySet()) {
System.out.println(key + ":" + hashMap.get(key));
}
}
}
程序执行结果:
null:null
1:青花椒
2:张三
3:李四
从结果可以看出,虽然 null 是最后一个被 put 进去的,但是在遍历输出的时候是在第一位的。
对比一下 LinkedHashMap:
/**
* @author QHJ
* @date 2022/11/1 10:44
* @description: LinkedHashMapTest
*/
public class LinkedHashMapTest {
public static void main(String[] args) {
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("1", "青花椒");
linkedHashMap.put("2", "张三");
linkedHashMap.put("3", "李四");
linkedHashMap.put(null, null);
for (String key : linkedHashMap.keySet()) {
System.out.println(key + ":" + linkedHashMap.get(key));
}
}
}
程序执行结果:
1:青花椒
2:张三
3:李四
null:null
null 在最后一位插入,就在最后一位输出。这就证明,HashMap 是无序的,LinkedHashMap 是可以维持插入顺序的。
扒 LinkedHashMap 的源码就会发现,它并没有重写 HashMap 的 put() 方法,而是重写了 put() 方法里面需要调用的 newNode()
方法:
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;
}
在上面的介绍中我们知道,LinkedHashMap.Entry 继承了 HashMap.Node,并且追加了两个字段 before 和 after。然后来扒一下 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;
}
}
在添加第一个元素的时候,会把 head 赋值为第一个元素,等到第二个元素添加进来的时候,会把第二个元素的 before 赋值给第一个元素,第一个元素的 after 赋值为第二个元素。这样就保证了键值对是按照顺序排列的。
11.3 访问顺序
LinkedHashMap 不仅能够维持插入顺序,还能够维持访问顺序。访问包括调用 get() 方法、remove() 方法和 put() 方法。
要维护访问顺序的话,就需要我们在声明 LinkedHashMap 的时候指定三个参数(初始容量、负载因子、是否指定访问顺序)
:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
如果第三个参数指定为 true 的话,就表示 LinkedHashMap 要维护访问顺序;否则,维护插入顺序。默认是 false。
/**
* @author QHJ
* @date 2022/11/1 10:44
* @description: LinkedHashMapTest
*/
public class LinkedHashMapTest {
public static void main(String[] args) {
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
linkedHashMap.put("1", "青花椒");
linkedHashMap.put("2", "张三");
linkedHashMap.put("3", "李四");
linkedHashMap.put(null, null);
System.out.println(linkedHashMap); // {1=青花椒, 2=张三, 3=李四, null=null}
linkedHashMap.get("2");
System.out.println(linkedHashMap); // {1=青花椒, 3=李四, null=null, 2=张三}
linkedHashMap.get("1");
System.out.println(linkedHashMap); // {3=李四, null=null, 2=张三, 1=青花椒}
}
}
当我使用 get() 方法访问键位 “2” 的元素后,输出结果中 “2=张三” 在最后;当我使用 get() 方法访问键位 “1” 的元素后,输出结果中 “1=青花椒” 在最后,“2=张三” 在倒数第二位。
也就是说,最不经常访问的放在头部
。
我们可以使用 LinkedHashMap 来实现 LRU 缓存,LRU 是 Least Recently Used 的缩写,即最近最少使用,也是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
自定义一个 MyLinkedHashMap 类,让它继承 LinkedHashMap,并且重写 removeEldestEntry() 方法——使 Map 最多可容纳 5 个元素,超出后就淘汰:
/**
* @author QHJ
* @date 2022/11/1 16:36
* @description: MyLinkedHashMap
*/
public class MyLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 5;
public MyLinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor, accessOrder);
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
}
测试类:
/**
* @author QHJ
* @date 2022/11/1 16:39
* @description: MyLinkedHashMapTest
*/
public class MyLinkedHashMapTest {
public static void main(String[] args) {
MyLinkedHashMap<String, String> map = new MyLinkedHashMap<>(16, 0.75f, true);
map.put("1", "青花椒");
map.put("2", "张三");
map.put("3", "李四");
map.put("4", "王五");
map.put(null, null);
System.out.println(map); // {1=青花椒, 2=张三, 3=李四, 4=王五, null=null}
map.put("beautiful", "美丽的程序猿");
System.out.println(map); // {2=张三, 3=李四, 4=王五, null=null, beautiful=美丽的程序猿}
map.put("cool", "酷酷的程序猿");
System.out.println(map); // {3=李四, 4=王五, null=null, beautiful=美丽的程序猿, cool=酷酷的程序猿}
}
}
由程序执行结果可以看出来,“1=青花椒” 和 “2=张三” 依次被淘汰出局。
那 LinkedHashMap 是如何来维持访问顺序的呢?主要就是依靠这三个方法:
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
afterNodeAccess() 会在调用 get() 方法的时候被调用,afterNodeInsertion() 会在调用 put() 方法的时候被调用,afterNodeRemoval() 会在调用 remove() 方法的时候被调用。
扒一下 afterNodeAccess()
方法的源码:
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;
}
}
afterNodeAccess() 方法其实就是哪个元素被 get 就把哪个元素放在最后。
在插入元素的时候,需要调用 put() 方法,该方法最后会调用 afterNodeInsertion()
方法,这个方法被 LinkedHashMap 重写了:
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() 方法会判断第一个元素是否超过了可容纳的最大范围。如果超出,就会调用 removeNode() 方法对最不经常访问的那个元素进行删除。
在主动调用 remove() 方法进行删除的时候,会调用 removeNode() 方法,removeNode() 方法里会继续调用 afterNodeRemoval()
方法:
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
由于 LinkedHashMap 要维护双向链表,所以在插入、删除操作的时候,花费的时间要比 HashMap 多一些。