前言
LinkedList 是 Java 又一种常用的集合类,与 ArrayList 有很大的不同,用链表结构存储元素。本文将对 Java 中的 LinkedList 源码进行全面的梳理,对其节点结构,属性,初始化以及增删改查等过程源码进行详细解析。
链表节点结构
LinkedList 是链表形式的集合,自然有链表节点,看一下节点的定义:
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;
}
}
从节点结构可以看出,LinkedList 属于双向链表,存储前驱和后继节点的引用。
属性和初始化
// 元素个数
transient int size = 0;
// 链表头节点
transient Node<E> first;
// 链表尾节点
transient Node<E> last;
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
属性这块也很简单,size 表示 LinkedList 的元素个数,first 和 last 维护着 LinkedList 的头节点和尾节点。
初始化也只有两个构造函数,一个是无参的,啥也没做,另一个是初始化时添加集合中的元素到 LinkedList 里,调用addAll(c)
,这个函数在增加元素里再讲。
增加元素
基本的增加方法
LinkedList 增加元素的方法特别多,但基本的还是增加头节点,增加尾节点和增加中间节点三个。
增加头节点
private void linkFirst(E e) {
final Node<E> f = first;
// 创建新节点,后继为原头节点
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
// 若之前链表为空,设置 last
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
先创建了一个新节点,item 为指定元素,后继为原头节点;再将此新节点设为 first,之后就是设置原头节点前驱为新节点了。此处需进行判断,如果之前 LinkedList 为空的话,自然无法设置原头节点(null)的前驱,并且 last 也为 null,所以需要设置 last 为新节点,否则,正常设置前驱即可。添加好了之后,更新 size 和 modCount 的值。(这个 modCount 是 AbstractList 的元素,LinkedList 是它的子类,也继承了这个属性,用来表示 LinkedList 被修改的次数)。
增加尾节点
void linkLast(E e) {
final Node<E> l = last;
// 创建新节点,前驱为原尾节点
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
// 若之前链表为空,设置 first
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
思路很简单,创建新节点,设置前驱为原尾节点,设置原尾节点后继时,先进行判断,和增加头节点类似的逻辑,不再赘述。
增加中间节点
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
// 创建新节点,后继为 succ,前驱为原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++;
}
源码里有个注释:assert succ != null;
,因为调用这个函数之前会做一些判断,保证这个 succ 不为 null。之后创建新节点,后继为 succ,前驱为 succ.prev,接着要设置 succ 的原 prev 的后继了,同样的思想,先判断其是否为空,之后进行相应操作即可。
这个函数的效果就是在 succ 前面增加了一个新节点。
扩展的增加方法
由于 LinkedList 实现了 List 和 Deque 两个接口,需要重写它们对应的增加方法,所以有很多版本的增加函数,但都是调用上面介绍的基础增加方法。
public boolean add(E e) {
linkLast(e);
return true;
}
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
public boolean offer(E e) {
return add(e);
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
public void push(E e) {
addFirst(e);
}
非常简单,不解释,也可以看出默认的 add 方法是加到链表尾的。
批量增加
批量增加最终会调用下面的函数,从指定的 index 开始,批量增加集合里的元素。
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
// 记录 index 处节点及其前驱
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 在该节点前驱 pred 后链式增加集合里的元素
// 相当于原链表在 pred 处断开了
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;
}
// 把两条链表合在一起
// succ 为空,设置 last 即可
if (succ == null) {
last = pred;
} else {
// succ 不为空,进行两条链表的链接
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
代码看着很长,其实很简单,首先是一些长度检查;通过了之后,找到 index 处的节点,和其前驱 pred,在 pred 后面增加集合中的元素,代码和 linkLast 类似,只不过每次会设置pred = newNode;
,保证一直在链表尾增加元素。
这里其实就相当于原链表从 pred 处断开了,pred 所在的这条链表一直增加元素,增加完了之后,在和 succ 所在的那条链表链接起来,链表的链接非常简单,设置下前驱和后继指针即可。里面有一些 null 的判断,和基本的增加方法比较类似。
函数的最终效果就是在 index 处的元素前增加给定集合里的所有元素。
删除元素
基本的删除方法
删除的方法也很多,但基本的还是删除头节点,尾节点和中间节点。
删除头节点
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
// 相应数据设为 null,从而之后GC清理
f.item = null;
f.next = null; // help GC
first = next;
// 就一个节点,删完了
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
这里在调用的时候就已经保证传入的 f 为非空头节点。先是把相应的引用设为 null,接着设置新头节点为 next,若 next 为空,说明链表已经空了,设置 last,否则,将 next 前驱设置为空,更新 size 和 modCount 即可。
删除尾节点
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
last = prev;
// 就一个节点,删完了
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
同样的,调用的时候保证 l 为非空尾节点,相应引用设为 null,设置新尾节点,进行必要的判空检查,最后更新 size 和 modCount。
删除中间节点
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;
// 为头节点,则设置新头节点
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 为尾节点,则设置新尾节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
如果节点是中间节点,其实起作用的就是prev.next = next; next.prev = prev;
,用这种方式,这个节点在这条链表里就消失了;但是还要考虑特殊情况,比如 prev 或 next 为 null,肯定是无法调用 prev.next
抑或是next.prev
的。特殊情况特殊判断一下即可。最后更新 size 和 modCount。
扩展的删除方法
和增加一样,删除也有很多扩展版本,不过基础还是上面三个方法。
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
public E pop() {
return removeFirst();
}
简单的代码,不解释。
查询元素
按下标查元素
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;
}
}
其实链表是没有所谓索引的,但是从实现我们可以看出,LinkedList 的作者把头节点的元素索引看为 0,后面的元素依次类推。查的时候,会判断 index 对应元素在前半部分还是后半部分,从而决定从头节点还是尾节点遍历,这是个小小的优化。
从元素找下标
public int indexOf(Object o) {
int index = 0;
// 为 null,直接比引用
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
// 否则,调用 equals
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
从头开始遍历链表查找相等的元素下标。还有个lastIndexOf,从尾节点开始,不再赘述。
按下标进行增删改
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
public E remove(int index) {
checkElementIndex(index);
// 找到之后,删了它
return unlink(node(index));
}
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
// 找到之后,修改节点的数据
E oldVal = x.item;
x.item = element;
return oldVal;
}
均是先调用node(index)
找到对应节点,再对该节点进行操作。
迭代器
迭代器的结构
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
private class ListItr implements ListIterator<E> {
// 当前停留的节点
private Node<E> lastReturned;
// 将要迭代的节点
private Node<E> next;
// 将要迭代的 index
private int nextIndex;
// 避免并发修改
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
...
}
和 ArrayList 一样,迭代器本身不保存数据,只是提供一种遍历的机制。用 next 和 nextIndex 来指示下一个要迭代的元素,lastReturned 表示当前停留的节点,expectedModCount 防止并发修改。
next 函数
迭代器最重要的函数,如下:
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasNext() {
return nextIndex < size;
}
先看 hasNext 方法,return nextIndex < size;
,很简单,下一个迭代的元素超出了范围,会返回false。
调用 next 方法,首先会判断 hasNext。合法的话,设置 next,nextIndex 和 lastReturned 的值,并正确返回。
调用原 LinkedList 的增删函数
迭代器本身不存储数据,但是可以通过调用原 LinkedList 的增删函数对 LinkedList 进行增删。以 add 为例:
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
在最后,有一句 expectedModCount++;
,这是因为调用原 LinkedList 的增删函数,会导致modCount++
,所以这里要更新对应的 expectedModCount。否则,调用迭代器的函数时,过不了下面的判断:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
调用迭代器函数,都会进行此判断。所以,如果用迭代器的时候,其它迭代器或原 LinkedList 进行了结构性修改,那么这个迭代器就废了,任何函数都不会调用成功。
forEachRemaining
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
// 并发修改检查以及不越界
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
对 next 以及之后的节点的 item 属性调用action.accept(next.item);
进行操作,Consumer<? super E> action
是 Java 提供的函数式接口,如果某次 while 循环里的条件因为modCount != expectedModCount
而停止,跳出循环后会调用checkForComodification();
,必然会抛出并发修改异常。
迭代器剩下的函数不再一一介绍了。
克隆方法
LinkedList 的克隆方法还有点意思,可以讲讲。
private LinkedList<E> superClone() {
try {
return (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
public Object clone() {
LinkedList<E> clone = superClone();
// 重置元素
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
// 重新初始化
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
return clone;
}
可能有人会问,调用了LinkedList<E> clone = superClone();
不就可以了,为啥要后面那一堆操作。关于 Java 的对象克隆,可以参考我之前的一篇博客:由Arrays.sort()简谈Comparable和Comparator 。
总之,如果你简单调用LinkedList<E> clone = superClone();
,得到的是非常非常浅的克隆,first 和 last 元素都指向原 LinkedList 的first 和 last 的引用。LinkedList 就是靠 first 和 last 遍历整个链表,进行一系列操作,如果是这样的克隆,那克隆根本没有任何作用,新得到的 LinkedList 只是个原 LinkedList 的别名罢了。
故 clone 方法后有这个循环:
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
别忘了,add 方法会调用 linkLast 方法,该方法有一句final Node<E> newNode = new Node<>(l, e, null);
,会创建新节点,这样,克隆的 LinkedList 每个节点都是自己的了,才真正有了一点克隆的作用。当然,两个链表的节点的 item 仍然是一个地方对象的引用,其实还是有点问题的。Java 的作者对此 clone 方法的注释如下:
Returns a shallow copy of this {@code LinkedList}. (The elements
* themselves are not cloned.)
应用
LinkedList 实现了 Deque 和 List 的接口,可以作为 List,可以作为双端队列,普通队列,可以作为单链表,双链表,由于实现了 pop 和 push 方法,甚至还可以用作栈,可以说用途非常广泛了。
联想到 Redis 的 List 结构
反正我看完这个 LinkedList 的双向链表实现之后,立刻联想到了 Redis 的 List,Redis 的 List 在节点比较多的时候,也是双向链表实现。
总结
本文对 LinkedList 的源码进行了全面的梳理,LinkedList 的优势是灵活,可以在两头增删元素,获取链表长度也是 O(1) 的复杂度。比单链表好的地方就是由于每个节点都有前驱,删除中间节点简单,在某节点前增加元素也简单(不需要额外找前驱节点)。这块代码不难,感兴趣可以自己看看,学一学双链表的操作。
PS
作者已经更新了 ArrayList 和 LinkedList 的源码解析,之后会陆续发 HashMap,HashSet,LinkedHashMap,CopyOnWriteArrayList 等集合的源码解析,敬请关注哦~~