深入学习Java之LinkedList
前言
LinkedList,作为最常用的List接口的实现类之一,与ArrayList基本齐名,两者都是在日常开发中非常常用的List类型,不过,两者的底层实现大不相同,这也导致了这两者的应用场景的不同,接下来,我们来详细学习LinkedList的具体实现
LinkedList的继承结构
这里跟前面学习ArrayList一样,首先先从宏观上来看下LinkedList的继承结构,然后自上而下学习其各个接口,最后再学习其源码的实现
LinkedList的继承结构如下所示
从上图中可以非常清楚地看到,LinkedList同样也是Iterable、Collection、AbstractCollection、ListAbstractList的子类,关于这几个类,由于在ArrayList部分我们已经进行过深入的学习了,这里就不再重复,如果看到这里还不熟悉的话,可以翻回前面的文章进行学习,接下来我们来学习新的接口中的方法,主要有Queue、Deque以及AbstractSequentialList
首先来看下Queue接口
相信学习过数据结构的你对Queue,也就是队列应该不陌生,队列是一个有序的数据结构,并且队列只有两种操作,入队和出队,其中的入队只能从后面入队,出队只能从前面出队,就跟日常生活中排队一样(不考虑插队的情况),结合上图可以看到,Queue接口对这些操作提供了定义,其中的add、offer是入队操作,也就是从队尾插入一个元素,不过add与offer的区别在于,如果队列的长度的有限制的,而当队列已经满的情况下,进行入队操作,add会抛出异常,而offer不会,同理,对于出队操作的remove、poll而言,两者都是从队首取出一个元素,remove在队列为空的情况下会抛出异常,而poll则仅是返回null而不会抛出异常,element、peek是查看队首的元素而不取出,这里需要注意跟remove、poll的区别,同理,element会在队列为空的情况下抛出异常,peek仅仅返回null而不抛出异常
接下来是Deque接口
Deque,全称是Double End Queue,也就是双端队列,所谓的双端队列,其实指的就是可以在两端进行操作的队列,这里的操作跟普通的队列操作一致,包括(入队,出队,以及查看队首元素),只不过由于是双端操作,所以Deque的方法比较多,Deque接口中同样还提供了将其当成Queue进行操作的普通方法,同时,Deque接口还提供了几个比较让人迷惑的方法,分别是push、pop这两者对应的是栈的操作,也就是说,Deque实际上还可以当成栈来使用,这也是可以理解的嘛,栈本质上就是一个只能在一端进行操作的线性表,而双端队列的每一个端都是可以进行插入和弹出的,所以本质上也是符合栈的操作特性的,只是这里将其混合在一起,个人感觉不是很符合逻辑上的想法
Deque中的方法,如上图所示,分别对应的是Queue的操作方法,只是由于可以进行双端操作,所以方法的数量加倍了,而且对应的是First、Last,也就是队首队尾同时可以进行操作,这些方法基本都是见名之意,理解的Queue接口的方法之后,理解Deque的方法就不难了
接下来是AbstractSequentialList
从一开始的LinkedList结构图中可以看到,AbstractSequentialList继承自AbstractCollection并且实现了List接口,之所以还需要提供AbstractSequentialList,是由于LinkedList的实现中,本质上是以链式结构将各个节点连接起来的,这种方式相对于以数组组成的线性结构的优势在于,对特点节点进行插入、删除操作的时候,所消耗的时间是常数级别的(数组形式的删除、插入操作需要移动整个数组中后半部分的内容),然而这种方式的缺点也是非常明显的,不支持索引操作,由于不是数组形式,所以是没有方法可以直接访问第n个元素的,而AbstractSequentialList抽象类,则定义了实现随机访问的操作方法,当然,这些离散访问的代价其实是相当高的,后面我们将看到在LinkedList中的具体实现
LinkedList的源码剖析
上面从宏观的角度了解了LinkedList的继承结构,总体上把握了LinkedList的方法之后,接下里我们来具体学习LinkedList的实现
在LinkedList的源码中,有一个Node类型的私有内部类,这个类就是构成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;
}
}
从上面的Node节点的定义中可以知道,LinkedList本质上是一个链表,并且是一个双向链表,所谓的双向链表指的就是链表中的节点都有一个前向指针和一个后向指针,分别指向自己的前向节点和后向节点,双向链表的优势在于,可以往任意方法查找元素,而不像普通的单向链表一个,每次都必须从链表的头部开始查找
学习完LinkedList的基本组成之后,接下来来学习下LinkedList的构造方法
// 空构造器,用于构造一个空链表
public LinkedList() {
}
//用另外一个容器来构造链表
public LinkedList(Collection<? extends E> c) {
this();
// 将该容器中的所有元素顺序地添加到链表中
addAll(c);
}
// 添加一个容器中的所有元素
public boolean addAll(Collection<? extends E> c) {
// 默认添加到当前元素的后面
return addAll(size, c);
}
// 在指定位置,添加一个容器中的所有元素
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
// 如果容器中没有元素,则不添加
if (numNew == 0)
return false;
// 新建两个节点,用于指示所要插入位置的元素的前后元素
Node<E> pred, succ;
// 如果index == size,表明插入的位置是后面,则succ
// 也就是后向指针为空,pred,前向指针指向当前最后一个元素
if (index == size) {
succ = null;
pred = last;
// 否则的话,获取当前index所指示的元素
// 并且使succ指向当前元素,pred指向其前面的元素
} else {
succ = node(index);
pred = succ.prev;
}
// 逐个添加元素
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// 使用每个元素构造一个节点,并且其前向节点为pred,
// 默认后向节点为null
Node<E> newNode = new Node<>(pred, e, null);
// 如果此时前向为null,说明此时的链表是空链表
// 则将first指向新建的节点,作为表头
if (pred == null)
first = newNode;
else // 如果不为空,则将pred的后向指针指向新建的节点
pred.next = newNode;
// pred后移,重新指向新建的节点
// 从这里也可以看出元素添加的顺序是添加到后面
// 也就是尾插入,而不是头插入
pred = newNode;
}
// 当容器中的元素添加完成后,如果succ为空,说明
// 所要插入的位置要么是最后一个元素所在位置,
// 要么就是链表为空,last指向最后一个元素
if (succ == null) {
last = pred;
// 如果不为空,说明链表中早已经有元素
// 则最后一个元素的后向指针指向该元素
// 并且该元素的前向指针指向最后一个元素
// 完成插入的过程
} else {
pred.next = succ;
succ.prev = pred;
}
// 统计链表中的元素总个数
size += numNew;
// 由于修改了链表结构,modCount加1
modCount++;
return true;
}
插入元素
// 由于是LinkedList实现了Deque接口,所以LinkedList本身也是双端
// 队列,所以支持双端的操作
// 添加到首部
public void addFirst(E e) {
linkFirst(e);
}
// 同样是插入到首部,可以看到,offerFirst其实
// 使用的也是addFirst
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
// 栈的添加方式push,默认也是添加到首部
public void push(E e) {
addFirst(e);
}
// 添加到首部的具体实现,其实就是头插法的应用
private void linkFirst(E e) {
final Node<E> f = first;
// 新建 节点,并且其前向指针指向空,后向指针指向链表的
// 头结点 first
final Node<E> newNode = new Node<>(null, e, f);
// fist重新指向新建的节点,也就是first重新成为头结点
first = newNode;
// 如果插入之前的头结点为空,说明链表为空
// 则尾指针同样指向新建的节点,此时只有一个节点
if (f == null)
last = newNode;
else // 如果不为空,则插入前的头结点的前向指针指向新建的节点,作为其后向节点
// 此时新建的节点真正成为头节点
f.prev = newNode;
size++;
modCount++;
}
// =========================================================
// 添加到尾部
public void addLast(E e) {
linkLast(e);
}
// 插入到尾部,本质也是使用addLast
public boolean offerLast(E e) {
addLast(e);
return true;
}
// 不指定添加方向
public boolean add(E e) {
// 默认添加到尾部
linkLast(e);
return true;
}
// 不指定方向,默认添加到尾部
public boolean offer(E e) {
return add(e);
}
// 插入到链表尾部,也就是尾插法
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
// 如果插入前尾节点为空,则说明插入前链表为空
// 头结点指向新建节点,此时链表只有一个节点
if (l == null)
first = newNode;
else // 插入前的尾节点指向新建的节点,新建节点作为新的尾节点
l.next = newNode;
size++;
modCount++;
}
// ===================================================
// 在指定位置插入元素
public void add(int index, E element) {
checkPositionIndex(index);
// 如果是尾部,则直接添加即可
if (index == size)
linkLast(element);
else // 否则,获取inde所在节点,并且链接到其前面
linkBefore(element, node(index));
}
// 将一个元素插入到指定元素前面
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
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++;
}
获取元素
// 获取第一个元素,直接返回first指向的节点的元素值即可
public E getFirst() {
final Node<E> f = first;
// 如果fist为空,说明此时链表为空
if (f == null)
throw new NoSuchElementException();
return f.item;
}
// 同样为获取第一个元素,但是当链表为空时,返回null而不是
// 抛出异常
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
// 同上
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
// 默认也是获取第一个元素,同样会抛出异常
public E element() {
return getFirst();
}
// 获得第index个节点的元素,注意,此操作比较消耗资源
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 获取第index个节点,从具体的实现中可以看到,基本上
// 使用索引来查找,是通过遍历半个链表来实现的
Node<E> node(int index) {
// 先判断所要获取的位置属于前半部分还是后半部分
// 这里设计的非常好,可以减少不必要的查找,提高效率,
// 学习了^_^
if (index < (size >> 1)) {
Node<E> x = first;
// 从头找第index个节点
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
// 从后往前找第index个节点
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
// ========================================================
// 获取最后一个元素,返回last指向的节点的值即可
public E getLast() {
final Node<E> l = last;
// 如果last为空,说明此时链表为空
if (l == null)
throw new NoSuchElementException();
return l.item;
}
// 获取最后一个元素,在链表为空时,返回null而不是抛出异常
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
替换元素
// 现获取第index个元素,然后将其值替换为新的值
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
删除元素
// 删除第一个元素
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
// 未指定方向时,默认删除第一个元素
public E remove() {
return removeFirst();
}
// 删除第一个元素,在链表为空时返回null,而不抛出异常
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// 未指定方向时,默认删除第一个元素,
// 在链表为空时返回null,而不抛出异常
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// 栈的弹出方式,默认弹出第一个元素
public E pop() {
return removeFirst();
}
// 删除第一个元素的具体实现
private E unlinkFirst(Node<E> f) {
final E element = f.item;
// 获取第一个元素的后一个元素
final Node<E> next = f.next;
// 删除第一个元素的值以及后向指针
f.item = null;
f.next = null; // help GC
// first指向新的节点
first = next;
// 如果新的节点为空,说明此时链表没有元素
// last也指向null
if (next == null)
last = null;
else // 删除next的前向指针,此时next为新的头结点
// 前向指针不应该还有指向
next.prev = null;
size--;
modCount++;
return element;
}
// ====================================================
// 删除最后一个元素
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
// 删除最后一个元素,链表为空时,返回null而不是抛出异常
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
// 删除最后一个元素的具体实现
private E unlinkLast(Node<E> l) {
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
// 如果此时最后一个元素的前向指针为空,
// 说明此时链表为空,first也应该指向null
if (prev == null)
first = null;
else
// 此时的prev为最后一个元素,next指向null
prev.next = null;
size--;
modCount++;
return element;
}
// =====================================================
删除第一个出现的元素
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
// 删除指定元素,默认是删除第一个出现的元素
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;
}
// 反向删除第一个出现的元素,原理同上
public boolean removeLastOccurrence(Object o) {
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
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;
// 如果前向节点为空,则说明该节点为第一个节点
// fist指向其后的节点
if (prev == null) {
first = next;
} else {
// 否则,其前节点的后向指针指向其后节点
// 相当于删除该节点
prev.next = next;
// 断开节点x的前向引用
x.prev = null;
}
// 如果后向节点为空,则说明该节点为最后一个节点
// last指向其前节点
if (next == null) {
last = prev;
} else {
// 否则,其后节点的前向指针指向其前节点
// 相当于删除该节点
next.prev = prev;
// 断开节点x的后向引用
x.next = null;
}
// 节点x指向空
x.item = null;
size--;
modCount++;
return element;
}
获得指定元素的索引
// 获取第一个出现的元素的索引
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
// 获得反向第一个出现的元素的索引
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
将链表转为数组
// 本质上是链表的遍历
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
// 同上,只是指定了元素的类型
public <T> T[] toArray(T[] a) {
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size)
a[size] = null;
return a;
}
总结
本小节主要先从宏观上了解LinkedList的继承结构,然后再逐个深入其实现接口的方法,最后,深入剖析了LinkedList的源码实现,从LinkedList的源码中可以看到,LinkedList比较适合从首部或者尾部进行元素的删除、插入,这是效率最高的,其次是在任意位置插入、删除元素,这里需要注意跟ArrayList的区别,LinkedList定位到某个元素所在位置时,需要遍历该链表,所以需要消耗O(n)时间,但是插入的时候不需要进行元素的移动,而ArrayList定位的时候只需要O(1)的时间,但是却需要移动元素,所以在数据量比较大的情况下,如果需要对数据进行频繁的插入、删除操作,使用LinkedList的优势大一些,而且由于LinkedList底层是通过节点之间的连接形成链的,所以不需要整一个连续的空间,这在某些情况下也是非常有利的条件(相对于ArrayList的数组结构)