唯有心静,身外的繁华才不至于扭曲和浮躁,才能倾听到内心真实的声音。


内容



1.LinkedList概述

LinkedList:JDK1.2的时候添加的集合类,底层是使用双向链表实现的线性表,可用作队列、双端队列,也可以模拟栈空间的储存。

LinkedList类图:

Java容器深度总结:LinkedList_迭代器

LinkedList定义:

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

可以看出来:

  • LinkedList< E > :LinkedList支持泛型。
  • 继承AbstractSequentialList< E >:该类是链表实现的线性表的统一父类,AbstractSequentialList继承了AbstractList,但该类只支持按次序访问。
  • 实现了List< E >:因此也可以使用get(index)访问元素,但是效率较低,因为该索引由LinkedList内部维护,需要从链表头或尾顺序遍历。
  • 实现了 Deque< E >:表明LinkedList支持双端队列(Deque)的相关操作。
  • 实现了Cloneable、Serializable接口:表明LinkedList可以通过调用clone()方法进行克隆,并且LinkedList是支持序列化的。

注意:LinkedList没有实现RandomAccess接口,因此它不具备快速随机访问的能力。

Tip:

LinkedList在不同JDK版本有细微差异:

  • JDK1.5时LinkedList直接实现Queue(队列)接口;JDK1.6开始改为实现Deque(双端队列)接口。
  • JDK1.6时,LinkedList由带头结点的双向循环链表实现;JDK1.7开始由不带头结点的普通双向链表实现。

文章主要分析JDK1.8中LinkedList的实现,旧版本会有所提及。

2.LinkedList数据结构

JDK1.6使用的是一个带有头结点的双向循环链表,头结点不存储实际数据,因此只能从头结点开始遍历查找。

Java容器深度总结:LinkedList_java_02

JDK1.7开始,使用的是不带头结点的普通的双向链表,增加两个节点指针first、last分别指向首尾节点。因此可以从头或尾结点开始遍历查找。

Java容器深度总结:LinkedList_链表_03

3.Node结点

LinkedList 不仅要保存相应的元素,还需要记录这个元素的前驱和后继,因此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;
}
}

注意:JDK1.6中,Node被叫做Entry,两者只是名字不同。

4.成员变量和构造函数

JDK1.6:

private transient Entry<E> header = new Entry<E>(null, null, null);

private transient int size = 0; // 元素个数

public LinkedList() {
header.next = header.previous = header;
}

public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

JDK1.6时,header为头结点指针,指向一个空的Entry结点,无参构造方法中,让这个空Entry结点的前驱后继都指向自己。带参构造传入一个Collection集合,先调用无参构造初始化双线循环链表,然后调用addAll()将元素添加到链表中。

JDK1.7开始:

transient int size = 0; //元素个数
transient Node<E> first; // 首元素结点指针
transient Node<E> last; // 尾元素结点指针

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

JDK1.7开始,无参构造为空实现,即首元素指针(first)和尾元素结点指针(last)都为null。带参构造同JDK1.6一样。

5.操作链表的底层方法

LinkedList提供了一系列直接操作链表的方法,很多方法都是基于这些底层方法实现的。

5.1 linkFirst(E e)

在表头添加指定元素e。

private void linkFirst(E e) {

//使节点f指向原来的头结点
final Node<E> f = first;

//新建节点newNode,节点的前驱指针指向null,后指针原来的头节点
final Node<E> newNode = new Node<>(null, e, f);

//头指针指向新的头节点newNode
first = newNode;

//如果原来的头结点为null,更新尾指针,否则使原来的头结点f的前置指针指向新的头结点newNode
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

在表头添加指定元素,只需要将新创建的结点(newNode)插在first指向的首结点之前,然后更新first指向新插入的结点(newNode)即可。

但是需要注意的是:

  • 如果向空链表插入新结点,那么还需让尾元素指针(last)也指向这个新结点,因为只有一个结点时,该结点既是首结点又是尾结点。
  • 不是空链表时,还需要将原来的首结点(f)的前驱指向新结点(newNode)。

最后再让size自增维护元素个数,modCount自增,记录一次结构修改。

5.2 linkLast(E e)

在表尾添加指定元素e。

void linkLast(E e) {

//使节点l指向原来的尾结点
final Node<E> l = last;

//新建节点newNode,节点的前指针指向l,后指针为null
final Node<E> newNode = new Node<>(l, e, null);

//尾指针指向新的头节点newNode
last = newNode;

//如果原来的尾结点为null,更新头指针,否则使原来的尾结点l的后置指针指向新的头结点newNode
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

在尾部插入新结点的逻辑与linkFirst()相似。

5.3 linkBefore(E e, Node< E > succ)

在指定结点succ之前插入指定元素e。

void linkBefore(E e, Node<E> succ) {

// assert succ != null;
//获得指定节点的前驱
final Node<E> pred = succ.prev;

//新建节点newNode,前置指针指向pred,后置指针指向succ
final Node<E> newNode = new Node<>(pred, e, succ);

//succ的前置指针指向newTouch
succ.prev = newNode;

//如果指定节点的前驱为null,将newTouch设为头节点。否则更新pred的后置节点
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}

在指定结点前插入指定元素也比较简单,只需要将新节点(newNode)先插入到指定结点(succ)和 指定结点(succ)的前一个结点之间。

插入之后,修改指定结点(succ)前驱为newNode。由于pred可能为null(succ本身就是首结点时),所以若pred为空,就要让首元素指针(first)指向新结点(newNode),否则就让pred的后继为新结点(newNode)。

最后再让size自增维护元素个数,modCount自增,记录一次结构修改。

5.4 unlinkFirst( Node< E > f)

删除并返回头结点f。

private E unlinkFirst( Node<E> f) {

// assert f == first && f != null;
// 保存头结点的值,用于返回
final E element = f.item;

// 保存头结点指向的下个节点
final Node<E> next = f.next;

//头结点的值置为null
f.item = null;
//头结点的后置指针指向null,帮助垃圾回收
f.next = null; // help GC

//将头结点置为next
first = next;

//如果next为null,将尾节点置为null,否则将next的后置指针指向null
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
//返回被删除的头结点的值
return element;
}

删除首结点时,需要将首元素结点(first)指向原首结点(f)的后继即可。其中需要注意删除f结点后成为空链表的情况。

5.5 unlinkLast(Node< E > l)

删除并返回尾结点 l 。

private E unlinkLast(Node<E> l) {

// assert l == last && l != null;
// 保存尾节点的值,用于返回
final E element = l.item;

//获取新的尾节点prev
final Node<E> prev = l.prev;

//旧尾节点的值置为null
l.item = null;
//旧尾节点的后置指针指向null,帮助垃圾回收
l.prev = null; // help GC

//将新的尾节点置为prev
last = prev;

//如果新的尾节点为null,头结点置为null,否则将新的尾节点的后置指针指向null
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
//返回被删除的尾节点的值
return element;
}

删除尾结点逻辑与unlinkFirst()相似。

5.6 unlink(Node< E > x)

删除指定结点x,返回被删除结点中保存的元素。

E unlink(Node<E> x) {

// assert x != null;
// 保存指定节点的值
final E element = x.item;

// 获取指定节点的下个节点next
final Node<E> next = x.next;

// 获取指定节点的前一个节点prev
final Node<E> prev = x.prev;

//如果prev为null,那么next为新的头结点,
//否则将prev的后置指针指向next,x的前置指针指向null
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}

//如果next为null,那么prev为新的尾结点,
//否则将next的前置指针指向prev,x的后置指针指向null
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

//x的值置为null
x.item = null;
size--;
modCount++;
//返回被删除的节点的值
return element;
}

删除指定结点时,需要注意该结点的前驱和后继为null的情况。

5.7 node(int index)

获取索引处结点。

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

  • 当 index < size / 2 时,从首元素结点开始遍历;
  • 否则从尾元素结点开始遍历;

从中可以发现,带有头结点指针和尾结点指针的双向链表在查询时,会根据下标index在链表中的相对位置来进行遍历,一定程度上加快了查询速度。

6.常用方法

6.1 列表方法

签名

描述

复杂度

E get(int index)

获取指定位置的元素

O(n)

E set(int index, E element)

设置指定位置的元素值

O(n)

void add(int index, E element)

在指定位置添加元素

O(n)

E remove(int index)

删除指定位置的元素

O(n)

E remove(Object o)

移除链表中第一次出现的指定元素

O(n)

int indexOf(Object o)

查询指定元素首次的位置

O(n)

boolean contains(Object o)

是否包含某个元素

O(n)

int size()

返回链表中的元素个数

O(1)

boolean isEmpty()

链表是否为空

O(1)

void clear()

从链表中移除所有元素

O(n)

6.2 队列(Queue)操作

签名

描述

复杂度

E peek()

返回头节点(队首)元素

O(1)

E element()

返回头节点(队首)元素

O(1)

E poll()

返回并删除头节点(出队 )

O(1)

E remove()

返回并删除头节点(出队 )

O(1)

boolean offer(E e)

尾结点添加元素(入队)

O(1)

6.3 双端队列(Deque)操作

签名

描述

复杂度

E peekFirst()

返回队列的头元素

O(1)

E peekLast()

返回队列的尾元素

O(1)

boolean offerFirst(E e)

插入指定元素到队列头部

O(1)

boolean offerLast(E e)

插入指定元素到队列尾部

O(1)

E pollFirst()

队首出队

O(1)

E pollLast()

队尾出队

O(1)

void push(E e)

入栈

O(1)

E pop()

出栈

O(1)

7.迭代器

7.1 ListIterator迭代器

LinkedList覆盖了listIterator()方法,返回了ListItr类的实例对象。

public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}

ListItr是LinkedList的内部类,实现了ListIterator接口:

private class ListItr implements ListIterator<E>

因此ListItr具备正向、反向迭代的能力。ListItr是快速失败(fail-fast)的。

7.2 Iterator迭代器

LinkedList没有覆盖iterator()方法,因此继承了父类AbstractSequentialList中的iterator()方法:

// AbstractSequentialList类中的iterator方法
public Iterator<E> iterator() {
return listIterator();
}

AbstractSequentialList调用了父类AbstractList中的listIterator()方法,返回ListIterator的实现类,这里发生了多态。

// AbstractList类中的 listIterator方法
public ListIterator<E> listIterator() {
return listIterator(0);
}

LinkedList重写了listIterator()方法,因此实际会执行LinkedList中的listIterator()方法。

public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}

因此,LinkedLis.iterator()方法实质上也会返回一个ListItr类的对象,只不过静态类型为Iterator,只能调用Iterator中定义的方法,因此只能正向迭代。

7.3 DescendingIterator迭代器

LinkedList.descendingIterator()方法可以返回一个DescendingIterator迭代器对象。

public Iterator<E> descendingIterator() {
return new DescendingIterator();
}

DescendingIterator迭代器实现了Iterator接口,因此只能正向迭代。该迭代器通过封装ListItr迭代器实现。

private class DescendingIterator implements Iterator<E> {

private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}

8.快速失败(fail-fast)

LinkedList像ArrayList一样,具有快速失败(fail-fast)机制,其实现原理与ArrayList相同,不再赘述。

9.clone()机制

LinkedList的clone()方法返回一个新的LinkedList,并且结点Node是深克隆的,但对于Node中保存的元素依然是浅克隆。

private LinkedList<E> superClone() {
try {
return (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}

public Object clone() {
LinkedList<E> clone = superClone();

// Put clone into "virgin" state
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;

// Initialize clone with our elements
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);

return clone;
}

10.序列化

LinkedList实现了writeObject()和readObject()方法,在序列化时不会序列化整个链表及其结点,只会序列化链表的元素个数和每个结点中保存的元素。

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {

// Write out any hidden serialization magic
s.defaultWriteObject();

// Write out size
s.writeInt(size);

// Write out all elements in the proper order.
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}

@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {

// Read in any hidden serialization magic
s.defaultReadObject();

// Read in size
int size = s.readInt();

// Read in all elements in the proper order.
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
}

11.LinkedList与ArrayList

  • LinkedList在进行插入、删除等操作时所花的开销都是固定的;而ArrayList只在数组末尾进行插入、删除很快,在数组中间插入、删除时会有元素移动,因此效率低。
  • LinkedList虽然提供了优化的查询方法,但效率依然较低(需要移动指针);ArrayList可以通过索引快速获取元素(随机访问快)。
  • ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。

总的来说:

  • ArrayList使用在查询比较多,但是插入和删除比较少的情况;
  • LinkedList用在查询比较少,而插入和删除比较多的情况;