LinkedList 源码解析
LinkedList 适用于集合元素先入先出和陷入后出的场景,在队列源码中频繁被使用。
整体架构
LinkedList 底层数据结构是一个双向链表,整体结构如下图所示:
上图代表了一个双向链表结构,链表中的每个节点够可以向前或者向后追溯
- 链表每个节点我们叫做 Node,
- Node 有 prev 属性,代表前一个节点的位置
- next 属性代表后一个节点的位置
- first 是双向链表的头节点,它的前一个节点是 null
- last 是双向链表的尾节点,它的后一个节点是 null
- 当链表中没有数据时,first 和 last 是同一个节点,前后指向都为 null
- 因为是个双向链表,只要内存足够,是没有大小限制的
链表中的元素叫做 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;
}
}
源码解析
追加(新增)
追加节点时,我们可以选择追加到链表头部,还是追加到链表尾部,add
方法默认是从尾部开始追加,addFirst
是追加到头部
从尾部追加(add)
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
// 把尾部节点暂时存起来
final Node<E> l = last;
// 新建一个Node节点,
// l:把前一个节点指向旧尾部节点
// next指向null,
final Node<E> newNode = new Node<>(l, e, null);
// 尾节点赋值为新增的节点
last = newNode;
// 如果尾节点为null,即链表为空
if (l == null)
// 头节点=尾节点=新节点
first = newNode;
else
// 把原尾节点的next指向新增节点
l.next = newNode;
// 链表size++ 版本++
size++;
modCount++;
}
从头部追加(addFirst)
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
// 把头节点暂存起来
final Node<E> f = first;
// 初始化新节点 新节点的next指向原头节点
final Node<E> newNode = new Node<>(null, e, f);
// 把头节点赋值为新增的及诶按
first = newNode;
// 如果头节点为空,即链表为空
if (f == null)
// 头节点=尾节点=新节点
last = newNode;
else
// 把原头节点的prev指向新增节点
f.prev = newNode;
// 链表size++ 版本++
size++;
modCount++;
}
头部追加节点和尾部追加节点非常类似,只是前者是移动头部节点的 prev 指向,后者是移动尾部节点的 next 指向。
节点删除
节点删除和节点追加类似,可以选择从头部删除,也可以选择从尾部删除,删除会把节点的值、prev、next都指向 null,帮助 GC 进行回收
从头部删除
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
// 拿出节点值,删除完后返回
final E element = f.item;
// 拿出头节点的next,它将成为新的头节点
final Node<E> next = f.next;
// 置null,帮助GC
f.item = null;
f.next = null; // help GC
// 原头节点的next成为新的头节点
first = next;
// 如果next为null,即链表为空
if (next == null)
// 头节点=尾节点=null
last = null;
else
// 链表不为null,把新的头节点prev设置为null
next.prev = null;
// 链表size--,版本++
size--;
modCount++;
// 返回删除节点的值
return element;
}
从尾部删除
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
// 拿出尾节点的值
final E element = l.item;
// 拿出尾节点的前一个节点
final Node<E> prev = l.prev;
// 赋值尾节点的值和尾节点的前一个节点为 null,帮助 GC 回收
l.item = null;
l.prev = null; // help GC
// 原尾节点的前一个节点 prev 为新的尾节点
last = prev;
// 如果 prev 为 null,表明链表为空
if (prev == null)
first = null;
else
// 链表为不空,设置新的尾节点的下一个节点为 null
prev.next = null;
// 大小和版本更改
size--;
modCount++;
// 返回被删除节点的值
return element;
}
从源码中我们可以了解到,链表结构的节点新增、删除都非常简单,仅仅把前后节点的指向修改而已,所以 LinkedList
新增和删除速度很快。
节点查询
链表查询某一个节点是比较慢的,需要遍历链表
/**
* 根据链表索引位置查询节点
*/
Node<E> node(int index) {
// assert isElementIndex(index);
// 如果 index 处于链表的前半部分,就从头开始查找,size >> 1 是 size/2 的意思
if (index < (size >> 1)) {
Node<E> x = first;
// 循环到index的前一个node停止
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
// 处于链表的后半部分,从尾部开始查找
else {
Node<E> x = last;
// 循环到index的后一个node停止
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
从源码中我们发现,LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能。