管中窥豹:LinkedHashMap源码学习

一、学习自

https://blog.csdn.net/justloveyou_/article/details/71713781
http://www.cnblogs.com/xrq730/p/5052323.html
http://www.importnew.com/18706.html

二、注意点

  • LinkedHashMapKeyValue都允许为null
  • LinkedHashMap允许重复数据,Key重复会覆盖、Value允许重复
  • LinkedHashMap有序
  • LinkedHashMap非线程安全
  • 经测试遍历时使用iterator()略快点

三、问题

1.LinkedHashMap继承自HashMap,内部方法与HashMap没多大差别啊,那发明它有什么用?
      大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序


2.为什么LinkedHashMap能保证输出与插入顺序一致呢,即为什么是有序的?
      LinkedHashMap可以理解为HashMap + LinkedList,即它既使用HashMap操作数据结构,又使用LinkedList(依靠双向链表)维护插入元素的先后顺序。
      那它怎么实现的呢?下面以示例结合源码进行理解(同时也是我的思考过程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// 例子
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
for(int i = 0; i < 10; i++) {
linkedHashMap.put("key" + i, "value" +i);
}
linkedHashMap.get("key" + 0);
System.out.println(linkedHashMap);
// 源码
// 由于我实例化时没有带参,所以调用的是无参构造函数
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the default initial capacity (16) and load factor (0.75).
*/
public LinkedHashMap() {
super();
accessOrder = false;
}
// 无参构造函数内部直接调用父类 HashMap 的构造函数,然后 accessOrder 是什么呢?
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
final boolean accessOrder; // 根据源码注释翻译下,大意是true对应存储顺序,false对应插入顺序
// 刚刚实例化时置为 false,因为此时内部还没有数据接下来要么是插入数据要么是做赋值操作
// 这个有什么用呢?暂时不清楚,可以继续往下看
/* LinkedHashMap内部没有定义put的方法,所以调用put时其实用的是HashMap的put方法
* 目前为止和HashMap没有什么区别,接下来看看get方法
* get 方法明显被重写了
* 先判断传入的 key 在 HashMap 里有没有对应的节点,没有就返回null,有的话继续
* 在 HashMap 里如果存在对应 key 的节点,那么会直接返回值的
* 但是在 LinkedHashMap 里先对 accessOrder 做了一个判断,如果为true就调用 afterNodeAccess 方法
* 这里还是不知道 accessOrder 有什么用,不妨去看看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; // 如果 accessOrder 是false,那么和HashMap一样直接返回value
}
/*
* afterNodeAccess
*/
void afterNodeAccess(Node<K,V> e) { // move node to last
/* 定义了一个 LinkedHashMap.Entry 类型变量,其实这里的 Entry 继承自 HashMap 里的 Node
* 也就是 HashMap 的基础数据结构单元,下面列出 Entry 类
* 阅读过 Entry 类后应该知道,此时的last中有 before 和 after 两个 LinkedHashMap 里定义的
* 和 HashMap 中定义的 hash,key,value以及next
*/
LinkedHashMap.Entry<K,V> last;
// tail其实是用来保存双链表的尾部节点
// 这里要求传入的key对应的节点不能是尾部节点
if (accessOrder && (last = tail) != e) {
/* 这一步把传入的key对应的节点转换为LinkedHashMap.Entry类型,那么e也拥有before和after属性了
*/ 定义 p,b,a 对 e 自己以及前后进行缓存
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
/* 这一步一开始不是很理解。
* 因为我示例中第一次调用put方法添加时,节点是HashMap里的Node类型,此时是没有after和before的
* 刚刚将e强转成LinkedHashMap.Entry使它拥有了after和before属性,但是这两属性应该一开始就是null啊
* 何必要进行赋值声明呢?
* 其实我进入了误区。
* 因为不论是put和get都没有改变过 accessOrder 的值,这样强行过来看代码路肯定不通
* 我应该先找到 accessOrder 在哪里改变的然后应该就能看到有关 before 和 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;
}
}
// Entry内部类
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); // 最终还是调用了 HashMap.Node 中的构造函数
}
}
/*
* 上述示例中,我直接打印输出发现 linkedHashMap 的确是按 put 的顺序输出的,这就真的很奇怪了,明明用的都是同一个put方法
* 那么我怀疑问题的关键点可能在HashMap里面。HashMap中其实调用的是putVal方法进行添加数据的
* 虽然做过HashMap的源码分析,putVal方法里代码也确实多,当时看到里面有调用
* afterNodeAccess以及afterNodeInsertion方法,但是HashMap里并没有具体实现,所以就没关注
* 现在很大程度怀疑 LinkedHashMap 就是在 HashMap 的putVal中实现的有序,试着在LinkedHashMap中搜了下,
* 果然有afterNodeAccess以及afterNodeInsertion方法
* 也就是说,当对LinkedHashMap调用put时内部调用的是LinkedHashMap重写过的
* afterNodeAccess以及afterNodeInsertion方法,而且当发生hash冲突时才会调用afterNodeAccess
* 那么,假设先不发生hash冲突,看看 afterNodeInsertion 里写的是什么
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
/*
* 如果不发生hash碰撞,那么数据结构维持数组形式
* head 是 LinkedHashMap 内部定义的,初始化是null,
* 不发生 hash冲突的情况下,那么 head 一直是null,下面这个判断也不成立
* 无意中点开 removeEldestEntry 方法发现直接 return false,这是逗我呢???
* 下面的判断怎么也不会成立了!!!
* 那这个方法最终有什么用,就是为了定义一个 first 属性???
*/
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
/*
* 不发生hash冲突的情况下,其实就是数组结构了,那么打印输出的确是有序的,但假如发生了hash冲突呢?
* 我发现在 HashMap 中有调用 afterNodeAccess 方法,但是,这里也并没有改变 accessOrder
* 然后在 LinkedHashMap 中搜索 accessOrder,果然整个类里面都没有对accessOrder进行赋值改变,
* 看了accessOrder要么用默认值要么我手动传入,afterNodeAccess方法又不通了。。。
*
* 虽然网上的文章说 LinkedHashMap 其实近似于 HashMap + LinkedList,
* 但是,就我看到的源码来说,貌似一直是 HashMap,它什么时候实现的双向链表结构呢?怎么关联的呢?
* 看了文章开头第二个链接,前辈讲的是1.6版本的,而我看的是1.8版本。。。
* 1.6版本在构造函数内部调了init(),init()内部将after,before与head关联了起来,但是1.8版本的构造函数没有
* MMP
* TMD
* fuck
* 我终于知道了!!!
* LinkedHashMap 内部重写了 newNode 方法!!!
* 在调用 putVal 时,内部会通过 newNode() 来生成新节点,我一开始没在意,以为仍然用的 HashMap 内部的方法
* 其实这里用的是 LinkedHashMap 内部 newNode() !!!
* 真的难找啊!!!
* 贴出 LinkedHashMap 内部 newNode()
* 该方法内主要将 HashMap 里面的 Node 节点转换为 LinkedHashMap.Entry,这样就会给 e 添加了 before和after属性
* 然后通过 linkNodeLast(p) 将before,after属性与链表节点关联起来
*/
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;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail; // 缓存当前链表尾节点
tail = p; // 把put进来的节点放到尾节点
if (last == null)
head = p; // 如果是第一次put的话,先前链表里head是null,第一次put生成的新节点同时是头节点
else {
p.before = last; // 这里其实与 LinkedList 双链表一样的,
// 新加进来的节点向前指向原先的最后节点last(也就是目前倒数第二个节点)
last.after = p; // 原先的尾节点自动向后指向新的尾节点
}
}
/*
* 这样就很容易明白了,即使不断put新数据进来,那么自动填充到尾节点,依次顺序排列。
* 如果 key 重复怎么办呢?当然是覆盖填充了!
*/

四、感想

      1.8版本虽然写法上改变大,但是思想上其实没什么大变化,万变不离其中。还有,要细心,多看看前辈们的讲解。

Newer Post

管中窥豹:TreeMap源码学习

一、学习自红黑树:http://www.cnblogs.com/skywang12345/p/3245399.htmlhttp://www.cnblogs.com/skywang12345/p/3310928.html 二、注意点TreeMap 是一个有序的key-value集合,它是通过红黑树实现 …

继续阅读
Older Post

管中窥豹:java集合中的fail-fast机制

学习自:http://www.cnblogs.com/skywang12345/p/3308762.html &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;java的集合里有一个叫做fail-fast机制。不论是HashMap,Hashtable,HashSet以及Arr …

继续阅读