LinkedList源码分析(JDK8)

一.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

猜你喜欢

转载自blog.csdn.net/LY121600HACKER/article/details/103685725