一、概述
LinkedHashMap类是继承自HashMap类,但是在HashMap的数据结构基础上,使得每个桶的元素又通过新Entry特殊的结构,组成一条双向链表。有了双向链表的结构,就能保证LinkedHashMap的实例在默认情况下能够保持元素的插入顺序。
二、源码剖析
(1) 类的声明
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
LinkedHashMap的继承实现结构比较简单,就是继承了HashMap类,然后实现了Map类,让LinkedHashMap拥有Map的特性。
(2) 元素结构
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类的结点Entry类实际是继承了HashMap类的结点Node类,并且在此基础上添加了before和after两个引用,用来记录双向链表的前后结点。
(3) 成员变量
//序列化标识ID
private static final long serialVersionUID = 3801124242820219131L;
//记录头结点
transient LinkedHashMap.Entry<K,V> head;
//记录尾结点
transient LinkedHashMap.Entry<K,V> tail;
//当accessOrder设置为false时,会按照插入顺序进行排序(在创建新节点的时候,把该节点放到了尾部),当accessOrder为true时,会按照访问顺序(也就是插入和访问都会将当前节点放置到尾部,尾部代表的是最近访问的数据)
final boolean accessOrder;
LinkedHashMap类中除了记录了头尾结点外,最重要的设置属性accessOrder来维持LinkedHashMap的实例中元素的顺序,有两种情况:当accessOrder设置为false时,会按照插入顺序进行排序(在创建新节点的时候,把该节点放到了尾部);当accessOrder为true时,会按照访问顺序(也就是插入和访问都会将当前节点放置到尾部,尾部代表的是最近访问的数据)。
对于成员变量accessOrder来说,使用final关键字修饰,表示不能改变:
a、和局部变量的不同点在于,成员变量有默认值,因此必须手动赋值
b、final的成员变量可以定义的时候直接赋值,或者使用构造方法在构造方法体里面赋值,但是只能二者选其一
c、如果没有直接赋值,那就必须保证所有重载的构造方法最终都会对final的成员变量进行了赋值
(4) 构造方法
//无参构造函数
public LinkedHashMap() {
//调用父类HashMap的构造函数(下同)
super();
//设置默认的排序规则为插入顺序(下同)
accessOrder = false;
}
//带初始容量的构造函数,initialCapacity也只是建议容量,并非最终容量
public LinkedHashMap(int initialCapacity) {
//调用父类HashMap的构造函数
super(initialCapacity);
//设置默认的排序规则为插入顺序
accessOrder = false;
}
//带初始容量和负载因子的构造函数
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//带初始容量、负载因子、排序规则的构造函数
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//参数为Map的构造函数
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
//将原Map中的元素插入新LinkedHashMap中
putMapEntries(m, false);
}
可以看到LinkedHashMap类几乎所有的构造函数都是调用的父类HashMap的构造函数,只是多了一步设置成员变量accessOrder为false的操作。当LinkedHashMap()是带Map的构造函数的时候,就需要调用HashMap的putMapEntries()方法,使得原Map的元素变得有序了,并且顺序就为元素插入顺序。
(5) putMapEntries()方法
//将一个Map中的所有元素添加到LinkedHashMap中,排序规则为evict
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取m的元素个数,必须大于0才执行
int s = m.size();
if (s > 0) {
如果LinkedHashMap中的容器table为null
if (table == null) { // pre-size
//获取根据m的元素个数s获取table的新容量,此时是float类型ft
float ft = ((float)s / loadFactor) + 1.0F;
//判断新容量是否小于2的30次方MAXIMUM_CAPACITY,小于则取ft的整数部分为t,大于则取MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果新容量t是否大于了阈值threshold,如果大于了,那么设置阈值threshold为大于等于形容量t的最小2次幂
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果容器table不为null,那么先判断m中的元素个数s是否超过了阈值,超过则调用HashMap的resize()方法进行扩容
else if (s > threshold)
resize();
//容器准备就绪,就开始往LinkedHashMap循环放入元素
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//调用HashMap的putVal()方法添加元素
putVal(hash(key), key, value, false, evict);
}
}
}
resize()方法涉及HashMap的动态扩容,是HashMap的核心方法之一,就不再多说,详细请看《我的jdk源码(十三):HashMap 一磕到底,追根溯源!》。putVal()方法虽然是写在HashMap类里,但是里面调用的afterNodeAccess()方法,HashMap中并没具体实现,而是在LinkedHashMap中重写了该方法,并且LinkedHashMap还重写了newNode()方法,那么我们结合putVal()方法和afterNodeAccess()方法一起看一下。
(6) putVal()方法和LinkedHashMap.afterNodeAccess()等方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的key、key 的 hashcode与写入的 key 是否相等,相等就赋值给e,在第下面会统一进行赋值及返回。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 该链为链表
else {
//如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 调用afterNodeAccess()方法进行链表排序
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 最后判断是否需要进行扩容。超过最大容量就扩容,实际大小大于阈值则扩容。
if (++size > threshold)
resize();
// 调用afterNodeInsertion()方法进行链表排序
afterNodeInsertion(evict);
return null;
}
//这是LinkedHashMap重写后的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;
}
//把元素P放到双向链表的最后
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;
}
}
//这是LinkedHashMap重写后的afterNodeAccess()方法,当accessOrder为true并且传入的节点不是最后一个时,然后将该节点放到尾部
void afterNodeAccess(Node<K,V> e) {
//在执行方法前的上一次的尾结点
LinkedHashMap.Entry<K,V> last;
//当accessOrder为true并且传入的节点并不是上一次的尾结点时,执行下面的方法
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//p:当前节点
//b:当前节点的前一个节点
//a:当前节点的后一个节点;
//将p.after设置为null,断开了与后一个节点的关系,但还未确定其位置
p.after = null;
/**
* 因为将当前节点p拿掉了,那么节点b和节点a之间断开了,我们先站在节点b的角度建立与节点a
* 的关联,如果节点b为null,表示当前节点p是头结点,节点p拿掉后,p的下一个节点a就是头节点了;
* 否则将节点b的后一个节点设置为节点a
*/
if (b == null)
head = a;
else
b.after = a;
/**
* 因为将当前节点p拿掉了,那么节点a和节点b之间断开了,我们站在节点a的角度建立与节点b
* 的关联,如果节点a为null,表示当前节点p为尾结点,节点p拿掉后,p的前一个节点b为尾结点,
* 但是此时我们并没有直接将节点p赋值给tail,而是给了一个局部变量last(即当前的最后一个节点),因为
* 直接赋值给tail与该方法最终的目标并不一致;如果节点a不为null将节点a的前一个节点设置为节点b
*
* (因为前面已经判断了(last = tail) != e,说明传入的节点并不是尾结点,既然不是尾结点,那么
* e.after必然不为null,那为什么这里又判断了a == null的情况?
* 以我的理解,java可通过反射机制破坏封装,因此如果都是反射创建出的Entry实体,可能不会满足前面
* 的判断条件)
*/
if (a != null)
a.before = b;
else
last = b;
/**
* 正常情况下last应该也不为空,为什么要判断,原因和前面一样
* 前面设置了p.after为null,此处再将其before值设置为上一次的尾结点last,同时将上一次的尾结点
* last设置为本次p
*/
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
//最后节点p设置为尾结点,完事
tail = p;
++modCount;
}
}
//这是LinkedHashMap重写后的afterNodeInsertion()方法,目的是移除链表中最老的节点对象,也就是当前在头部的节点对象,但实际上在JDK8中不会执行,因为removeEldestEntry方法始终返回false。
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);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
由源码可以得知,当accessOrder设置为false时,会按照插入顺序进行排序,当accessOrder为true时,会按照访问顺序进行排序。具体的操作就是,把元素放到双向链表的末尾。
(7) 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;
}
三、总结
LinkedHashMap类其实也比较好理解,动态扩容就是HashMap的resize()方法,LinkedHashMap类只是会根据属性accessOrder值来进行排序,当accessOrder为默认值false的时候,每次插入元素的时候,就将插入的元素放到双向链表的末尾;当accessOrder为true时,每次调用put方法和get方法的时候,都会将元素放到链表的末尾。敬请期待《 我的jdk源码(十七):Objects 》。
更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!