一、类结构源码
HashMap 是无序的,TreeMap 可以按照 key 进行排序,LinkedHashMap 本身是继承 HashMap 的,所以它拥有 HashMap 的所有特性。
另外还提供二个特性
- 按照插入顺序进行访问;
- 实现了访问最少最先删除功能,其目的是把很久都没有访问的 key 自动删除
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
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);
}
}
private static final long serialVersionUID = 3801124242820219131L;
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder;
-
LinkedHashMap继承关系
LinkedHashMap
继承自HashMap
,同时实现了Map
接口。这意味着它除了继承了HashMap
的所有功能外,还额外添加了一些自身的特性。
-
内部Entry类
Entry
是LinkedHashMap
的一个静态内部类,它继承自HashMap.Node
。- 相比于
HashMap.Node
,Entry
多了两个额外的引用before
和after
,用于维护双向链表的结构。
-
head和tail引用
head
引用指向双向链表的表头(最老的Entry)。tail
引用指向双向链表的表尾(最新的Entry)。- 通过这两个引用,可以高效地访问链表的头部和尾部。
-
accessOrder标志位
accessOrder
是一个final的boolean类型变量。- 当
accessOrder
为true时,表示按访问顺序(access-order)维护Entry的顺序;当为false时,表示按插入顺序(insertion-order)维护Entry的顺序。
-
实现原理
LinkedHashMap
在HashMap
的基础上,额外维护了一个双向链表,用来保存Entry的插入或访问顺序。- 当新的Entry被插入时,会被追加到链表的尾部;当某个Entry被访问时(get或put操作),会被移动到链表的尾部。
- 这种方式使得
LinkedHashMap
可以高效地维护Entry的插入或访问顺序,而不需要额外的排序操作。
二、如何初始化
LinkedHashMap(int initialCapacity)
public LinkedHashMap(int initialCapacity) {
super(initialCapacity); // 调用HashMap构造器指定初始容量
accessOrder = false; // 默认按插入顺序维护Entry顺序
}
该构造函数创建一个按插入顺序维护Entry顺序的LinkedHashMap
实例,初始容量由initialCapacity
参数指定。
LinkedHashMap()
public LinkedHashMap() {
super(); // 调用HashMap无参构造器
accessOrder = false; // 默认按插入顺序维护Entry顺序
}
该无参构造函数创建一个按插入顺序维护Entry顺序的LinkedHashMap
实例,使用默认的初始容量(16)和加载因子(0.75)。
LinkedHashMap(Map<? extends K, ? extends V> m)
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(); // 调用HashMap无参构造器
accessOrder = false; // 默认按插入顺序维护Entry顺序
putMapEntries(m, false); // 将给定Map的映射关系添加到当前Map
}
该构造函数创建一个按插入顺序维护Entry顺序的LinkedHashMap
实例,并将给定Map
中的所有映射关系添加到新创建的LinkedHashMap
中。它使用默认的初始容量和加载因子。
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor); // 调用HashMap构造器指定初始容量和加载因子
this.accessOrder = accessOrder; // 设置访问顺序标志
}
该构造函数创建一个LinkedHashMap
实例,初始容量由initialCapacity
指定,加载因子由loadFactor
指定。accessOrder
参数决定了是按插入顺序还是按访问顺序维护Entry的顺序。
无论使用哪个构造函数,LinkedHashMap
都会继承HashMap
的特性,如自动扩容、桶的链表存储等。
三、put新增源码
- 看如图put是调用HashMap的方法
- HashMap中调用LinkedHashMap新增节点的newNode实现方法
3.ewNode/newTreeNode
方法,控制新增节点追加到链表的尾部,这样每次新节点都追加到尾部,即可保证插入顺序了
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;
}
//新节点插入到双向链表的尾部,并更新相应的head和tail引用以及节点之间的before和after引用。
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
// 获取当前链表的尾节点
LinkedHashMap.Entry<K,V> last = tail;
// 将新节点设置为新的尾节点
tail = p;
// 如果原来的链表为空,将新节点也设置为头节点
if (last == null)
head = p;
else {
// 将新节点的before指针指向原来的尾节点
p.before = last;
// 将原来尾节点的after指针指向新节点
last.after = p;
}
}
四、get查询
就是经常访问的元素会被追加到队尾,这样不经常访问的数据自然就靠近队头。就可以通过设置删除策略,比如当 Map 元素个数大于多少时,把头节点删除
public V get(Object key) {
Node<K,V> e;
// 调用HashMap的getNode方法获取与给定键关联的节点
if ((e = getNode(hash(key), key)) == null)
return null; // 如果节点不存在,直接返回null
// 如果设置了accessOrder为true,表示按访问顺序维护Entry顺序
if (accessOrder)
// 调用afterNodeAccess方法,将被访问的节点移动到链表尾部
afterNodeAccess(e);
return e.value; // 返回节点的值
}
//afterNodeAccess方法实现了将被访问的节点移动到双向链表尾部的功能,从而维护了Entry的访问顺序
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
// 如果设置了accessOrder为true,并且被访问的节点不是尾节点
if (accessOrder && (last = tail) != e) {
// 将节点e转换为LinkedHashMap.Entry类型
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;
// 记录被访问节点的前一个和后一个节点
LinkedHashMap.Entry<K,V> b = p.before, a = p.after;
// 1. 将被访问节点从双向链表中移除
p.after = null;
if (b == null)
head = a; // 如果被访问节点是头节点,将head指向a
else
b.after = a; // 否则,将b的after指向a
if (a != null)
a.before = b; // 将a的before指向b
else
last = b; // 如果a为null,说明被访问节点是尾节点,将last指向b
// 2. 将被访问节点重新插入到双向链表的尾部
if (last == null)
head = p; // 如果链表为空,将head和tail都指向p
else {
p.before = last;
last.after = p;
}
tail = p; // 将tail指向被访问节点p
// 增加modCount计数器,表示链表结构被修改
++modCount;
}
}
get方法的主要逻辑是通过HashMap的getNode
方法获取节点,而LinkedHashMap
额外增加了对accessOrder的支持。当accessOrder为true时,每次访问一个节点,都会将该节点移动到双向链表的尾部,从而实现了按访问顺序维护Entry的功能。
五、最少访问数据删除策略如何实现?
- map 中元素个数大于 3 时,把队头的元素删除,当 put(1, 1) 执行的时候,正好把队头的 10 删除,这个体现了达到我们设定的删除策略时,会自动的删除头节点。
public class test {
public static void main(String[] args) {
testAccessOrder();
}
public static void testAccessOrder() {
// 新建 LinkedHashMap
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(4, 0.75f, true) {
{
put(10, 10);
put(9, 9);
put(20, 20);
put(1, 1);
}
@Override
// 覆写了删除策略的方法,我们设定当节点个数大于 3 时,就开始删除头节点
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > 3;
}
};
//{9=9, 20=20, 1=1}
System.out.println(map);
}
}
2. 执行 put 方法时,队头元素被删除,LinkedHashMap 本身是没有 put 方法实现,调用的是 HashMap 的 put 方法,但 LinkedHashMap 实现了 put 方法中的调用 afterNodeInsertion 方法,实现了删除策略
// 删除很少被访问的元素,被 HashMap 的 put 方法所调用
void afterNodeInsertion(boolean evict) {
// 得到元素头节点
LinkedHashMap.Entry<K,V> first;
// removeEldestEntry 来控制删除策略,如果队列不为空,并且删除策略允许删除的情况下,删除头节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
// removeNode 删除头节点
removeNode(hash(key), key, null, false, true);
}
}