LinkedHashMap(附源码)
字数: 0 字 时长: 0 分钟
假如我们需要一个按照插入顺序来排列的键值对集合,那 HashMap 就无能为力了,这时候就要用到 LinkedHashMap。
为了提高查找效率,HashMap 在插入的时候会对键进行哈希计算,这也导致 HashMap 本身并不能保证元素的插入顺序。
那怎么保证键值对的插入顺序呢?
LinkedHashMap 就是为这个需求应运而生的。
LinkedHashMap 继承了 HashMap,所以 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;
Entry<K, V> after;
Entry(
int hash,
K key,
V value,
Node<K, V> next) {
super(
hash,
key,
value,
next
);
}
}用 LinkedHashMap 替换 HashMap,就可以做到有序插入。
1. 插入顺序
在 HashMap 中,null 会插入到 HashMap 的第一位。
Map<String, String> hashMap =
new HashMap<>();
hashMap.put(
"张",
"张无忌"
);
hashMap.put(
"赵",
"赵敏"
);
hashMap.put(
"韦",
"韦一笑"
);
hashMap.put(
"灭",
"灭绝师太"
);
hashMap.put(
null,
null
);
for (
String key :
hashMap.keySet()
) {
System.out.println(
key
+ " : "
+ hashMap.get(key)
);
}输出结果:
null : null
张 : 张无忌
赵 : 赵敏
韦 : 韦一笑
灭 : 灭绝师太虽然:
null是最后一个 put() 进去的,但遍历输出时却跑到了第一位。
再对比一下 LinkedHashMap:
public static void main(
String[] args) {
Map<String, String> linkedHashMap =
new LinkedHashMap<>();
linkedHashMap.put(
"张",
"张无忌"
);
linkedHashMap.put(
"赵",
"赵敏"
);
linkedHashMap.put(
"韦",
"韦一笑"
);
linkedHashMap.put(
"灭",
"灭绝师太"
);
linkedHashMap.put(
null,
null
);
for (
String key :
linkedHashMap.keySet()
) {
System.out.println(
key
+ " : "
+ linkedHashMap.get(key)
);
}
}输出结果:
张 : 张无忌
赵 : 赵敏
韦 : 韦一笑
灭 : 灭绝师太
null : nullnull 在最后一位插入,也在最后一位输出。
输出结果可以再次证明:
HashMap 是无序的,LinkedHashMap 可以维持插入顺序。
那 LinkedHashMap 是如何做到这一点的?
要想搞清楚,就需要深入研究一下 LinkedHashMap 的源码。
LinkedHashMap 并没有重写 HashMap 的 put() 方法,而是重写了 put() 方法需要调用的内部方法:
newNode()这是 HashMap 的:
Node<K, V> newNode(
int hash,
K key,
V value,
Node<K, V> next) {
return new Node<>(
hash,
key,
value,
next
);
}这是 LinkedHashMap 的:
HashMap.Node<K, V> newNode(
int hash,
K key,
V value,
HashMap.Node<K, V> e) {
LinkedHashMap.Entry<K, V> p =
new LinkedHashMap.Entry<>(
hash,
key,
value,
e
);
linkNodeLast(p);
return p;
}LinkedHashMap.Entry 继承了:
HashMap.Node并且追加了两个字段:
before
after用来维持键值对之间的关系。
static class Entry<K, V>
extends HashMap.Node<K, V> {
Entry<K, V> before;
Entry<K, V> after;
Entry(
int hash,
K key,
V value,
Node<K, V> next) {
super(
hash,
key,
value,
next
);
}
}在 LinkedHashMap 中,链表中的节点顺序按照插入顺序维护。
当使用 put() 方法向 LinkedHashMap 中添加键值对时,会把新节点插入到链表尾部,并更新:
before
after从而保证链表的顺序关系。
这个过程由:
linkNodeLast()完成。
/**
* 将指定节点插入到链表的尾部
*
* @param p 要插入的节点
*/
private void linkNodeLast(
LinkedHashMap.Entry<K, V> p) {
// 获取链表的尾节点
LinkedHashMap.Entry<K, V> last =
tail;
// 将 p 设置为新的尾节点
tail =
p;
// 如果链表为空
if (last == null) {
// p 同时也是头节点
head =
p;
} else {
// p 的前驱指向原来的尾节点
p.before =
last;
// 原尾节点的后继指向 p
last.after =
p;
}
}LinkedHashMap 添加第一个元素的时候:
head
↓
第一个元素等第二个元素添加进来以后:
第二个元素.before
↓
第一个元素
第一个元素.after
↓
第二个元素这样就保证了键值对按照插入顺序排列。
2. 访问顺序
LinkedHashMap 不仅能够维持插入顺序,还能够维持访问顺序。
访问包括调用:
get()
remove()
put()要维护访问顺序,需要在声明 LinkedHashMap 的时候指定三个参数:
LinkedHashMap<String, String> map =
new LinkedHashMap<>(
16,
0.75f,
true
);第一个参数:
初始容量第二个参数:
负载因子第三个参数如果为:
true表示 LinkedHashMap 维护访问顺序。
否则维护插入顺序。
默认值:
false来看一个例子:
Map<String, String> linkedHashMap =
new LinkedHashMap<>(
16,
0.75f,
true
);
linkedHashMap.put(
"灭",
"灭绝师太"
);
linkedHashMap.put(
"绝",
"灭绝师太"
);
linkedHashMap.put(
"师",
"灭绝师太"
);
linkedHashMap.put(
"太",
"灭绝师太"
);
System.out.println(
linkedHashMap
);
linkedHashMap.get(
"绝"
);
System.out.println(
linkedHashMap
);
linkedHashMap.get(
"师"
);
System.out.println(
linkedHashMap
);输出结果:
{灭=灭绝师太, 绝=灭绝师太, 师=灭绝师太, 太=灭绝师太}
{灭=灭绝师太, 师=灭绝师太, 太=灭绝师太, 绝=灭绝师太}
{灭=灭绝师太, 太=灭绝师太, 绝=灭绝师太, 师=灭绝师太}当使用 get() 访问键为:
绝的元素以后:
绝=灭绝师太被移动到了最后。
继续访问键为:
师的元素以后:
师=灭绝师太又被移动到了最后。
此时:
绝=灭绝师太位于倒数第二位。
也就是说:
最不经常访问的元素会逐渐位于链表头部。
这就有意思了。
有意思在哪呢?
3. LRU 缓存
可以使用 LinkedHashMap 来实现:
LRU 缓存LRU 是:
Least Recently Used的缩写,也就是:
最近最少使用它是一种常用的页面置换算法,会优先淘汰最近最久没有使用的数据。
/**
* 自定义 MyLinkedHashMap,继承 LinkedHashMap。
*
* 用于实现一个具有固定大小的缓存。
* 当缓存达到最大容量时,
* 自动移除最早加入的元素。
*
* @param <K> 键的类型
* @param <V> 值的类型
*/
public class MyLinkedHashMap<K, V>
extends LinkedHashMap<K, V> {
// 最大键值对数量
private static final int MAX_ENTRIES =
5;
/**
* @param initialCapacity 初始容量
* @param loadFactor 负载因子
* @param accessOrder 访问顺序
*/
public MyLinkedHashMap(
int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(
initialCapacity,
loadFactor,
accessOrder
);
}
/**
* 判断是否删除最早加入的元素
*
* @param eldest 最早加入的元素
* @return 超过最大容量返回 true
*/
@Override
protected boolean removeEldestEntry(
Map.Entry eldest) {
return size()
> MAX_ENTRIES;
}
}MyLinkedHashMap 是一个自定义类,它继承了 LinkedHashMap,并且重写:
removeEldestEntry()让 Map 最多保存:
5 个元素超过以后就进行淘汰。
测试一下:
MyLinkedHashMap<String, String> map =
new MyLinkedHashMap<>(
16,
0.75f,
true
);
map.put(
"灭",
"灭绝师太"
);
map.put(
"绝",
"灭绝师太"
);
map.put(
"师",
"灭绝师太"
);
map.put(
"太",
"灭绝师太"
);
map.put(
"一个固执倔强的老女人",
"一个固执倔强的老女人"
);
System.out.println(
map
);
map.put(
"一个顽固迂腐的老女人",
"一个顽固迂腐的老女人"
);
System.out.println(
map
);
map.put(
"一个爱恨分明的老女人",
"一个爱恨分明的老女人"
);
System.out.println(
map
);输出:
{灭=灭绝师太, 绝=灭绝师太, 师=灭绝师太, 太=灭绝师太, 一个固执倔强的老女人=一个固执倔强的老女人}
{绝=灭绝师太, 师=灭绝师太, 太=灭绝师太, 一个固执倔强的老女人=一个固执倔强的老女人, 一个顽固迂腐的老女人=一个顽固迂腐的老女人}
{师=灭绝师太, 太=灭绝师太, 一个固执倔强的老女人=一个固执倔强的老女人, 一个顽固迂腐的老女人=一个顽固迂腐的老女人, 一个爱恨分明的老女人=一个爱恨分明的老女人}可以看到:
灭=灭绝师太
绝=灭绝师太依次被淘汰出局。
假如在:
put("一个爱恨分明的老女人")之前访问一下键为:
绝的元素:
MyLinkedHashMap<String, String> map =
new MyLinkedHashMap<>(
16,
0.75f,
true
);
map.put(
"灭",
"灭绝师太"
);
map.put(
"绝",
"灭绝师太"
);
map.put(
"师",
"灭绝师太"
);
map.put(
"太",
"灭绝师太"
);
map.put(
"一个固执倔强的老女人",
"一个固执倔强的老女人"
);
System.out.println(
map
);
map.put(
"一个顽固迂腐的老女人",
"一个顽固迂腐的老女人"
);
System.out.println(
map
);
map.get(
"绝"
);
map.put(
"一个爱恨分明的老女人",
"一个爱恨分明的老女人"
);
System.out.println(
map
);输出结果就会发生变化:
{灭=灭绝师太, 绝=灭绝师太, 师=灭绝师太, 太=灭绝师太, 一个固执倔强的老女人=一个固执倔强的老女人}
{绝=灭绝师太, 师=灭绝师太, 太=灭绝师太, 一个固执倔强的老女人=一个固执倔强的老女人, 一个顽固迂腐的老女人=一个顽固迂腐的老女人}
{太=灭绝师太, 一个固执倔强的老女人=一个固执倔强的老女人, 一个顽固迂腐的老女人=一个顽固迂腐的老女人, 绝=灭绝师太, 一个爱恨分明的老女人=一个爱恨分明的老女人}这次:
灭=灭绝师太
师=灭绝师太被淘汰出局。
那 LinkedHashMap 是如何维持访问顺序的?
可以研究下面这三个方法:
void afterNodeAccess(
Node<K, V> p) {
}
void afterNodeInsertion(
boolean evict) {
}
void afterNodeRemoval(
Node<K, V> p) {
}afterNodeAccess() 会在调用:
get()的时候被调用。
afterNodeInsertion() 会在调用:
put()的时候被调用。
afterNodeRemoval() 会在调用:
remove()的时候被调用。
以:
afterNodeAccess()为例来看一下。
/**
* 在访问节点后,将节点移动到链表尾部
*
* @param e 要移动的节点
*/
void afterNodeAccess(
HashMap.Node<K, V> e) {
LinkedHashMap.Entry<K, V> last;
// 按访问顺序排列,
// 并且当前访问节点不是尾节点
if (
accessOrder
&& (
last = tail
) != e
) {
LinkedHashMap.Entry<K, V> p =
(
LinkedHashMap.Entry<K, V>
) e;
LinkedHashMap.Entry<K, V> b =
p.before;
LinkedHashMap.Entry<K, V> 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;
}
}为什么 LinkedHashMap 可以实现 LRU 缓存,把最不经常访问的元素淘汰?
插入元素时需要调用:
put()该方法最后会调用:
afterNodeInsertion()这个方法被 LinkedHashMap 重写。
/**
* 在插入节点以后,
* 根据需要删除最早加入的元素
*
* @param evict 是否需要删除最早加入的元素
*/
void afterNodeInsertion(
boolean evict) {
LinkedHashMap.Entry<K, V> first;
if (
evict
&& (
first = head
) != null
&& removeEldestEntry(
first
)
) {
// 获取要删除元素的 Key
K key =
first.key;
// 删除元素
removeNode(
hash(key),
key,
null,
false,
true
);
}
}removeEldestEntry() 会判断第一个元素是否已经超出了允许容纳的最大范围。
如果超出:
removeEldestEntry()
↓
返回 true
↓
removeNode()
↓
删除最不经常访问的元素4. 小结
由于 LinkedHashMap 需要额外维护:
双向链表所以 LinkedHashMap 在插入、删除操作时,花费的时间会比 HashMap 多一些。
简单总结一下。
HashMap 是一种常用的哈希表数据结构,可以快速进行键值对的:
查找
插入但是 HashMap 本身并不能保证键值对的顺序。
如果需要按照:
插入顺序
访问顺序遍历键值对,就可以使用:
LinkedHashMapLinkedHashMap 继承自 HashMap,并在 HashMap 的基础上增加:
双向链表用来维护键值对的顺序。
链表可以按照:
插入顺序
访问顺序排列。
其中:
head
↓
最早插入或访问的元素
tail
↓
最晚插入或访问的元素这个链表使 LinkedHashMap 可以保持键值对的顺序,并按照顺序进行遍历。
LinkedHashMap 还可以通过构造方法指定:
插入顺序
访问顺序在按照访问顺序排列的情况下,每次访问一个键值对,都会将该键值对移动到链表尾部,从而保证最近访问的元素位于最后。
如果需要删除最早加入的元素,可以通过重写:
removeEldestEntry()实现。
总之:
LinkedHashMap 通过维护双向链表来保持键值对的顺序,可以按照插入顺序或者访问顺序遍历键值对。