linkedList说明

LinkedList是List接口的另一种实现,它的底层是基于双向链表实现的,因此它具有插入删除快而查找修改慢的特点,此外,通过对双向链表的操作还可以实现队列和栈的功能。

LinkedList解释说明

说明文档解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
* 双向链表是实现了List和Deque接口。实现了所有List的操作和允许值为Null。
*
* 所有的操作执行都与双向链表相似。操作索引将遍历整个链表,至于是从头开始遍历还是从尾部
* 开始遍历取决于索引的下标距离哪个比较近。
*
* 需要注意的是这个方法不是同步的方法,需要同步的应用(ConcurrentLinkedDeque高效的队列),如果多个
* 线程同时操作LinkedList实例和至少有一个线程修改list的结构,必须在外部加同步操作。
* 关于结构性操作可以看前面的HashMap的介绍。这个同步操作通常是压缩在某些对象头上面。(synchronized就是存储在对象头上面)
*
* 如果对象头不存在这样的对象,这个列表应该使用{@link Collections#synchronizedList Collections.synchronizedList}工具
* 来封装,这个操作最好是在创建List之前完成,防止非同步的操作。
* List list = Collections.synchronizedList(new ArrayList(...));
* 但是一般不用这个方法,而是用JUC包下的ConcurrentLinkedDeque更加高效,(因为这个底层采用的是CAS操作)
*
* 快速失败机制,当一个list被多个线程同时修改的时候会抛出异常。但是不能用来保证线程安全。
* 所以在多线程环境下,还是要自己加锁或者采用JUC包下面的方法来保证线程安全,
* 而不能依靠fail-fast机制抛出异常,这个方法只是用来检测bug。

LinkedList数据结构

image-20210527093412978
  1. 节点Node主要由三部分组成:pre:前驱引用,ele1:节点信息,next:后驱引用。
  2. first和last分别指向头结点和尾结点。

源码

1
2
3
4
5
6
7
8
9
10
11
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;
}
}
  • Node 类是LinkedList中的私有内部类,LinkedList中就是通过Node来存储集合中的元素。
  • E :节点的值。
  • Node next:当前节点的后一个节点的引用(可以理解为指向当前节点的后一个节点的指针)。
  • Node prev:当前节点的前一个节点的引用(可以理解为指向当前节点的前一个节点的指针)。
image-20210527093919777
  1. LinkedList继承了AbstractSequentialList抽象类,在遍历LinkedList的时候,官方更推荐使用顺序访问,也就是使用迭代器。因为LinkedList底层是通过一个链表来实现的,虽然LinkedList也提供了get(int index)方法,但是底层的实现是:每次调用get(int index)方法的时候,都需要从链表的头部或者尾部进行遍历,每次的遍历时间复杂度是O(index),而相对比ArrayList的底层实现,每次遍历的时间复杂度都是O(1)。所以不推荐通过get(int index)遍历LinkedList。
  2. 至于从链表的头部或者尾部进行遍历,官方对遍历进行了优化:通过判断索引index更靠近链表的头部还是尾部来选择遍历的方向,因此这里遍历LinkedList推荐使用迭代器。
  3. 实现了List接口。即提供List接口中所有方法的实现。
  4. 实现了Cloneable接口,支持克隆(浅克隆),底层实现:LinkedList节点并没有被克隆,只是通过Object的clone()方法得到的Object对象强制转化为了LinkedList,然后把它内部的实例域都置空,然后把被拷贝的LinkedList节点中的每一个值都拷贝到clone中。(后面有源码解析)
  5. 实现了Deque接口,实现了Deque所有的可选的操作。
  6. 实现了Serializable接口。表明它支持序列化。和ArrayList一样,底层都提供了两个方法:readObject(ObjectInputStream o)、writeObject(ObjectOutputStream o),用于实现序列化,而在底层只序列化节点的个数和节点的值。

注:由于后面源码分析过多,所以直接把总结部分放到这里。

小结

  1. LinkedList底层是一个双链表,是一个直线型的链表结构。

  2. LinkedList内部实现了6种主要的辅助方法:它们都是private修饰的方法或者没有修饰符,表明这里都只是为LinkedList的其他方法提供服务,或者同一个包中的类提供服务。在LinkedList内部,绝大部分方法的实现都是依靠这6种辅助方法,所以只要把这6个辅助方法理解了,LinkedList的基本操作也就掌握了。

    • void linkFirst(E e)

    • void linkLast(E e)

    • linkBefore(E e, Node succ)

    • E unlinkFirst(Node f)

    • E unlinkLast(Node l)

    • E unlink(Node x)

LinkedList源码分析

方法字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//元素个数
transient int size = 0;

/**
* 指向第一个节点
*
*/
transient Node<E> first;

/**
* 指向最后一个节点
*/
transient Node<E> last;
.
.
.
.
}
  • size:用来记录LinkedList的元素个数。
  • Node first:用来表示LinkedList的头节点。
  • Node last:用来表示LinkedList的尾节点。

构造方法

1
2
3
4
5
6
7
8
9
10
//空的构造器
public LinkedList() {
}


//包含一个数组的构造函数,链表中的顺序按照集合中的元素顺序进行插入
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);//这里调用了addAll(),就是插入所有元素
}

在传入一个集合进行初始化的时候主要调用了addAll()方法,那么这个addAll()方法是怎么样添加元素的呢?

addAll(int index, Collection<? extends E> c)

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

//通过调用addAll(int index, Collection<? extends E> c) 完成集合的添加。
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}



public boolean addAll(int index, Collection<? extends E> c) {
//几乎所有的涉及到在指定位置添加或者删除或修改操作都需要判断传进来的参数是否合法。
checkPositionIndex(index);

//先把集合转化为数组,然后为该数组添加一个新的引用(Objext[] a)。
Object[] a = c.toArray();//为什么要将集合转为数组?????
//存储数组的长度
int numNew = a.length;
//如果待添加的集合为空,直接返回,无需进行后面的步骤。后面都是用来把集合中的元素添加到LinkedList中
if (numNew == 0)
return false;

//Node<E> succ:指代待添加节点的位置。Node<E> pred:指代待添加节点的前一个节点。
//下面的代码是依据新添加的元素的位置分为两个分支:
//1.新添加的元素的位置位于LinkedList最后一个元素的后面。
Node<E> pred, succ;
if (index == size) {//插入的位置刚好在最后位置
//则succ为空
succ = null;
//前驱为last所引用的尾结点
pred = last;
} else {
//寻找插入的节点位置
succ = node(index);
//找到插入节点的前驱
pred = succ.prev;
}

//循环插入每个节点
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;//向下转型
Node<E> newNode = new Node<>(pred, e, null);//生成一个新节点
//前驱为空,表示在第一个位置插入
if (pred == null)
first = newNode;
//否则在index前面插入新节点
else
pred.next = newNode;
pred = newNode;
}
{
//后继为空
if (succ == null)
last = pred;//在末尾插入
} else {
//否则链接最后的节点
pred.next = succ;
succ.prev = pred;
}

size += numNew;//节点个数增加
modCount++;//结构性修改
return true;
}

//集合转数组
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}

中间位置插入元素图解如下:

  1. 找到节点

    image-20210527141853019
  2. 插入新节点

    image-20210527141952529

  3. 改变指针

    image-20210527142031213

linkFirst(E e)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
//在头部插入一个新节点
private void linkFirst(E e) {
//因为需要把插入的该元素设置为头节点,所以需要新建一个变量把原来的头节点存储起来。
final Node<E> f = first;
//然后新建一个节点,保存插入节点的值e,由于插入的节点为头结点,因此前驱为null,而后继则为原来的头结点
final Node<E> newNode = new Node<>(null, e, f);
//将头结点引用指向新节点
first = newNode;
//如果原来的头结点为空,则说明没有头结点,头尾节点均为null
if (f == null)
//将新节点置为尾结点
last = newNode;
else
//否则原来的头结点的前引用指向新节点
f.prev = newNode;
//size和modCount自增
size++;
modCount++0;
}

图解如下:

image-20210527162010829

linkLast(E e)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//在尾部插入一个新节点
void linkLast(E e) {
//因为需要把插入的该元素设置为尾节点,所以需要新建一个变量把原来的尾节点存储起来。
final Node<E> l = last;
//然后新建一个节点,保存插入节点的值e,由于插入的节点为尾结点,因此前驱为l,而后继则为null
final Node<E> newNode = new Node<>(l, e, null);
//last引用指向新节点
last = newNode;
//如果原来的尾结点为空,则说明没有尾结点,则尾结点为新节点
if (l == null)
first = newNode;
else
//否则原来尾结点的后引用指向新的尾结点
l.next = newNode;
size++;
modCount++;
}

过程与头部插入节点类似。

linkBefore(E e, Node succ)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

//在某个节点之前插入一个新节点
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//找到该节点的前一个节点,因为要在succ前面插入一个节点
final Node<E> pred = succ.prev;//
//新节点的前一个节点就是原来succ的前一个节点(pred),后一个节点当然就是succ
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
//如果pred为空,说明要插入的节点位置为第一个节点
if (pred == null)
first = newNode;
else
//否则pred的下一个节点就是新节点
pred.next = newNode;
size++;
modCount++;

unlinkFirst(Node f)

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

//删除LinkedList中第一个节点(该节点不为空,并且返回删除的节点的值)
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//
//(因为我们需要设置f节点的下一个节点为头结点,而且需要把f节点的值设置为空)
//
//接着判断一个它的下一个节点是否为空,如果为空的话,则需要把last设置为空。否则
//的话,需要把next的prev设置为空,因为next现在指代头节点。
//定义一个变量element指向待删除节点的值,
final E element = f.item;
//接着定义一个变量next指向待删除节点的下一个节点。
final Node<E> next = f.next;
//接着把f的值和它的next设置为空,把它的下一个节点设置为头节点。
f.item = null;
f.next = null; // help GC 解开与下一个元素的连接,方便GC回收
first = next;
//如果待删除节点的下一个节点为空,说明LinkedList中就一个元素
if (next == null)
//则需要把last设置为空。
last = null;
else
//断开后一个节点与待删除节点的连接
next.prev = null;
size--;
modCount++;
return element;
}

unlinkLast(Node l)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//删除LinkedList的最后一个节点。(该节点不为空,并且返回删除节点对应的值)
//思路和unlinkFirst()方法差不多。
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
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
 
//删除一个节点(该节点不为空)
E unlink(Node<E> x) {
// assert x != null;

//创建变量用来存储当前被删除节点的值,后面要把该节点返回
final E element = x.item;
//第二个变量用来存储待删除节点的前一个节点
final Node<E> next = x.next;
//第三个变量用来存储待删除节点的后一个节点
final Node<E> prev = x.prev;

//判断prev
//如果待删除节点的前一个节点为空,表明待删除的节点是头结点
if (prev == null) {
//把待删除节点的后一个节点设置为头结点
first = next;
//如果不为空,就需要把待删除的节点的前、后节点链接起来
} else {
//将待删除节点的前后节点进行连接
prev.next = next;
//断开待删除节点与其前一个节点的联系
x.prev = null;
}

//判断next是否为空
//如果为空则表明待删除节点是尾节点,则需要把待删除节点的前一个节点设置为尾节点。
if (next == null) {
last = prev;
//如果不为空,则把前后节点进行连接
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}

图解如下:

  1. 找到待删除节点的信息以及它的前后节点

image-20210527204905178

  1. 修改前置指针

image-20210527205037292

  1. 修改后继指针

    image-20210527205107998

  2. 将待删除节点置空,方便GC

image-20210527205148729

node(int index)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

//计算指定索引上的节点(返回Node)
Node<E> node(int index) {
// assert isElementIndex(index);
//比较index更靠近链表(LinkedList)的头节点还是尾节点。然后进行遍历,获取相应的节点。
if (index < (size >> 1)) {
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;
}
}

removeFirst()

1
2
3
4
5
6
7
8
9

//提供给用户使用的删除头结点,并返回删除的值。
//直接调用了上面的工具方法unlinkFirst(Node f)
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}

removeLast()

1
2
3
4
5
6
7
8
9

//删除链表中的最后一个节点,并返回被删除节点的值。
//和上面一样调用了unlinkLast(Node last)方法。
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}

addFirst(E e)、addLast(E e)

1
2
3
4
5
6
7
8

//在LinkedList头部添加一个新的元素、尾部添加一个新元素,都是调用了私有方法。
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}

contains(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

//判断LinkedList是否包含某一个元素,底层通过调用indexof()。
//该方法主要用于计算元素在LinkedList中的位置。
//思路:先依据obejct是否为空,分为两种情况,然后通过在每种情况下,从头节点开始遍历LinkedList,判断是否有与object相等的元素,如果有,则返回对应的位置index,如果找不到,则返回-1。
public boolean contains(Object o) {
return indexOf(o) != -1;
}
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}

size()

1
2
3
4
5

//计算LinkedList的大小,直接返回实例域size。
public int size() {
return size;
}

add(E e)

1
2
3
4
5
6

//添加一个新元素。直接在最后面添加,调用了linkLast()方法。
public boolean add(E e) {
linkLast(e);
return true;
}

remove(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

//从LinkedList中删除指定元素。(且只删除第一次出现的指定的元素,如果指定的元素在集合中不存在,则返回false,否则返回true)
//该方法也是通过object是否为空分为两种情况去,之后与LinkedList中的每一个元素比较,如果找到了,就删掉,返回true即可。如果找不到,则返回false。
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

clear()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

//清空LinkedList中的所有元素
//该方法也简单,直接遍历整个LinkedList,然后把每个节点都置空,最后要把头节点和尾节点设置为空,size也设置为空,但是modCount仍然自增
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}

get(int index)

1
2
3
4
5
6
7

//获取对应index的节点的值。
//通过node()方法返回其值。(node()方法依据索引的值返回其对应的节点。)
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

set(int index, E element)

1
2
3
4
5
6
7
8
9
10

//设置对应index的节点的值。
//首先检查一下索引是否合法,然后通过node()方法求出旧值,然后设置新值。最后把旧值返回回去。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

add(int index, E element)

1
2
3
4
5
6
7
8
9
10
11

//在指定的位置上添加新的元素。
//在方法中先判断新添加的元素是否是位于LinkedList的最后,然后则直接调用linkLast()方法添加即可。否则的话,调用linkBefore()添加即可。
public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

remove(int index)

1
2
3
4
5
6

//移除指定位置上的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}

isPositionIndex(int index)

1
2
3
4
5

//判断新添加元素的时候,传进来的index是否合法,而且新添加的元素可能在LinkedList最后一个元素的后面,所以这里允许index<=size。
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}

checkElementIndex(int index)

1
2
3
4
5
6

//判断参数index是否是元素的索引(如果不是则抛出异常)
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

checkPositionIndex(int index)

1
2
3
4
5
6

//判断新添加元素的时候,传进来的index是否合法,(调用的是isPositionIndex(index)方法)而且新添加的元素可能在LinkedList最后一个元素的后面,所以这里允许index<=size。如果不合法,则抛出异常。
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

lastIndexOf(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

//在LinkedList中查找object在LinkedList中的位置。(从后向前遍历,只返回第一出线的元素的索引,如果没找到,则返回-1)
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}

总结

  1. LinkedList是基于双向链表实现的,不论是增删改查方法还是队列和栈的实现,都可通过操作结点实现。
  2. LinkedList无需提前指定容量,因为基于链表操作,集合的容量随着元素的加入自动增加。
  3. LinkedList删除元素后集合占用的内存自动缩小,无需像ArrayList一样调用trimToSize()方法。
  4. LinkedList的所有方法没有进行同步,因此它也不是线程安全的,应该避免在多线程环境下使用。