LinkedList源码解析

1.概览:

1.1底层结构:

LinkedList底层使用的双向链表结构,使用 Node 存储链表节点信息,除了结点的值以外,包括前驱结点和后继结点。每个链表都维护了一个头指针first和一个尾指针last。这意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。

1.2 属性:

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


//LinkedList维护了一个内部类Node,用来存储链表节点信息,除了结点的值以外,包括前驱结点和后继结点。
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;
}
}

2.重要方法解析:

2.1构造器

//无参构造器
public LinkedList() {
}

//有参构造器,按照集合初始化数据,调用addAll()方法将集合中的元素按顺序添加到链表中。
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

2.2 新增/添加 方法

addLast(E e)

尾插 相当于add(E e)

public boolean add(E e) {
//尾插,调用linkLast()方法,成功返回true
linkLast(e);
return true;
}

public void addLast(E e) {
linkLast(e);
}

void linkLast(E e) {
//暂存尾节点
final Node<E> l = last;
//以传入的值初始化新节点,新节点前驱为旧的尾节点l,后继为null
final Node<E> newNode = new Node<>(l, e, null);
//尾指针移动到新节点上
last = newNode;
//如果旧的尾节点是null,说明原链表是空的,因此头指针也指向新节点
if (l == null)
first = newNode;
//否则旧的尾节点的后继指向新节点
else
l.next = newNode;
//大小和版本号都+1
size++;
modCount++;
}

addFirst(E e)

头插

public void addFirst(E e) {
//头插入,调用linkFirst()方法
linkFirst(e);
}

private void linkFirst(E e) {
//暂存头结点
final Node<E> f = first;
//以传入的值初始化新节点,新节点的前驱为null,后继为原来的头结点
final Node<E> newNode = new Node<>(null, e, f);
//将头指针指向新节点
first = newNode;
//如果原来的头结点为null,说明原来的链表是空链表,那么就将尾指针也指向新节点
if (f == null)
last = newNode;
//否则就让原来头结点的前驱指向新结点
else
f.prev = newNode;
//大小和版本+1
size++;
modCount++;
}

add(int index, E element)

按照索引位置插入

public void add(int index, E element) {
//检查索引是否越界
checkPositionIndex(index);
//如果索引正好在尾部,就调用尾插的方法
if (index == size)
linkLast(element);
//否则调用linkBefore()方法
else
linkBefore(element, node(index));
}
//检查索引是否越界
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//按照索引位置查找结点 (利用简单的二分法)

Node<E> node(int 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;
}
}

void linkBefore(E e, Node<E> succ) {
//保存原索引位置结点的前驱
final Node<E> pred = succ.prev;
//以传入的值初始新的结点,新节点以原位置结点的前驱为前驱结点,以原位置结点为后继结点
final Node<E> newNode = new Node<>(pred, e, succ);
//将原位置结点的前驱指针指向新节点
succ.prev = newNode;
//如果原位置结点的前驱为空,说明原位置结点是头结点,那么新结点就是新的头结点
if (pred == null)
first = newNode;
//否则将原位置结点的后继指向新结点
else
pred.next = newNode;
//维护链表新的大小和版本号
size++;
modCount++;
}

addAll()

addAll有两个重载函数,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,我们平时习惯调用的addAll(Collection<? extends E>)型会转化为addAll(int, Collection<? extends E>)型,所以我们着重分析此函数即可。

public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}

public boolean addAll(int index, Collection<? extends E> c) {
//检查索引是否越界
checkPositionIndex(index);
//将集合转化为Object数组
Object[] a = c.toArray();
//如果数组长度为空(即集合为空),添加失败返回false
int numNew = a.length;
if (numNew == 0)
return false;

Node<E> pred, succ;
//如果插入的位置是链表的末尾,则后继为null,前驱为原尾节点
if (index == size) {
succ = null;
pred = last;
} else {
//否则查找原插入位置的结点,前驱为原插入位置结点的前驱
succ = node(index);
pred = succ.prev;
}
//遍历Object数组
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;
}
//维护链表的长度以及版本号,添加成功返回true
size += numNew;
modCount++;
return true;
}

2.3删除方法

//按照索引删除
public E remove(int index) {
//检查索引是否越界
checkElementIndex(index);
//node(index)取得原索引位置的结点
return unlink(node(index));
}
//按照值删除
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;
}

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;
//如果待删除结点的前驱为null,则说明待删除结点为头结点,所以头指针后移
if (prev == null) {
first = next;
//否则,将待删除结点的后继指向待删除结点的后继,待删除结点的前驱断开(赋空)
} else {
prev.next = next;
x.prev = null;
}
//如果待删除结点的后继为null,则说明待删除结点为尾节点,所以尾指针前移
if (next == null) {
last = prev;
//否则将后继结点的前驱指向待删除结点的前驱,待删除结点的后继赋空
} else {
next.prev = prev;
x.next = null;
}
//待删除结点赋空值
x.item = null;
//维护链表大小和版本号
size--;
modCount++;
//返回删除结点
return element;
}
//头删
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}

//从头删除节点 f 是链表头节点
private E unlinkFirst(Node<E> f) {
// 拿出头节点的值,作为方法的返回值
final E element = f.item;
// 拿出头节点的下一个节点
final Node<E> next = f.next;
//帮助 GC 回收头节点
f.item = null;
f.next = null;
// 头节点的下一个节点成为头节点
first = next;
//如果 next 为空,表明链表为空
if (next == null)
last = null;
//链表不为空,头节点的前一个节点指向 null
else
next.prev = null;
//修改链表大小和版本
size--;
modCount++;
return element;
}

//尾删
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}

2.4get和set方法

get

public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//按照索引位置查找结点 (利用简单的二分法)
Node<E> node(int 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;
}
}
/*体会:从源码中我们可以发现,LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能,这种思想值得我们借鉴。*/


public E set(int index, E element) {
//检查索引是否越界
checkElementIndex(index);
//找到索引位置的结点
Node<E> x = node(index);
//给结点重新复制
E oldVal = x.item;
x.item = element;
//返回旧的值
return oldVal;
}

2.5迭代器

因为 LinkedList 要实现双向的迭代访问,所以我们使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。Java 新增了一个迭代接口,叫做:ListIterator,这个接口提供了向前和向后的迭代方法,如下所示:

迭代顺序

方法

从尾到头迭代方法

hasPrevious、previous、previousIndex

从头到尾迭代方法

hasNext、next、nextIndex

LinkedList 实现了 ListIterator 接口,如下图所示:

// 双向迭代器
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;//上一次执行 next() 或者 previos() 方法时的节点位置
private Node<E> next;//下一个节点
private int nextIndex;//下一个节点的位置
//expectedModCount:期望版本号;modCount:目前最新版本号
private int expectedModCount = modCount;
…………
}

我们先来看下从头到尾方向的迭代:

// 判断还有没有下一个元素
public boolean hasNext() {
return nextIndex < size;// 下一个节点的索引小于链表的大小,就有
}

// 取下一个元素
public E next() {
//检查期望版本号有无发生变化
checkForComodification();
if (!hasNext())//再次检查
throw new NoSuchElementException();
// next 是当前节点,在上一次执行 next() 方法时被赋值的。
// 第一次执行时,是在初始化迭代器的时候,next 被赋值的
lastReturned = next;
// next 是下一个节点了,为下次迭代做准备
next = next.next;
nextIndex++;
return lastReturned.item;
}

上述源码的思路就是直接取当前节点的下一个节点,而从尾到头迭代稍微复杂一点,如下:

// 如果上次节点索引位置大于 0,就还有节点可以迭代
public boolean hasPrevious() {
return nextIndex > 0;
}
// 取前一个节点
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// next 为空场景:1:说明是第一次迭代,取尾节点(last);2:上一次操作把尾节点删除掉了
// next 不为空场景:说明已经发生过迭代了,直接取前一个节点即可(next.prev)
lastReturned = next = (next == null) ? last : next.prev;
// 索引位置变化
nextIndex--;
return lastReturned.item;
}

这里复杂点体现在需要判断 next 不为空和为空的场景,代码注释中有详细的描述。

迭代器删除

LinkedList 在删除元素时,也推荐通过迭代器进行删除,删除过程如下:

public void remove() {
checkForComodification();
// lastReturned 是本次迭代需要删除的值,分以下空和非空两种情况:
// lastReturned 为空,说明调用者没有主动执行过 next() 或者 previos(),直接报错
// lastReturned 不为空,是在上次执行 next() 或者 previos()方法时赋的值
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
//删除当前节点
unlink(lastReturned);
// next == lastReturned 的场景分析:从尾到头递归顺序,并且是第一次迭代,并且要删除最后一个元素的情况下
// 这种情况下,previous() 方法里面设置了 lastReturned = next = last,所以 next 和 lastReturned会相等
if (next == lastReturned)
// 这时候 lastReturned 是尾节点,lastNext 是 null,所以 next 也是 null,这样在 previous() 执行时,发现 next 是 null,就会把尾节点赋值给 next
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}

2.6堆和栈

总结起来如下表格:

第一个元素(头部)                 最后一个元素(尾部)
抛出异常 特殊值 抛出异常 特殊值
插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e)
移除 removeFirst() pollFirst() removeLast() pollLast()
检查 getFirst() peekFirst() getLast() peekLast()

(06) LinkedList可以作为FIFO(先进先出)的队列,作为FIFO的队列时,下表的方法等价:

队列方法 等效方法
add(e) — addLast(e)
offer(e) — offerLast(e)
remove() — removeFirst()
poll() ---- pollFirst()
element() — getFirst()
peek() — peekFirst()
(07) LinkedList可以作为LIFO(后进先出)的栈,作为LIFO的栈时,下表的方法等价:

栈方法 等效方法
push(e) — addFirst(e)
pop() — removeFirst()
peek() — peekFirst()

3.小结

​2.2.2 Arraylist 与 LinkedList 区别?​​(内容来自JavaGuide,侵删)

  • 1. 是否保证线程安全:​ArrayList​​​ 和 ​​LinkedList​​ 都是不同步的,也就是不保证线程安全;
  • 2. 底层数据结构:​Arraylist​​ 底层使用的是 Object 数组;​​LinkedList​​ 底层使用的是 双向链表 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  • 3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行​​add(E e)​​​方法的时候, ​​ArrayList​​​ 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(​​add(int index, E element)​​)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
  • 4. 是否支持快速随机访问:​LinkedList​​​ 不支持高效的随机元素访问,而 ​​ArrayList​​​ 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于​​get(int index)​​方法)。
  • 5. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。