Java集合(四):LinkedList

Catalogue
  1. 1 初始化
  2. 2 add(E e)
  3. 3 get(int index)
  4. 4 set(int index, E element)
  5. 5 遍历
  6. 6 其它操作
  7. 7 总结
  8. 参考资料

注意
本系列文章是笔者在参考了大量优秀文章的基础上的理解,由于水平有限,难免会出现错误,望批评指正,同时我也会把自己最新的理解及时更新到文章中去。最后,我会在文中注明参考文章,特别感谢这些无私奉献的作者。

在开始研究 LinkedList 之前,强烈建议先阅读廖雪峰老师的下面三篇文章:
1、使用Queue
2、使用Deque
3、使用Stack

1 初始化

LinkList 的属性很少,就三个。

1
2
3
4
5
transient int size = 0;

transient Node<E> first;

transient Node<E> last;

size 就是当前的元素个数,first、last分别指头尾元素。为什么要分别定义头尾节点呢,这个问题我们先放着,文章结尾有总结。

下面是它的构造方法。

1
2
3
4
5
6
7
public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

无参数的方法什么也没做,有参数的调用了addAll方法,并最终调用了addAll(int index, Collection<? extends E> c) ,此时 index==0 。

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
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);

Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;

Node<E> pred, succ;
if (index == size) {
succ = null;
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;
else
pred.next = newNode;
pred = newNode;
}

if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}

size += numNew;
modCount++;
return true;
}

从代码看出,这里就是一个依据提供的集合构造双向链表的过程。

2 add(E e)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean add(E e) {
linkLast(e);
return true;
}

void linkLast(E e) {
final Node<E> l = last; //将尾元素保存。
final Node<E> newNode = new Node<>(l, e, null); //构建新节点。
last = newNode; //尾元素重新赋值为新节点。
if (l == null) //之前LinkList为空。
first = newNode; //头节点重新赋值。
else
l.next = newNode; //之前尾元素的 next 指向新元素。
size++;
modCount++;
}

代码逻辑在注释中已经标出了。

对于 LinkedList 的其它增删操作,原理差不多,这里不再赘述。

3 get(int index)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public E get(int index) {
checkElementIndex(index);//检查是否越界。
return node(index).item;
}

Node<E> node(int index) {
// assert isElementIndex(index);

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;
}
}

node(int index)方法的作用是依据传入的 index 找到对应的 Node 元素。查找的逻辑很巧妙,通过判断 index 处在 LinkedList 的前部分还是后半部分来决定遍历的方向,这有助于减少遍历的次数。

4 set(int index, E element)

1
2
3
4
5
6
7
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

代码结构很清晰,通过 node(int index) 找到对应的 node 替换即可。node(int index) 方法在上面讲过了,这里不再赘述。

5 遍历

同ArrayList一样,LinkedList 也有两种遍历方式。

方式一:

1
2
3
for (int i=0;i<size;i++){
linkedList.get(i);
}

方式二:

1
2
3
4
5
//LinkedList 的 iterator() 和 listIterator() 返回的对象是一样的。
ListIterator listIterator = linkedList.listIterator();
while (listIterator.hasNext()) {
listIterator.next();
}

对于方式二,listIterator 是一个 ListItr 对象,在讲 ArrayList 时我们已经介绍过该迭代器了,这里的逻辑和之前的一致,如:fast-fail机制、增删改操作逻辑等。只不过之前是通过外部类 Arraylist 操作数组,现在是通过外部类 LinkedList 操作双向链表。

6 其它操作

从 LinkedList 的继承关系图我们知道,它还实现了 Deque 接口,而 Deque 又继承了 Queue,所以可以用 LinkedList 实现队列\双向队列\栈的功能。

比如,我们要实现一个队列操作:

1
2
Queue<String> queue = new LinkedList<>();
queue.xxx();

要实现双向队列、栈的操作:

1
2
Deque<String> deque = new LinkedList<>();
deque.xxx();

注意,双向队列和栈的操作都是通过 Deque 来实现的,在使用时分别调用他们各自的方法即可(虽然可以混用,但还是建议按照规范来使用),比如当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰。

7 总结

再来看文章刚开始的问题:为什么要分别定义头尾节点呢?这里有两个原因:

一、查询时可依据 index 在链表中的位置来决定遍历的方向,知道了头尾节点,按照不同的方向遍历也就方便了,提升了查找效率。

二、为 Queue 和 Deque 的实现提供便利。

通过这篇文章的研究,我们知道了LinkedList是基于双向链表实现的,没有扩容操作。同时,Linkedlist 也为 Queue 和 Deque 的实现提供了便利。LinkedList 不适合查询、修改频繁的场景,它适合插入、删除操作频繁的场景(操作指定位置时,需要遍历),而 ArrayList 则适合查询、修改频繁,插入、删除不频繁的场景。


参考资料

搞懂 Java LinkedList 源码