提到ArrayList,就会比较与LinkedList的区别。本文来看看LinkedList的核心原理。

image

如图所示 LinkedList 底层是基于双向链表实现的,也是实现了 List 接口,所以也拥有 List 的一些特点(JDK1.7/8 之后取消了循环,修改为双向链表)。

一、LinkedList属性

LinkedList为空的情况图

1
2
3
4
5
6
//链表的节点个数.
transient int size = 0;
//Pointer to first node.
transient Node<E> first;
//Pointer to last node.
transient Node<E> last;

二、Node的结构

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

三、添加元素

3.1 LinkedList表头添加一个元素

LinkedList表头添加元素图

当向表头插入一个节点时,很显然当前节点的前驱一定为 null,而后继结点是 first 指针指向的节点,当然还要修改 first 指针指向新的头节点。除此之外,原来的头节点变成了第二个节点,所以还要修改原来头节点的前驱指针,使它指向表头节点,源码的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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节点
first = newNode;
if (f == null)
last = newNode;//初始就是个空LinkedList的话,last指向当前新节点
else
f.prev = newNode;//初始值不为空,将其前置指针指向新节点
size++;
modCount++;
}

3.2 LinkedList表尾添加一个元素

表尾添加元素图

当向表尾插入一个节点时,很显然当前节点的后继一定为 null,而前驱结点是 last 指针指向的节点,然后还要修改 last 指针指向新的尾节点。此外,还要修改原来尾节点的后继指针,使它指向新的尾节点,源码的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void addLast(E e) {
linkLast(e);
}

void linkLast(E e) {
final Node<E> l = last;
//新节点前置指针指向空,后置指针指向first节点
final Node<E> newNode = new Node<>(l, e, null);
//新节点作为新的last节点
last = newNode;
//如果原来有尾节点,则更新原来节点的后继指针,否则更新头指针
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

3.3 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
public void add(int index, E element) {
//判断数组是否越界
checkPositionIndex(index);

if (index == size)
linkLast(element);//直接插在最后一个
else
linkBefore(element, node(index));//在index节点之前插入
}

private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}

void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//找到索引位置的前面一个元素pred
final Node<E> pred = succ.prev;
//新节点,前置指针指向pred,后置指针指向索引处元素
final Node<E> newNode = new Node<>(pred, e, succ);
//修改索引出元素的前置指针为新节点
succ.prev = newNode;
if (pred == null)
first = newNode;//说明是插在表头
else
pred.next = newNode;//说明是插在非表头位置,修改pred后置指针为新指针
size++;
modCount++;
}

可见每次插入都是移动指针,和 ArrayList 的拷贝数组来说效率要高上不少。

四、删除元素

删除元素图

删除操作与添加操作大同小异,例如删除指定节点的过程如下图所示,需要把当前节点的前驱节点的后继修改为当前节点的后继,以及当前节点的后继结点的前驱修改为当前节点的前驱。

就不赘述了。

五、获取元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//获取指定索引对应的元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

//寻找元素的方向是根据index在表中的位置决定的
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;
}
}

上述代码,利用了双向链表的特性,如果index离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。

  • node()会以O(n/2)的性能去获取一个结点
    • 如果索引值大于链表大小的一半,那么将从尾结点开始遍历

这样的效率是非常低的,特别是当 index 越接近 size 的中间值时。

总结

1、理论上无容量限制,只受虚拟机自身限制影响,所以没有扩容方法。

2、和ArrayList一样,LinkedList也是是未同步的,多线程并发读写时需要外部同步,如果不外部同步,那么可以使用Collections.synchronizedList方法对LinkedList的实例进行一次封装。

3、和ArrayList一样,LinkedList也对存储的元素无限制,允许null元素。

4、顺序插入速度ArrayList会比较快,因为ArrayList是基于数组实现的,数组是事先new好的,只要往指定位置塞一个数据就好了;LinkedList则不同,每次顺序插入的时候LinkedList将new一个对象出来,如果对象比较大,那么new的时间势必会长一点,再加上一些引用赋值的操作,所以顺序插入LinkedList必然慢于ArrayList

5、基于上一点,因为LinkedList里面不仅维护了待插入的元素,还维护了Entry的前置Entry和后继Entry,如果一个LinkedList中的Entry非常多,那么LinkedList将比ArrayList更耗费一些内存

6、数据遍历的速度,看最后一部分,这里就不细讲了,结论是:使用各自遍历效率最高的方式,ArrayList的遍历效率会比LinkedList的遍历效率高一些

7、有些说法认为LinkedList做插入和删除更快,这种说法其实是不准确的:

  • LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Entry的引用地址
  • ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址

所以,如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,因为它是双向链表,所以在第2个元素后面插入一个数据和在倒数第2个元素后面插入一个元素在效率上基本没有差别,但是ArrayList由于要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList

从这个分析看出,如果你十分确定你插入、删除的元素是在前半段,那么就使用LinkedList;如果你十分确定你删除、删除的元素在比较靠后的位置,那么可以考虑使用ArrayList。如果你不能确定你要做的插入、删除是在哪儿呢?那还是建议你使用LinkedList吧,因为一来LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;二来插入元素的时候,弄得不好ArrayList就要进行一次扩容,记住,ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作.

8、ArrayList使用最普通的for循环遍历,LinkedList使用foreach循环比较快.注意到ArrayList是实现了RandomAccess接口而LinkedList则没有实现这个接口.关于RandomAccess这个接口的作用,看一下JDK API上的说法:

image

9、如果使用普通for循环遍历LinkedList,在大数据量的情况下,其遍历速度将慢得令人发指

整理自: