LinkedList类是基于双向链表实现的,它在内存中不占用连续的内存空间,里面的每个元素都能指向前一个元素和后一个元素,这使得它可以双向遍历。LinkedList类和ArrayList类相比,不具备快速随机访问的能力,但是插入和删除元素要比ArrayList类高效。
public class LinkedList
extends AbstractSequentialList
implements List, Deque, Cloneable, java.io.Serializable
分析:
* LinkedList类继承的是AbstractSequentialList类:AbstractSequentialList类提供了一个对list的骨架型的实现,实现了一个按次序访问的功能。如果要实现随机访问,应该先使用AbstractList。也就是说LinkedList类也是不支持快速随机访问的。
* 实现List接口,一是为了增加可读性,清晰看到实现的接口,二是降低维护成本,如果AbstractSequentialList类不实现List了,LinkedList类也不受影响。
* 实现Deque接口,实现双端队列的一些功能方法,那么自然LinkedList类也是双端队列。
* Cloneable接口也是克隆标记接口,表示此类可以被克隆,此类的实例可以调用clone()方法;未实现Cloneable接口的类的实例调用clone()方法会报错,在Object类中已经定义。
* Serializable接口是序列化标记接口,表示此类可以被序列化到内存中。目的是为类可持久化,比如在网络传输或本地存储,为系统的分布和异构部署提供先决条件。
//已有元素个数
transient int size = 0;
//头结点
transient Node first;
//尾结点
transient Node last;
//序列化UID
private static final long serialVersionUID = 876323262645176354L;
与ArrayList相比,LinkedList少了容量这个成员变量,所以理论上LinkedList是可以无限延长的,所以也不需要什么扩容之类的。transient关键字已经说过很多次了,就是标记一下,在序列化的时候不把修饰的字段进行数据持久化。
//无参构造函数
public LinkedList() {
}
//传入一个集合的构造函数
public LinkedList(Collection extends E> c) {
this();
addAll(c);
}
值得注意的是,当传入一个集合的时候,返回的LinkedList对象内元素的顺序是集合的迭代顺序。
private static class Node {
E item;
Node next;
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList存放的元素是其静态内部类Node的实例,通Node的结构我们可以清晰的看到,每个Node都会记录上一个Node和下一个Node的引用,这样就保证了LinkedList双向列表的结构。
//添加结点到头结点
private void linkFirst(E e) {
//获取当前List的头结点,取名 “老头结点”
final Node f = first;
//组建新结点,把新结点的前结点设置为null,把新结点的后结点设置为 “老头结点”
final Node newNode = new Node<>(null, e, f);
//把新结点设置为头结点,取名 “新头结点”
first = newNode;
//判断“老头结点”是否为null,也就是判读之前是否是空链表
if (f == null)
//如果之前就是空链表,那么新节点也是尾结点
last = newNode;
else
//如果之前不是空链表,那么将“老头结点”的上一个节点指向新节点
f.prev = newNode;
//链表长度+1
size++;
//修改次数+1,为了保证多线程高并发的情况下,能够快速失败
modCount++;
}
//添加新节点作为尾结点
void linkLast(E e) {
//获取原List的尾结点,取名“老尾结点”
final Node l = last;
//创建新结点,把新节点的前结点设置为“老尾结点”,把新结点的后结点设置为null
final Node newNode = new Node<>(l, e, null);
//把新节点设置为尾结点
last = newNode;
//判断原链表List是否为null
if (l == null)
//如果原List为null,那么新结点即是尾结点,也是头结点,所以这里把新结点设置为头结点
first = newNode;
else
//如果原List不为null,那么就把“老尾结点”的后结点设置为新结点
l.next = newNode;
//元素数量+1
size++;
//修改次数+1
modCount++;
}
//在结点succ之前插入内容为e的节点
void linkBefore(E e, Node succ) {
//先获取succ的前结点pred
final Node pred = succ.prev;
//创建新结点,新结点的前结点为succ的前结点pred,新结点的后结点为succ
final Node newNode = new Node<>(pred, e, succ);
//同是将新节点newNode设置为succ的前结点
succ.prev = newNode;
//如果succ的前结点pred为null,则表示succ是原来的头结点,所以新节点newNode成为新的头结点
if (pred == null)
first = newNode;
else
//如果succ不是头结点,那么就将pred的后结点指向新结点
pred.next = newNode;
size++;
modCount++;
}
值得注意的是,插入元素时间复杂度是O(1),但是如果是单向链表,则因为无法获取当前结点的前结点,而导致只能通过遍历去获取,那么时间复杂度就变成了O(n)。
//移除头结点
private E unlinkFirst(Node f) {
//获取头节点的内容element
final E element = f.item;
//获取头结点的后结点next
final Node next = f.next;
//将头结点的内容置为null
f.item = null;
//将头结点的后结点也置为null,因为头结点的前结点本就为null,这时候其实这个结点就已经全部为null了,便于GC回收
f.next = null; // help GC
//把刚刚获取的节点next作为新的头结点
first = next;
//判断next是否为null
if (next == null)
//如果此时next为null,则表示此链表没有结点了,把尾结点也置为null
last = null;
else
//如果此时next不为null,则表示此链表还有结点,所以把next的前结点置为null
next.prev = null;
//元素数量-1
size--;
//修改次数+1
modCount++;
//返回移除节点的内容
return element;
}
unlinkFirst()方法是默认参数结点f就是头结点,所以源码中也没有加判断,这是因为unlinkFirst()方法和下面的unlinkLast()方法都要结合到实际的场景中使用,也就是链表的增删改查操作,里面调用的都是这些基础操作,而调用之前是做了判断的,具体我们下文再看。
//移除尾结点
private E unlinkLast(Node l) {
//获取结点l的内容element
final E element = l.item;
//获取l的前结点prev
final Node prev = l.prev;
//设置l的内容为null
l.item = null;
//设置l的前结点为null,以便GC回收
l.prev = null; // help GC
//设置l的前结点prev为新的尾结点
last = prev;
//判断prev是否为null
if (prev == null)
//如果prev为null,表示链表中已无结点存在,则设置头结点也为null
first = null;
else
////如果prev不为null,则设置prev结点的后结点为null
prev.next = null;
//元素数量-1
size--;
//修改次数+1
modCount++;
//返回移除节点的内容element
return element;
}
//移除指定非空结点
E unlink(Node x) {
//分别获取要移除的结点的内容、前结点、后结点
final E element = x.item;
final Node next = x.next;
final Node prev = x.prev;
//如果前结点为null,表示移除的结点是头结点
if (prev == null) {
//将后结点next设置为新的头结点
first = next;
//如果移除的结点不是头结点
} else {
//那么设置前结点的后结点为后结点next
prev.next = next;
//并且设置结点x的前结点为null
x.prev = null;
}
//如果后结点为null,表示移除的结点是尾结点
if (next == null) {
//那么将x的前结点prev设置为尾结点
last = prev;
} else {
//如果后结点不为null,就将后结点的前结点设置为x的前结点prev
next.prev = prev;
//并且将x的后结点置为null
x.next = null;
}
//此时的x结点,前结点为null,后结点为null,再将值item设置为null,整个Node对象就为null了,以便GC回收
x.item = null;
//结点数量-1
size--;
//修改次数+1
modCount++;
//返回移除结点x的item值element
return element;
}
//移除一个内容为O的结点
public boolean remove(Object o) {
//先判断O是否为null,如果是,则遍历的时候,用==进行比较
if (o == null) {
for (Node x = first; x != null; x = x.next) {
if (x.item == null) {
//调用移除结点的方法unlink()
unlink(x);
return true;
}
}
//判断O不为null,则遍历的时候,用equals进行比较
} else {
for (Node x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
实际上到这个地方,LinkedList的内部方法基本就介绍完毕, 其他的方法基本都是进行了一些判断后,调用了上面的这些方法。我们再看几个常用的普通方法源码就一目了然。
//添加一个结点e
public boolean add(E e) {
//直接调用linkLast()在链表末尾添加结点
linkLast(e);
//如果成功则返回true
return true;
}
//在指定下标添加一个结点e
public void add(int index, E element) {
//判断是否越界,条件是index >= 0并且index <= size,源码如下
checkPositionIndex(index);
//如果index刚好等于size,表示是添加的尾结点,直接调用linkLast()方法
if (index == size)
linkLast(element);
else
//如果添加的结点不是尾结点,就直接调用linkBefore()方法
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
//从指定下标index开始,添加集合C中所有的元素到链表中
public boolean addAll(int index, Collection extends E> c) {
//老规矩,先判断下标是否越界
checkPositionIndex(index);
//将集合c转为Object数组a
Object[] a = c.toArray();
//得到a的长度numNew
int numNew = a.length;
//如果长度为0,那么表示集合c为null,直接返回false
if (numNew == 0)
return false;
//声明两个Node变量pred和succ
Node pred, succ;
//下面这个if判断,主要是为了获得插入坐标插入新Node后的前后结点
//如果index刚好等于size,也就是说是从链表尾部开始添加,那么插入段的前结点就是原链表的尾结点last,插入段的尾结点就为null
if (index == size) {
succ = null;
pred = last;
//如果是从链表中间插入,则插入段的尾结点就是插入前原链表index位置上的结点,而插入段的前结点就是原链表Index位置上的结点的前结点
} else {
succ = node(index);
pred = succ.prev;
}
//开始循环插入结点
for (Object o : a) {
//创建新结点newNode,内容为e,前结点就是我们上面记录的前结点pred,后结点是下一个新结点(如果还有的话)
@SuppressWarnings("unchecked") E e = (E) o;
Node newNode = new Node<>(pred, e, null);
//判断前结点pred是否为null,如果是,那么这个新结点newNode就是头结点first
if (pred == null)
first = newNode;
//如果pred不为nul,那么就直接设置前结点的后结点为当前新结点newNode
else
pred.next = newNode;
//将新的结点又设置为下一个节点的前结点
pred = newNode;
}
//循环结束后,判断尾结点是否为null,如果尾结点succ为null,表示最后添加的结点(上一行代码可知被设置为了pred)就是整个新链表的尾结点last
if (succ == null) {
last = pred;
//如果succ不为null,表示是在原链表中间插入的,那么设置记录的前结点的后结点为尾结点;设置记录的尾结点设置给后结点的前结点。上面过程中,succ在被记录后是一直没变的,pred会一直被指向最新的结点。
} else {
pred.next = succ;
succ.prev = pred;
}
//结点数据增加数组a的长度
size += numNew;
//修改记录+1
modCount++;
//返回true表示添加成功
return true;
}
以上方法中addAll(int index, Collection extends E> c)方法可能看代码有些晦涩,但是脑海中有整个过程模型,就很好理解了。如下图:
public E get(int index) {
//检查是否越界
checkElementIndex(index);
//返回对应下标的Node结点的item
return node(index).item;
}
Node node(int index) {
//判断坐标index是否小于1/2的size,也就是说在前半部分的话,就只用正向遍历前半段链表
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//如果是在后半部分的话,就只用反向遍历后半段链表
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
LinkedList类的实例在查找结点元素的时候,巧妙的利用了自己双向链表的特点,虽然时间复杂度还是O(n),但是也算是巨大的提升了。
public Object clone() {
LinkedList clone = superClone();
//初始化克隆后的对象clone
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
//循环往clone中添加元素的引用,所以这是浅克隆
for (Node x = first; x != null; x = x.next)
clone.add(x.item);
return clone;
}
//直接将当前的链表转化为数组
public Object[] toArray() {
//创建链表长度size的Object数组result
Object[] result = new Object[size];
int i = 0;
//循环往result中添加元素
for (Node x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
//将当前链表的内容转化到指定数组a中
public T[] toArray(T[] a) {
//先判断传入的数组a能否装得下整个链表,装不下则重新创建一个数组
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a;
//从坐标0开始往数组a中放入链表元素,是直接替换,而不是增加
for (Node x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size)
a[size] = null;
return a;
}
从代码中可以看出,数组a的length小于等于size时,a中所有元素被覆盖,被拓展来的空间存储的内容都是null;若数组a的length的length大于size,则0至size-1位置的内容被覆盖,size位置的元素被设置为null,size之后的元素不变。
除了以上的方法外,LinkedList类除了Node以外,还有两个内部类分别是ListItr类和DescendingIterator类。我们再来详细看下源码:
// 最近一次返回的节点,也是当前持有的节点
private Entry lastReturned = header;
// 对下一个元素的引用
private Entry next;
// 下一个节点的index
private int nextIndex;
private int expectedModCount = modCount;
// 构造方法,接收一个index参数,返回一个ListItr对象
ListItr(int index) {
// 如果index小于0或大于size,抛出IndexOutOfBoundsException异常
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
// 判断遍历方向
if (index < (size >> 1)) {
// next赋值为第一个节点
next = header.next;
// 获取指定位置的节点
for (nextIndex=0; nextIndexindex; nextIndex--)
next = next.previous;
}
}
// 根据nextIndex是否等于size判断时候还有下一个节点(也可以理解为是否遍历完了LinkedList)
public boolean hasNext() {
return nextIndex != size;
}
// 获取下一个元素
public E next() {
checkForComodification();
// 如果nextIndex==size,则已经遍历完链表,即没有下一个节点了(实际上是有的,因为是循环链表,任何一个节点都会有上一个和下一个节点,这里的没有下一个节点只是说所有节点都已经遍历完了)
if (nextIndex == size)
throw new NoSuchElementException();
// 设置最近一次返回的节点为next节点
lastReturned = next;
// 将next“向后移动一位”
next = next.next;
// index计数加1
nextIndex++;
// 返回lastReturned的元素
return lastReturned.element;
}
public boolean hasPrevious() {
return nextIndex != 0;
}
// 返回上一个节点,和next()方法相似
public E previous() {
if (nextIndex == 0)
throw new NoSuchElementException();
lastReturned = next = next.previous;
nextIndex--;
checkForComodification();
return lastReturned.element;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex-1;
}
// 移除当前Iterator持有的节点
public void remove() {
checkForComodification();
Entry lastNext = lastReturned.next;
try {
LinkedList.this.remove(lastReturned);
} catch (NoSuchElementException e) {
throw new IllegalStateException();
}
if (next==lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = header;
expectedModCount++;
}
// 修改当前节点的内容
public void set(E e) {
if (lastReturned == header)
throw new IllegalStateException();
checkForComodification();
lastReturned.element = e;
}
// 在当前持有节点后面插入新节点
public void add(E e) {
checkForComodification();
// 将最近一次返回节点修改为header
lastReturned = header;
addBefore(e, next);
nextIndex++;
expectedModCount++;
}
// 判断expectedModCount和modCount是否一致,以确保通过ListItr的修改操作正确的反映在LinkedList中
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
ListItr实现了ListIterator接口,可知它是一个迭代器,通过它可以遍历修改LinkedList。 在LinkedList中提供了获取ListItr对象的方法:listIterator(int index)。
// 获取ListItr对象
final ListItr itr = new ListItr(size());
// hasNext其实是调用了itr的hasPrevious方法
public boolean hasNext() {
return itr.hasPrevious();
}
// next()其实是调用了itr的previous方法
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
这是一个反向的Iterator,也是调用的ListItr类中的方法。
LinkedList类是基于双向链表实现的,并且在内存中不连续,使用上没有大小限制,理论上可以无限增加,所以LinkedList类中也没有扩容方法;此外LinkedList类不具备随机访问,插入和删除效率较高,但是查找效率较低。LinkedList类允许结点的元素可以为null,也允许重复,并且也有modCount来实现快速失败的机制。双向链表由于可以反向遍历,相较于单向链表在某些操作上具有性能优势,但是由于每个结点都需要额外的内存空间来存储前驱指针,所以双向链表相对来说需要占用更多的内存空间,这也是空间换时间的一种体现。
更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!