管中窥豹:LinkedList源码学习

一、学习自:

  1. http://www.cnblogs.com/skywang12345/p/3308807.html
  2. https://www.cnblogs.com/ITtangtang/p/3948610.html
  3. https://www.cnblogs.com/xrq730/p/5005347.html
  4. https://blog.csdn.net/qq_19431333/article/details/54572876
  5. https://blog.csdn.net/androidcon/article/details/50950629
  6. https://blog.csdn.net/jianyuerensheng/article/details/51200274
  7. https://blog.csdn.net/silent123go/article/details/52693735
  8. https://zhuanlan.zhihu.com/p/29627391
  9. https://www.cnblogs.com/xnzzj/p/4509034.html
  10. https://blog.csdn.net/nzfxx/article/details/51728516

二、注意点

  • LinkedList允许为空
  • LinkedList允许重复数据
  • LinkedList有序
  • LinkedList非线程安全
  • 如果使用普通for循环遍历LinkedList,在大数据量的情况下,其遍历速度将慢得令人发指,较快的是foreach循环
  • LinkedList可以被当作堆栈、队列或双端队列进行操作

三、备注

      1.  如果用的是1.8版本的,可以看第四个链接。当然,可以看看前三个老版本的源码实现,进行对比学习。


      2.  LinkedList是一个继承于AbstractSequentialList双向链表。当我看到双向链表时就感觉懵逼,这是什么玩意???是不是还有单向链表???
        2.1  要想解决上述问题,首先得大概知道什么是链表。可以看百度百科了(有条件的看维基百科)或者本文开头的第5和第6个以及第8个链接。
        小总结一下:链表是一种数据结构,和数组同级。是一条相互链接的数据节点表。每个节点由两部分组成:数据和指向下一个节点的指针。一般来说,链表的头结点不存放具体的数据,所以也被称为哑节点(dummy node)。原因在于这样可以比较好地区分链表的头结点,而且可以大大简化链表的各种操作,避免很多不必要的边界讨论。


        2.2  接下来还得知道什么是单向链表和双向链表?
        单向链表可以看第7个链接的介绍。特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始。


        2.3  双向链表可以看第8和第9以及第10个链接。第9个讲的比较一目了然当然也比较简单了。第10个图更多。初学者可能会发现有的地方给的图示,链表头部和尾部互相链接了,其实那是双向循环链表,普通的双向链表头尾并不链接的


      3.如果大概了解过链表相关知识,那么源码看起来就会省力很多。源码中类似下面这样的:

1
2
3
4
5
6
7
8
9
10
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

      起出看肯定不理解这是什么玩意?只能靠猜大概猜对。但是为什么可以这样实现估计都不敢想。其实这里定义的是链表单个节点。在HashMap等源码里都有类似的形式。

四、示例

以一个小例子来理解LinkedList内部增删改机制:

1
2
3
4
5
6
LinkedList linkedList = new LinkedList<>();
linkedList.add("one");
linkedList.add("two");
linkedList.add(0, "yibu");
linkedList.get(1);
linkedList.push("four");

源码分析(其实就是互相引用):

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
transient Node<E> last;
// LinkedList 只有两个构造函数,一个无参一个带参。无参构造函数内部没有任何内容
// 主要看 add() 后的代码
public boolean add(E e) {
linkLast(e);
return true;
}
// add() 内部调用了 linkLast(e)
/**
* Links e as last element. 作为最后一个节点插入
*/
void linkLast(E e) {
final Node<E> l = last; // 保存原先的最后一个节点。当我第一次 add 时, last 一开始为 null,第二次 add 时才有值
final Node<E> newNode = new Node<>(l, e, null); // 通过这一步创建新节点并保存,Node 类在上方有贴出来过
// 其实就是把 e 变成链表节点给它添加前后引用
last = newNode; // last 始终是 add 进来的最新节点
if (l == null) // 这里也是判断如果是第一次 add,那么first 就是第一个节点
first = newNode;
else // 不是第一次add的话,那么 l 对应原先最后一个节点,
l.next = newNode; // 原先最后一个节点由指向null转为指向新节点了
size++; // 长度自增
modCount++; // 修改次数自增
}
// 接下来是第 add(int index, E element)
public void add(int index, E element) { // 带索引的add
checkPositionIndex(index); // 其实就是判断index是不是在链表容量内且大于0
if (index == size)
linkLast(element); // 索引正好==size,插到最后
else
linkBefore(element, node(index)); // 在本来已经互相引用的两节点中横插一脚,小三啊~~~
}
// 看看 linkBefore() 方法
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev; // 目前链表中对应索引的节点succ对前一个节点的引用succ.prev
final Node<E> newNode = new Node<>(pred, e, succ); // 由于横插一脚,需要让新插入的节点e向后引用succ,
// 向前引用succ.prev。
// 注意,此时succ和succ.prev间的引用还未切断
succ.prev = newNode; // 这一步改变succ的向前引用
if (pred == null)
first = newNode; // 如果原先succ向前引用的是null,那么插入的节点就是第一个节点
else
pred.next = newNode; // 这一步改变原先succ.prev的向后引用(其实已经不是succ.prev了)
size++;
modCount++;
}
// 接下来看看get()
public E get(int index) {
checkElementIndex(index);
return node(index).item; // 主要通过node()来实现的
}
// 看看 node()
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { // 这一步先拿 index 和 size的一半作比较。
// 这里依赖于 LinkedList 是有序的,
// 不然不可能因为index与size的一半比较就能得出index大概在什么地方
Node<E> x = first; // 先把第一个节点保存起来
for (int i = 0; i < index; i++)
x = x.next; // 从第一个节点开始遍历查找对应节点
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--) // 这边直接从最后一个节点查
x = x.prev;
return x;
}
}
// 模拟堆栈主要用到 push 和 pop
// 刚开始没在意,以js的思维去想主观上认为 push 是放到最后一个节点上,但这和 add 不就重合了吗
// 看了下源码,貌似自己想错了,实践了下,果然, push 其实是放到第一个节点
// 又看了下 pop(),发现果然删的是向后引用,难怪叫后进先出呢
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first; // 套路啊,保存的是 first
final Node<E> newNode = new Node<>(null, e, f); // 这里新增是新节点没有向前引用,倒是向后引用了原先的第一节点
first = newNode; // 到这里就该明白了,第一节点改朝换代了
if (f == null)
last = newNode;
else
f.prev = newNode; // 改变原第一节点的向前引用
size++;
modCount++;
}

Newer 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 …

继续阅读
Older Post

管中窥豹:ArrayList源码学习

学习自:http://www.cnblogs.com/skywang12345/p/3308556.html removeAll和retainAll()看这里https://lizhongzhen11.github.io/2018/03/20/thinkingInJava/#1571 问题及注意点1 …

继续阅读