LinkedList是一个结合了队列和链表的数据结构,分别实现了List和Deque接口,并继承了AbstractSequentialList。
可以看到,LinkedList的集合继承体系主要有两个:Queue和List,分别包含了队列与链表的特征。同时它还包含了栈的特征。
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;
}
}
Node静态内部类包含三个成员:item,next和prev,分别表示当前结点的数据,后继结点与前驱结点。
依赖于这个Node类,LinkedList的成员变量如下:
// list的大小
transient int size = 0;
// 头结点
transient Node<E> first;
//尾结点
transient Node<E> last;
注意:三个变量都使用了transient修饰,说明默认序列化时不会序列化这些值。
// 空构造器
public LinkedList() {
}
// 使用另一个集合来初始化当前list,调用空构造器,然后调用addAll方法来赋值
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean add(E e) {
linkLast(e);
return true;
}
这里的主体就是调用了linkLast方法,说明默认的添加会将数据添加到链表尾部:
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++;
}
添加步骤如下:
- 获取尾结点last
- 创建一个新节点newNode,用传入的元素赋值,并将前驱赋值为last,后继赋值为null
- 让last指向newNode,作为新的尾结点
- 判断原list是否为空。如果为空,将头指针也赋值为last;否则将原尾结点的后继复制为新节点。
- 更新size和操作次数
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
* 如果不是有效的索引,则抛出异常
*/
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 判断当前索引是否存储了有效数据
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
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;
// 将当前结点的前驱置为null
x.prev = null;
}
// 如果后继为null,那么当前为尾结点
if (next == null) {
// 删除当前结点后,前驱变为尾结点
last = prev;
} else {
// 否则,将后继的前驱置为当前结点的前驱
next.prev = prev;
// 当前结点的后继置为空
x.next = null;
}
// 将当前结点的数据置为空(重要)
// size自减
x.item = null;
size--;
modCount++;
// 返回被删除的结点元素
return element;
}
单元素删除的主要步骤如下:
- 判断当前索引是否是有效的索引;
- 获取当前索引的值以及前驱与后继;
- 判断边界情况:是否是头结点或尾结点;按照边界的情况分别更新头结点与尾结点;
- 如果是普通的结点,需要分别更新前驱与后继;
- size自减并返回删除的元素值;
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 get(int index) {
checkElementIndex(index);
return node(index).item;
}
该方法除了判断越界以外,主体就是调用node方法获取索引的值:
Node<E> node(int 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;
}
}
这个方法分为两步:
- 如果索引位于list前一半,那么就从前向后遍历取值;
- 否则从后向前遍历取值
队列的插入操作是在队列的尾部进行插入,因此相当于重用了作为链表特征add(E e)方法:
public boolean offer(E e) {
return add(e);
}
队列的删除操作是删除队列的头部,调用了链表的unlinkFirst或removeFirst方法:
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E remove() {
return removeFirst();
}
这两个方法的区别主要在于:poll方法会在空list情况下做空判断,removeFirst在空list情况下会抛出一个NoSuchElementException
的异常:
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;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
很简单,略过
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
这两个方法仅仅是分别调用了addFirst与removeFirst方法,对外呈现的效果不同而已。
public void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
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);
}
序列化步骤如下:
- 写入默认的可序列化魔数
- 写入list的大小
- 循环依次写入list的每一个元素的值
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int size = s.readInt();
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
}
反序列化与序列化步骤相反:
读取默认的可序列化魔数
读取list的size并赋值
按次序读取元素并添加到list中
LinkedList在外部表现上,同时具备了链表,栈和队列的特征。但在其内部实现上,主体采用了链表进行实现的。栈和队列的操作只是复用了链表的方法。