一.LinkedList的关系依赖
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
继承了一个类,实现了四个接口
AbstractSequentialList抽象类,是AbstractList的子类。此类提供了List接口的基本实现,以最大程度地减少由顺序访问数据存储(例如链表)支持的实现此接口所需的工作。对于随机访问数据(例如数组),应优先使用AbstractList。
List接口,有序集合。允许重复的元素,也允许多个空元素,拥有List集合的常用方法。
Deque接口,是Queue的子接口,支持在两端插入和删除元素的线性集合。
Cloneable接口和Serializable接口都是标记接口,言外之意这两个接口没有内容。分别进行拷贝和序列化,这里不做详细概述。
注:不过需要注意的是,要使用Object的clone()方法必须实现Cloneable接口,否则会报java.lang.CloneNotSupportedException的异常。
类图
二.属性
/**
* 集合元素的个数
*/
transient int size = 0;
/**
* 指向第一个节点的指针
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* 指向最后一个节点的指针
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
三.构造方法
/**
* 无参构造
*/
public LinkedList() {
}
/**
* 构造一个包含特定集合的list,其元素按照迭代器的顺序返回。
*
* @param c 集合的元素都要放到list中
* @throws NullPointerException 如果这个集合为空,抛出空指针异常
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
四.代码分析
1.节点类
/**
* 节点类
*
* @param <E>
*/
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;
}
}
三个属性,分别是前驱节点(指针)的信息,元素,后继节点(指针)的信息。
2.添加元素
(1).添加元素
/**
* 将元素插入到list尾部
* <p>
* 此方法与addLast方法调用的是一个方法
*/
@Override
public boolean add(E e) {
linkLast(e);
return true;
}
LinkedList默认将元素添加到列表尾部,所以add方法与addLast方法的作用是一样的。下面来看一下linkLast方法。
/**
* 在链表尾部添加数据
*/
private void linkLast(E e) {
//获取当前尾部节点
final Node<E> l = last;
//声明新节点
final Node<E> newNode = new Node<>(l, e, null);
//使新节点成为新的尾部节点
last = newNode;
//如果l为空,那么是空链表。新节点即是链表的头部节点,也是链表的尾部节点
if (l == null)
{
first = newNode;
} else
//否则,将新节点指向之前尾部元素的后继节点
{
l.next = newNode;
}
//元素个数和链表修改次数进行计数
size++;
modCount++;
}
因为想在链表的尾部添加新元素,所以需要先拿到尾部节点的信息,存在l中。然后创建要添加的元素的节点,其中前驱节点就是刚才的l,元素为e,后继节点为null,因为在链表的尾部。接着把新节点变成链表的尾部元素。剩下最后一个步骤,将二者进行关联。判断之前的尾部节点是否为空,如果为空那么新节点的元素是空链表的唯一元素,否则,将新节点与之前的尾部节点进行关联即可。随后更新元素个数和链表修改的次数。
(2).在链表头部插入新元素
/**
* 将元素插入到链表头部
*/
@Override
public void addFirst(E e) {
linkFirst(e);
}
linkFirst具体内容如下:
/**
* 在链表头部添加数据
*/
private void linkFirst(E e) {
//获取当前头部节点
final Node<E> f = first;
//声明新节点
final Node<E> newNode = new Node<>(null, e, f);
使新节点成为新的头部节点
first = newNode;
//判空
if (f == null)
{
last = newNode;
} else
{
f.prev = newNode;
}
//计数
size++;
modCount++;
}
与上面的添加元素的方法类似
(3).在链表的特定位置插入元素
/**
* 在链表的特定位置插入元素
* 之前该位置的元素及后面的元素均要右移
*
* @param index 指定链表的位置
* @param element 元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
@Override
public void add(int index, E element) {
//校验索引位置是否合法,即index >= 0 && index <= size。不合法会报出索引越界的异常。
checkPositionIndex(index);
//如果索引位置为链表的size,那么添加的位置便是链表尾部
if (index == size)
{
linkLast(element);
} else
//在非空节点succ之前插入新节点
{
linkBefore(element, node(index));
}
}
在链表的指定位置添加元素,首先需要判断索引的位置是否合法,即在0和size之间,否则报出越界的异常。然后判断索引位置是否为size,如果为size,只需调用linkLast方法,在链表尾部添加新节点即可;如果不为size,还需调用linkBefore方法。下面来看一下linkBefore方法。
/**
* 在非空节点succ之前插入新元素
*/
void linkBefore(E e, Node<E> succ) {
//获取succ的前驱节点
final Node<E> pred = succ.prev;
//声明新节点
final Node<E> newNode = new Node<>(pred, e, succ);
//使新节点成为succ节点的前驱节点
succ.prev = newNode;
//判空
if (pred == null)
{
first = newNode;
} else
{
pred.next = newNode;
}
//更新计数
size++;
modCount++;
}
这几个添加方法本质上是一样的,理解其一,其他的也就都懂了。
(4).将集合作为参数,添加到链表中
/**
*
* 内容有些深奥,尚未消化
*
* @param c 集合
* @return {@code true} if this list changed as a result of the call
* @throws NullPointerException 如果集合为空,空指针异常
*/
@Override
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
3.删除元素
(1).删除元素
默认删除链表的头结点,与removeFirst方法作用一样。
/**
* 删除链表头元素
*
* @return 链表头元素
* @throws NoSuchElementException 如果链表为空
* @since 1.5
*/
@Override
public E remove() {
return removeFirst();
}
不进行传参的remove方法,默认删除链表的头结点,不过需要进行向上转型。ArrayList的remove方法必须传参。
/**
* 从列表中删除并返回第一个元素
*
* @return 被删除的节点
* @throws NoSuchElementException 如果链表为空
*/
@Override
public E removeFirst() {
//将链表的头指针传给f
final Node<E> f = first;
//如果节点为空
if (f == null) {
throw new NoSuchElementException();
}
return unlinkFirst(f);
}
unlinkFirst方法具体内容如下:
/**
* 删除非空头结点
*/
private E unlinkFirst(Node<E> f) {
// 确定f为非空头结点
//获取被删除头结点的元素
final E element = f.item;
//获取头结点的下一个节点
final Node<E> next = f.next;
//清空该节点的元素和后继指针
f.item = null;
f.next = null;
//f后继节点成为新的头节点
first = next;
//后继节点为空,那么该链表是空链表
if (next == null) {
last = null;
} else {
//否则,新的前驱节点置空
next.prev = null;
}
//更新计数
size--;
modCount++;
//返回被删除的元素
return element;
}
(2).删除指定位置的元素
/**
* 删除并返回指定位置的元素
*
* @param index 索引位置
* @return 被删除的元素
* @throws IndexOutOfBoundsException 索引越界
*/
@Override
public E remove(int index) {
//是否越界
checkElementIndex(index);
return unlink(node(index));
}
/**
* 删除非空节点
*/
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;
}
(3).删除链表中第一次出现的元素
/**
* 删除链表中第一次出现的元素,如果该元素存在。如果该元素不存在,那么链表没有变化
*
* @param o 要被删除的链表元素。
* @return {@code true} 删除成功返回真,否则返回假
*/
@Override
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;
}
判断要删除的元素是否为空,然后遍历链表,找到为空的元素后调用unlink方法就行删除;如果不为空,遍历列表,如果与链表中的元素相等,那么就行删除。总的来说都用调用unlink方法,那么这个判断有什么作用呢?我还不是很懂。希望有大佬指点一二。
(4).清空链表中的全部元素
/**
* 清空链表的全部元素
*/
@Override
public void clear() {
//遍历链表
for (Node<E> x = first; x != null; ) {
//先获取x的后继节点,便于清空x后进行操作
Node<E> next = x.next;
//将x节点置空
x.item = null;
x.next = null;
x.prev = null;
//指向x的后继节点,继续遍历
x = next;
}
//清空链表之后,头结点,尾节点清空
first = last = null;
//更新计数
size = 0;
modCount++;
}
4.替换链表特定位置的元素
/**
* 替换指定位置的元素
*
* @param index 索引
* @param element 要替换的元素
* @return 被替换的元素
* @throws IndexOutOfBoundsException 索引越界
*/
@Override
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):
/**
* 返回索引位置的节点
*/
Node<E> node(int index) {
//>>位运算符,右移1位,即除2
//如果索引小于链表容量的一半
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;
}
}
根据JDK8的源码进行解读,依照添删改查的顺序简单的就行了梳理,方便自己的理解和学习。由于部分代码还不是很明白,后期还会就行更深入的解析。
前路漫漫,编程作伴 --by mirror6