LinkedList源码深度解析以及迭代器机制【一万字】

基于JDK1.8对LinkedList集合的源码进行了深度解析,包括各种方法、链表构建、迭代器机制的底层实现。

1 LinkedList的概述

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

JDK1.2的时候添加的集合类,底层是使用双向链表实现的线性表,能够从链表头部或者尾部操作数据元素,可用作双端队列,也可以模拟栈空间的储存。

直接继承了AbstractSequentialList抽象类,该类是链表实现的线性表的统一父类。对于随机访问数据(如数组)实现的线性表,应该直接继承AbstractList。AbstractSequentialList也继承AbstractList。

实现了List接口,因此也可以使用“索引”方法get(index)访问元素,但是效率较低,因为所有的get(index)实际上都是从头或尾开始顺序遍历的!此索引非数组的索引,而是自己维护的索引!

没有实现RandomAccess标志性接口,不支持快速随机访问。

实现了Cloneable, Serializable标志性接口,支持克隆和序列化!

JDK1.5时添加实现了Queue(队列)接口,JDK1.6时,改为直接实现Deque(双端队列)接口,提供了一批额外的方法(下面会讲到)!

此实现不是同步的。可以使用:List list = Collections.synchronizedList(new LinkedList(...))来转换成线程安全的List!

2 LinkedList的API方法

这里的API方法,是指除了List接口通用API方法之外的LinkedList特有的API方法。

其诞生之时(JDK1.2),针对链表的头和尾部提供了一系列方法:

public void addFirst(E e)
将指定元素插入此列表的开头。
public void addLast(E e)
将指定元素添加到此列表的结尾。
public E getFirst()
返回此列表的第一个元素。如果此列表为空,则抛出NoSuchElementException异常。
public E getLast()
返回此列表的最后一个元素。如果此列表为空,则抛出NoSuchElementException异常。
public E removeFirst()
移除并返回此列表的第一个元素。如果此列表为空,则抛出NoSuchElementException异常。
public E removeLast()
移除并返回此列表的最后一个元素。如果此列表为空,则抛出NoSuchElementException异常。

在JDK1.5的时候由于实现了Queue(队列)接口,又添加了一些新的方法:

public E peek()
获取但不移除此列表的头(第一个元素)。如果此列表为空,则返回null。
public E element()
获取但不移除此列表的头(第一个元素)。如果此列表为空,则抛出NoSuchElementException异常。
public E poll()
获取并移除此列表的头(第一个元素)。如果此列表为空,则返回null。
public E remove()
获取并移除此列表的头(第一个元素)。如果此列表为空,则抛出NoSuchElementException异常。
public boolean offer(E e)
将指定元素添加到此列表的末尾(最后一个元素)。

在JDK1.6的时候,LinkedList不直接实现Queue,而是改为实现了一个Deque(双端队列,由Deque直接继承了Queue)接口,针对链表的头,针链表的尾部,又提供了一套新方法:

public boolean offerFirst(E e)
在此列表的开头插入指定的元素。
public boolean offerLast(E e)
在此列表末尾插入指定的元素。
public E peekFirst();
获取但不移除此列表的头(第一个元素)。如果此列表为空,则返回null。
public E peekLast();
获取但不移除此列表的最后一个元素。如果此列表为空,则返回 null。
public E pollFrist()
获取并移除此列表的头(第一个元素)。如果此列表为空,则返回 null。
public E pollLast()
获取并移除此列表的最后一个元素。如果此列表为空,则返回 null。

总结: 可以看出来,不同JDK时期提供了不同的方法实现了相同的功能,为了兼容版本,并没有移除老的方法,但是JDK1.6提供的新方法,比JDK1.2和JDK1.5的方法功能强大,我们在时使用,建议使用JDK1.6提供的新的API方法。

  1. JDK 1.2、JDK1.5:链表当中元素的个数为null时,在使用方法的时候,可能会抛出NoSuchElementException异常。
  2. JDK 1.6:链表当中元素的个数为null时,在使用方法的时候,不会报异常,会返回null。

3 LinkedList的源码解析

在这里插入图片描述

如上图所示,LinkedList属于线性表的链式存储结构的实现(Vector和ArrayLsit都是线性表的顺序存储结构的实现),LinkedList底层使用的双向链表结构,外部保持有一个头节点和一个尾节点引用,双向链表意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。

由于设计到数据结构,因此需要具有数据结构的部分知识。本文不会讲解数据结构的知识,主要讲解源码,关于线性表的顺序结构的实现和链式结构的实现以及区别,在我的 数据结构与算法专栏 中有专门的相关文章详细介绍:Java中的线性表数据结构详解以及实现案例。实际上线性表还是很好理解的!

3.1 主要类属性

    // LinkedList中元素个数
    transient int size = 0;
    //链表头节点
    transient Node<E> first;
    //链表尾节点
    transient Node<E> last;

头节点和尾节点的加入,让LinkedList可以从第一个节点添加也可以从最后一个节点添加,也就是说可以作为先进先出(FIFO)的队列,也可以作为LIFO(后进先出)的栈

3.2 Node节点

不同于ArrayList的内部数组具有的天然顺序存储,链表的实现需要记录某个元素的前驱后者后继,因此LinkedList使用一个对象Node来作为元素节点。

Node作为LinkedList的核心,也就是LinkedList中真正用来存储元素的数据结构。内部类Node就是实际的节点对象,或者说每个节点存放一个Node,用于存放实际元素的地方。

在JDK1.6及以前Node被叫做Entry。

private static class Node<E> {
    /**
     * 数据域,实际存放的元素,节点的值
     */
    E item;
    /**
     * 后继,储存下一个节点的引用
     */
    Node<E> next;
    /**
     * 前驱,储存上一个节点的引用
     */
    Node<E> prev;

    /**
     * Node节点的构造函数
     *
     * @param prev    前驱,即上一个节点的引用
     * @param element 存储的元素的值
     * @param next    后继,即下一个节点的应用
     */
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

3.3 构造器

LinkedList有两个构造器,并且都很简单。

3.3.1 LinkedList()

构造一个空列表,里面没有任何实现,仅仅只是将header节点的前一个元素、后一个元素都指向自身(null)。

public LinkedList() {
}

3.3.2 LinkedList(Collection<? extends E> c)

构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。

public LinkedList(Collection<? extends E> c) {
    //首先调用无参构造其,创建一个空列表
    this();
    //调用addAll方法
    addAll(c);
}

3.4 添加的方法

3.4.1 添加到尾部的方法

public boolean add(E e)

将指定元素添加到此列表的结尾。

源码解析:

public boolean add(E e) {
    //内部调用linkLast方法
    linkLast(e);
    return true;
}

/**
 * 将元素链接到链表尾部
 * @param e 被添加的元素
 */
void linkLast(E e) {
    //创建一个节点引用l,指向原尾节点
    final Node<E> l = last;
    //创建新的要插入的节点作为新的尾节点,它的prev就是当前尾节点,e就是存储的数据e,next节点为null
    final Node<E> newNode = new Node<>(l, e, null);
    //新的要插入的节点作为新的尾节点
    last = newNode;
    //判断原尾节点是否为null,即原集合有没有节点数据
    if (l == null)
        //如果原尾节点为null,即原集合没有节点数据,那么将新节点同时也作为首节点
        first = newNode;
    else
        //如果原尾节点不为null,即原集合有节点数据,那么将原来尾节点的next引用指向新的尾节点
        l.next = newNode;
    //节点数自增1
    size++;
    //结构改变的次数自增1
    modCount++;
}

其他添加到尾部的方法,原理都是一样的:

public void addLast(E e)

public void addLast(E e) {
    linkLast(e);
}

public boolean offerLast(E e)

public boolean offerLast(E e) {
    addLast(e);
    return true;
}

public boolean offer(E e)

public boolean offer(E e) {
    return add(e);
}

3.4.2 添加到头部的方法

public void addFirst(E e)

将指定元素插入此列表的开头。

public void addFirst(E e) {
    //内部调用linkFirst方法
    linkFirst(e);
}

/**
 * 将元素链接到链表头部
 * @param e 被添加的元素
 */
private void linkFirst(E e) {
    //创建一个节点引用l,指向原头节点
    final Node<E> f = first;
    //创建新的要插入的节点作为新的头节点,它的prev就是null,e就是存储的数据e,next节点为当前头节点
    final Node<E> newNode = new Node<>(null, e, f);
    //新的要插入的节点作为新的头节点
    first = newNode;
    //判断原头节点是否为null,即原集合有没有节点数据
    if (f == null)
        //如果原头节点为null,即原集合没有节点数据,那么将新节点同时也作为尾节点
        last = newNode;
    else
        //如果原头节点不为null,即原集合有节点数据,那么将原来头节点的prev引用指向新的头节点
        f.prev = newNode;
    //节点数自增1
    size++;
    //结构改变的次数自增1
    modCount++;
}

可以到,这添加到尾部节点的方法是差不多的。

其他添加到尾部的方法,原理都是一样的:

public boolean offerFirst(E e)

    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }

3.4.3 指定位置添加

public void add(int index,E element)
index - 要在其中插入指定元素的索引
element - 要插入的元素

在指定位置添加元素,多了一步是找到对应原索引处的元素,然后同样改变引用关系就行了,原索引的元素链接在新元素的后面。

源码解析:

add(int index,E element)

public void add(int index, E element) {
    //检查索引是否处于[0-size]之间
    checkPositionIndex(index);
    //判断索引index是否等于size
    if (index == size)
        //如果索引index等于size,那实际上就是添加在链表尾部,调用linkLast(element)方法就行了
        linkLast(element);
    else
        //如果不等,调用linkBefore方法
        linkBefore(element, node(index));
}

/**
 * 检查索引的方法
 * @param index 索引
 */
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 索引是否大于等于0,且小于等于size
 * @param index 索引
 * @return 是,返回true;否,返回false
 */
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

linkBefore方法需要给定两个参数,一个插入节点的值,一个查找到的指定索引处的node,所以我们首先需要调用node(index)方法去找到index对应的node。

/**
 * 获取对应索引处的Node节点
 * @param index 索引
 * @return 对应的节点
 */
Node<E> node(int index) {
    /*判断索引是否小于节点数量的一半,size >> 1是位运算,就是size/2的意思*/
    if (index < (size >> 1)) {
        /*如果索引小于节点数量的一半,那么从链表头开始查找对应索引处的节点*/
        //获取头节点的引用x
        Node<E> x = first;
        //循环获取x的next节点,获取一次i++,当i=index时,就表示找到了对应索引处的元素
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        /*如果索引大于节点数量的一半,那么从链表尾开始查找对应索引处的节点*/
        //获取尾节点的引用x
        Node<E> x = last;
        //循环获取x的prev节点,获取一次i--,当i=index时,就表示找到了对应索引处的元素
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

从node节点查找的实现可以看出来,这里的索引的查找,是从头或者尾开始循环遍历查找,时间复杂度为O(n),因此效率相对于ArrayLsit的O(1)的查找时间复杂度来说,效率比较低。

接下来是linkBefore方法:

/**
 * 在 指定节点 前插入一个元素,这里 指定节点对象不为 null
 * @param e 要存入的元素值
 * @param succ 指定节点
 */
void linkBefore(E e, Node<E> succ) {
    // 获取指定节点 succ 前面的一个节点
    final Node<E> pred = succ.prev;
    //新建一个节点,头部指向 succ 前面的节点,尾部指向 succ 节点,数据为 e
    final Node<E> newNode = new Node<>(pred, e, succ);
    //让 succ 节点头部指向 新建的节点
    succ.prev = newNode;
    //如果 succ 前面的节点为空,说明 succ 就是第一个节点,那现在新建的节点就变成第一个节点了
    if (pred == null)
        first = newNode;
    else
        //如果前面有节点,让前面的节点的下一个节点指向新节的点
        pred.next = newNode;
    //节点数自增1
    size++;
    //结构改变的次数自增1
    modCount++;
}

3.5 移除的方法

3.5.1 移除头节点的方法

E remove()

获取并移除此列表的头(第一个元素)。如果列表为null,则抛出NoSuchElementException异常。

/**
 * 获取并移除此列表的头(第一个元素)。如果列表为null,则抛出NoSuchElementException异常。
 *
 * @return 被移除的元素的值
 */
public E remove() {
    //内部直接调用removeFirst方法
    return removeFirst();
}

/**
 * 获取并移除此列表的头(第一个元素)。如果列表为null,则抛出NoSuchElementException异常。
 *
 * @return 被移除的元素的值
 */
public E removeFirst() {
    //获取头结点的引用
    final Node<E> f = first;
    //如果头节点为null,则抛出NoSuchElementException异常,说明该集合元素没有元素
    if (f == null)
        throw new NoSuchElementException();
    //如果头节点不为null,则调用unlinkFirst方法
    return unlinkFirst(f);
}

/**
 * 获取并移除此列表的头(第一个元素)。
 *
 * @param f 头结点
 * @return 头结点的值
 */
private E unlinkFirst(Node<E> f) {
    //获取该节点(头节点)的值
    final E element = f.item;
    //获取头结点的下一个节点
    final LinkedLisNode<E> next = f.next;
    //该节点(头节点)的节点值置null
    f.item = null;
    //该节点(头节点)的下一个节点引用值置null
    f.next = null;
    //使头节点引用指向下一个节点
    first = next;
    //如果下一个节点为null,即只有一个元素,此时集合为空
    if (next == null)
        //那么尾节点的引用也置null
        last = null;
    else
        //下一个节点不为null,即不只有一个元素,将下一个节点作为头节点,并将下一个节点的prev引用置null
        next.prev = null;
    //节点数自减1
    size--;
    //结构改变的次数自增1
    modCount++;
    //返回原头节点的值,此时没有任何引用指向原头节点,原头节点将被gc清理
    return element;
}

其他移除头结点的方法,原理都是一样的:

E removeFirst()

获取并移除此列表的头(第一个元素)。如果列表为null,则抛出NoSuchElementException异常。

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

E poll()

获取并移除此列表的头(第一个元素)。如果列表为null,则返回null。

public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

E pollFrist()

获取并移除此列表的头(第一个元素)。如果列表为null,则返回null。

public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

3.5.2 移除尾节点的方法

public E removeLast()

获取并移除此列表的尾(最后一个元素)。如果列表为null,则抛出NoSuchElementException异常。

/**
 * 获取并移除此列表的尾(最后一个元素)。如果列表为null,则抛出NoSuchElementException异常。
 *
 * @return 被移除的元素的值
 */
public E removeLast() {
    //获取尾结点的引用
    final Node<E> l = last;
    //如果尾节点为null,则抛出NoSuchElementException异常,说明该集合元素没有元素
    if (l == null)
        throw new NoSuchElementException();
    //如果尾节点不为null,则调用unlinkFirst方法
    return unlinkLast(l);
}

/**
 * 获取并移除此列表的尾(最后一个元素)。
 *
 * @return 被移除的元素的值
 */
private E unlinkLast(Node<E> l) {
    //获取该节点(尾节点)的值
    final E element = l.item;
    //获取尾节点的上一个节点
    final Node<E> prev = l.prev;
    //该节点(尾节点)的节点值置null
    l.item = null;
    //该节点(尾节点)的下一个节点引用值置null
    l.prev = null;
    //使尾节点引用指向上一个节点
    last = prev;
    //如果上一个节点为null,即只有一个元素,此时集合为空
    if (prev == null)
        //那么头节点的引用也置null
        first = null;
    else
        //上一个节点不为null,即不只有一个元素,将上一个节点作为尾节点,并将上一个节点的next引用置null
        prev.next = null;
    //节点数自减1
    size--;
    //结构改变的次数自增1
    modCount++;
    //返回原尾节点的值,此时没有任何引用指向原尾节点,原尾节点将被gc清理
    return element;
}

其他移除尾结点的方法,原理都是一样的:

public E pollLast()

获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。

public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}

3.5.2 指定位置移除

E remove(int index)

移除此列表中指定位置处的元素。返回从列表中删除的元素。

和删除头、尾节点的原理差不多,多了一步找到对应索引处的元素,然后改变前后节点的引用即可。

/**
 * 移除此列表中指定位置处的元素,后面索引处的元素链接在被删除的位置上,返回从列表中删除的元素。
 *
 * @param index
 * @return
 */
public E remove(int index) {
    //检查索引,超出范围index >= 0 && index < size,将抛出IndexOutOfBoundsException
    //注意这里和指定位置添加元素时检查的索引范围不一样,删除是[0,size),添加是[0,size]
    checkElementIndex(index);
    //调用unlink方法,传入该指定索引处的节点
    return unlink(node(index));
}

/**
 * 取消链接非空节点 x
 *
 * @param x 查找到的指定索引处的节点
 * @return 删除的节点的值
 */
E unlink(Node<E> x) {
    // 获取该节点的值
    final E element = x.item;
    //创建节点引用next,指向该节点的下一个节点
    final Node<E> next = x.next;
    //创建节点引用prev,指向该节点的上一个节点
    final Node<E> prev = x.prev;
    //判断该节点是否存在上一个节点
    if (prev == null) {
        //不存在上一个节点,则该节点为首节点,则让首节点引用指向该节点的下一个节点
        first = next;
    } else {
        //存在上一个节点,则上一个节点的下一个节点引用指向该节点的下一个节点
        prev.next = next;
        //该节点的上一个节点的引用指向null
        x.prev = null;
    }
    //判断该节点是否存在下一个节点
    if (next == null) {
        //不存在下一个节点,则该节点为尾节点.则让尾节点引用指向该节点的上一个节点
        last = prev;
    } else {
        //存在下一个节点,则下一个节点的上一个节点引用指向该节点的上一个节点
        next.prev = prev;
        //该节点的下一个节点的引用指向null
        x.next = null;
    }

    //该节点的值置空.此时没有任何引用指向该node节点对象,该对象将会被垃圾回器回收
    x.item = null;
    //节点数自减1
    size--;
    //结构改变的次数自增1
    modCount++;
    //返回该节点的值
    return element;
}

3.6 获取的方法

3.6.1 获取头节点的方法

public E getFirst()

返回此列表的第一个元素。如果此列表为空,则抛出NoSuchElementException异常。

public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

public E peek()

获取但不移除此列表的头(第一个元素)。如果此列表为空,则返回null。

public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

public E element()

获取但不移除此列表的头(第一个元素)。如果此列表为空,则抛出NoSuchElementException异常。

public E element() {
    return getFirst();
}

public E peekFirst();

获取但不移除此列表的第一个元素;如果此列表为空,则返回null。

public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
 }

3.6.2 获取尾节点的方法

public E getLast()

返回此列表的最后一个元素。如果此列表为空,则抛出NoSuchElementException异常。

public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

public E peekLast()

获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。

public E peekLast() {
    final Node<E> l = last;
    return (l == null) ? null : l.item;
}

3.6.3 指定位置获取

public E get(int index)

返回此列表中指定位置处的元素。

    /**
     * 返回此列表中指定位置处的元素。
     * @param index 指定索引
     * @return 该位置的元素的值
     */
    public E get(int index) {
        //检查索引,超出范围:index >= 0 && index < size,即[0,size),将抛出IndexOutOfBoundsException
        checkElementIndex(index);
        //返回找到的节点的值
        return node(index).item;
    }

可以看到内部调用的node方法顺序查找指定索引的元素,因此查找方法效率不是很高。

3.7 contains和indexOf

public boolean contains(Object o)

如果此列表包含指定元素,则返回 true。方法通过判断indexOf(Object o)方法返回的值是否是-1来判断链表中是否包含对象o。

public int indexOf(Object o)

返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。方法内部是通过equals方法来判断两个元素值是否相等的。因此一般来说如果存入数据是对象,那么需要重写equals方法。

/**
 * 如果此列表包含指定元素,则返回 true。
 *
 * @param o 比较的元素
 * @return true 存在 false 不存在
 */
public boolean contains(Object o) {
    //内部直接调用indexOf的方法来判断的
    return indexOf(o) != -1;
}

/**
 * 返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。
 *
 * @param o 查找的元素
 * @return 出现的索引,-1表示不存在
 */
public int indexOf(Object o) {
    int index = 0;
    // 从前向后顺序遍历查找链表,返回"值为对象(o)的节点对应的索引"  不存在就返回-1
    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) {
            //可以看到LinkedList是通过元素值的equals方法来比较是否相等的
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

3.8 clone方法

和大多数集合类一样,LinkedList的克隆方法并不会对数据进行克隆,算是浅克隆。

/**
 * 浅克隆(元素不会被克隆)
 * @return 一个副本
 */
public Object clone() {
    //调用父类(Object)的克隆方法
    LinkedList<E> clone = superClone();

    //清空克隆对象的内部变量引用值
    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;

    // 重新赋值
    // 深克隆Node,这时新旧对象的Node是不一样了,但是对应索引的Node中的Item却还是浅克隆,指向了同一个对象,改变对象的值会对两个链表都产生影响!
    // 但是如果储存的是直接量则不会影响!
    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);
    //返回被克隆对象
    return clone;
}

@SuppressWarnings("unchecked")
private LinkedList<E> superClone() {
    try {
        //Object的clone方法
        return (LinkedList<E>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
}

3.9 clear方法

清空链表,并不是简单的将外部引用置空,而是循环整个链表,将所有节点之间的关联一一解除。

/**
 * 清空链表
 */
public void clear() {
    //手动清理全部链表节点之间的引用关系,帮助GC
    for (LinkedList.Node<E> x = first; x != null; ) {
        LinkedList.Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    //最后将外部的头\尾节点引用置空
    first = last = null;
    //size置0
    size = 0;
    //结构改变的次数自增1
    modCount++;
}

4 迭代器机制

凡是List体系的集合都具有Iterator 和ListIterator 迭代器,他们的方法都是一样的,只是具体的实现是由这些实现类自己来实现的!

Iterator 和ListIterator 迭代器的方法和ArrayList的迭代器的实现,包括快速失败和安全失败以及“并发修改”异常机制等都在这篇文章:Java的ArrayList集合源码深度解析以及应用介绍中有讲解,下面遇到相同原理的时候不会赘述!

下面来看看LinkedList集合的迭代器的实现,直觉告诉我们,这两个集合的实现是有区别的!

4.1 Iterator迭代器

List体系的集合的迭代器提供的方法都是一样的,只是提供了不同的实现,下面是LinkedList的Iterator迭代器的一个正向遍历-删除使用案例。

所谓的正向遍历就是从头到尾,那么反向遍历就是从尾到头了,不过反向遍历是ListIterator迭代器的功能,后面会讲!

@Test
public void test2() {
    LinkedList<Integer> linkedList = new LinkedList<>(Arrays.asList(1, 2, 3));
    System.out.println("移除元素前: " + linkedList);
    //获取Iterator迭代器实例
    Iterator<Integer> iterator = linkedList.iterator();
    //循环处理数据
    while (iterator.hasNext()) {
        //获取下一个元素
        Integer next = iterator.next();
        System.out.println(next);
        //移除下一个元素
        iterator.remove();
    }
    System.out.println("移除元素后: " + linkedList);
}

4.1.1 Iterator源码介绍

下面来看Iterator迭代器在LinkedList中的实现!

首先我们跟进iterator(),该方法用于获取迭代器,但是我们在LinkedList的实现类中并没有找到iterator()方法,此时我们知道,那肯定调用的方法在父类中被定义了,并且子类没有重写该方法。

然后进入LinkedList的父类AbstractSequentialList,在父类中,我们果然找到了iterator()方法的源码

/**
 * 父类AbstractSequentialList中的iterator()方法
 * @return 返回迭代器实例
 */
public Iterator<E> iterator() {
    //竟然是返回的一个listIterator对象实例
    return listIterator();
}

我们能够看到,所调用iterator()方法,竟然是返回的一个listIterator的实例(调用的listIterator()方法),但这是可以成立的,因为ListIterator接口继承了Iterator接口,Java中返回的子类对象可以使用父类来接收!

接下来进入listIterator()方法,我们发现竟然来到了AbstractList类中,原来,LinkedList的父类AbstractSequentialList也没有重写listIterator()方法,该方法出现在AbstractSequentialList的父类——AbstractList类中,竟然这么复杂,但是我们还是接着看吧,我们查看AbstractList类的listIterator()实现:

/**
 * 父类AbstractSequentialList的父类AbstractList中的listIterator()方法
 * @return 返回一个列表迭代器
 */
public ListIterator<E> listIterator() {
    //内部调用listIterator(0)方法,接受一个参数0
    return listIterator(0);
}

内部又调用了另外一个带参的listIterator(0)方法,那么这个方法又在哪里呢?我们发现LinkedList已经重写了这个带参数的方法,那么这一次肯定是调用的自己的重写的方法了,看看它的实现:

/**
 * LinkedList重写的带参数的方法
 * @param index 实际上表示从列表迭代器返回的第一个元素的索引,即开始迭代的元素的索引
 * @return 一个列表迭代器实例
 */
public ListIterator<E> listIterator(int index) {
    /*检查索引是否越界*/
    checkPositionIndex(index);
    /*返回ListItr实例,该实例就是一个Iterator对象*/
    return new ListItr(index);
}

/**
 * 检查索引是否越界
 * @param index
 */
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 检查索引是否大于等于0并且小于等于size
 * @param index 索引
 * @return true-是 false-否
 */
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

在进行索引范围检查之后,终于我们看到了返回的实例,一个ListItr的对象。然后我们在LinkedList类中找到了ListItr内部类的实现,我们来看看比较常用的方法:

/**
 * 内部类ListItr实现了ListIterator接口,因此除了具有Iterator接口的方法之外还具有更多的方法。
 * 例如add(),set()这里先不介绍,先来看看Iterator接口的正向迭代方法
 */
private class ListItr implements ListIterator<E> {
    /**
     * 最后被返回的节点
     */
    private Node<E> lastReturned;
    /**
     * 下一个被返回的节点
     */
    private Node<E> next;
    /**
     * 下一个被返回的节点的索引
     */
    private int nextIndex;
    /**
     * 预期结构修改计数 expectedModCount = modCount,用于检测"并发修改"异常
     */
    private int expectedModCount = modCount;

    /**
     * 构造器
     * @param index 表示从列表迭代器返回的第一个元素的索引,即开始迭代的元素的索引
     */
    ListItr(int index) {
        //获取下一个要被返回的节点,如果index == size 那说明下一个将要被返回的节点是null(因为索引是从0开始计数的);
        //否则便是使用node(index)方法查找下一个被返回的节点
        next = (index == size) ? null : node(index);
        //获取下一个要被返回的节点的索引,让其等于index
        nextIndex = index;
    }

    /**
     * 是否具有下一个节点
     * @return  true-有 false-无
     */
    public boolean hasNext() {
        //如果下一个要被返回的节点的索引小于size,那么就返回true,表示还存在没有迭代的节点
        // 否则返回false,表示已经迭代完毕
        return nextIndex < size;
    }

    /**
     * 获取下一个节点
     * @return 下一个节点的值
     */
    public E next() {
        /*检测"并发修改"*/
        checkForComodification();
        /*检测是否还存在可以获取的节点*/
        if (!hasNext())
            throw new NoSuchElementException();
        //设置最后被返回的节点指向next节点
        lastReturned = next;
        //next节点指向next节点的下一个节点
        next = next.next;
        //下一个被返回的节点的索引自增1
        nextIndex++;
        //返回最后被返回的节点的值
        return lastReturned.item;
    }

    /**
     * 移除下一个节点
     */
    public void remove() {
        /*检测"并发修改"*/
        checkForComodification();
        /*检测最后被返回的节点是否为null,当创建了迭代器对象直接使用该方法时,lastReturned就指向null
         * 因此不能先于next()方法使用,也不能连续使用remove()方法*/
        if (lastReturned == null)
            throw new IllegalStateException();
        /*获取最后被返回的节点的下一个节点*/
        Node<E> lastNext = lastReturned.next;
        /*移除lastReturned节点和链表之间的引用关系,后续的节点链接到原节点的位置*/
        unlink(lastReturned);
        /*如果下一个节点等于最后被返回的节点*/
        if (next == lastReturned)
            //那么下一个节点的引用指向最后被返回的节点的下一个节点,实际上这是为倒序迭代遍历服务的(previous),正序遍历使用不到
            next = lastNext;
        else
            //否则下一个被返回的节点的索引自减1,实际上这是为正序迭代遍历服务的(next),倒序遍历使用不到
            nextIndex--;
        //将最后被返回的节点的引用指向null
        lastReturned = null;
        //由于调用了外部类的unlink方法,因此modCount肯定自增了1,为了保证不出现"ConcurrentModificationException"异常,最后将expectedModCount也自增1
        expectedModCount++;
    }

    /**
     * 检测"并发修改",如果出现则抛出ConcurrentModificationException异常
     */
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

4.1.2 Iterator实例解析

下面结合源码以及上面的使用案例来介绍LinkedList中的Iterator迭代器的正向遍历-删除是如何工作的。

  1. 准备工作
    1. 首先创建了一个链表,它的元素节点是1——2——3。
    2. 然后创建一个Iterator迭代器实例,实际上是返回的ListItr对象,该对象直接实现了ListIterator迭代器,但是由于ListIterator迭代器继承了Iterator迭代器,由于Java的向上转型,因此该返回是合理的,只是我们只能使用Iterator迭代器中的方法而已。
    3. 根据源码,new ListItr(index)中index默认传入的是0,那么在ListItr对象的构造器中,next=1节点,nextIndex=0,此时lastReturned=null,expectedModCount被初始化等于modCount。
  2. 然后开始第一轮循环
    1. 首先是hasNext()方法,明显nextIndex=0<size=3是成立的,因此进入循环体中。
    2. 接下来是next()方法,该方法获取下一个元素,首先是一系列“并发修改”检查,关于并发修改检查(快速失败or安全失败),在ArrayList部分有详细讲解,这里不多赘述。明显这里是可以通过检查的。然后lastReturned = next=1节点,next= next.next=2节点,nextIndex++之后变成1。然后返回lastReturned的值,即返回1。
    3. 这里打断一下,如果后续没有remove方法,即循环中只有next()遍历的方法,此时进入第二轮循环,nextIndex=1<size=3,然后继续执行next()方法,这一次lastReturned = next=2节点,next= next.next=3节点,nextIndex++之后变成2。然后返回lastReturned的值,即返回2。然后继续循环,直到3节点遍历完毕,此时nextIndex=3,不满足小于size的条件,此时循环迭代遍历结束!因此正向循环遍历的原理还是很简单,他维护了一个nextIndex遍历一次就自增一次,当等于size时,表示集合迭代遍历完毕!
    4. 现在回来,我们接着案例讲,接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的next方法中lastReturned=1节点,因此不为null。然后获取lastNext=lastReturned.next=2节点,然后调用unlink方法解除lastReturned指向的1节点与链表的关系,此时链表节点变成了2——3。然后判断next是否等于lastReturned,这里说明一下,如果是倒序遍历,那么就会返回true,正序遍历就会返回false,我们本次的遍历明显是正序遍历,因此肯定返回false,然后将nextIndex–之后又变成了0,最后expectedModCount++,因为unlink方法中modCount会自增1,为了防止出现ConcurrentModificationException异常,因此这里expectedModCount也而要自增1。
    5. 到此第一轮循环完毕,循环完毕之后我们看到next返回1,即0索引处的1节点的值,remove则是把1节点移除链表,最后nextIndex=0,next=2节点,lastReturned=null,expectedModCount还是等于modCount。
  3. 然后开始第二轮循环
    1. 首先是hasNext()方法,注意,由于上一轮循环中的unlink操作,会导致size减去1,这里size=2,但是明显nextIndex=0<size=2还是成立的,因此进入循环体中。
    2. 接下来是next()方法,该方法获取下一个元素,首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后lastReturned = next=2节点,next= next.next=3节点,nextIndex++之后变成1。然后返回lastReturned的值,即返回2。
    3. 接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的next方法中lastReturned=2节点,因此不为null。然后获取lastNext=lastReturned.next=3节点,然后调用unlink方法解除lastReturned指向的2节点与链表的关系,此时链表节点变成了3。然后判断next是否等于lastReturned,返回false,然后将nextIndex–之后又变成了0,最后expectedModCount++,让expectedModCount和modCount始终相等。
    4. 到此第二轮循环完毕,循环完毕之后我们看到next返回2,即0索引处的2节点的值,remove则是把2节点移除链表,最后nextIndex=0,next=3节点,lastReturned=null,expectedModCount还是等于modCount。
  4. 然后开始第三轮循环
    1. 首先是hasNext()方法,这里size=1,但是明显nextIndex=0<size=1还是成立的,因此进入循环体中。
    2. 接下来是next()方法,该方法获取下一个元素,首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后lastReturned = next=3节点,next= next.next=null,nextIndex++之后变成1。然后返回lastReturned的值,即返回3。
    3. 接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的next方法中lastReturned=3节点,因此不为null。然后获取lastNext=lastReturned.next=null,然后调用unlink方法解除lastReturned指向的3节点与链表的关系,此时链表没有了节点。然后判断next是否等于lastReturned,返回false,然后将nextIndex–之后又变成了0,最后expectedModCount++,让expectedModCount和modCount始终相等。
    4. 到此第三轮循环完毕,循环完毕之后我们看到next返回3,即0索引处的3节点的值,remove则是把3节点移除链表,最后nextIndex=0,next=null,lastReturned=null,expectedModCount还是等于modCount。
  5. 然后开始第四轮循环
    1. 首先是hasNext()方法,这里size=0,明显nextIndex=0<size=0不成立了,因此循环结束,此时链表节点已经被遍历、删除完毕,再次打印链表,只会返回“[]”。

总结:我们看到,LinkedList的Iterator迭代器的正序遍历-删除思想和ArrayList的迭代器的遍历-删除思想是一致的,即始终定位到链表头部,遍历一个删除一个,后续的节点自动成为头节点,而循环的条件始终是nextIndex=0和size的比较,删除一次size减少1,当size等于0时,自然循环结束,遍历-删除的操作结束,可以看到它的原理还是比较简单的!

4.2 ListIterator迭代器

ListIterator继承了Iterator迭代器,功能更加强大!可以实现反向遍历,add、set等操作。下面是LinkedList的ListIterator迭代器的一个反向遍历-删除使用案例。

/**
 * ListIterator迭代器的反向遍历+删除案例
 */
@Test
public void test3() {
    LinkedList<Integer> linkedList = new LinkedList<>(Arrays.asList(1, 2, 3));
    System.out.println("移除元素前: " + linkedList);
    //获取ListIterator迭代器实例
    ListIterator<Integer> iterator = linkedList.listIterator(3);
    //反向遍历 处理数据
    while (iterator.hasPrevious()) {
        //获取下一个元素
        Integer next = iterator.previous();
        System.out.println(next);
        //移除下一个元素
        iterator.remove();
    }
    System.out.println("移除元素后: " + linkedList);
}

4.2.1 ListIterator源码介绍

下面来看ListIterator迭代器在LinkedList中的实现!

首先我们跟进listIterator(),该方法用于获取迭代器,但是我们在LinkedList的实现类中并没有找到listIterator()方法,此时我们知道,那肯定调用的方法在父类中被定义了,并且子类没有重写该方法。

然后在LinkedList的父类AbstractSequentialList中,我们也没有找到listIterator()方法,继续向上查找,在祖父类AbstractList中,我们果然找到了listIterator()方法的源码

public ListIterator<E> listIterator() {
    return listIterator(0);
}

是不是觉得似曾相似,没错,上面讲的iterator()方法也是间接的调用了这个方法,没想到吧!

实际上,LinkedList的iterator()和listIterator()返回的都是ListItr内部类对象,不同之处只是前面的静态类型不一样,简化来写就是这样的:

Iterator iterator=new ListItr(0);   
ListIterator listIterator=new ListItr(0);

那这么说来上面的iterator实际上已经拥有了原本属于listIterator的方法?

在我们看来的确是这样的,但是根据Java方法的调用规则,只有左边静态类型拥有的方法才能被调用,上面的iterator虽然实际类型是ListItr,但是由于静态类型属于Iterator类型,因此还是只能调用Iterator迭代器的方法,而下面的listIterator由于是ListIterator类型,则可以调用全部的方法!

方法调用这里涉及到了JVM层面,不了解Java方法调用规则的可以看看这篇文章:Java的JVM运行时栈结构和方法调用详解

由于ListIterator迭代器增加了很多方法,我就以上面的反向遍历-删除为例子来讲解关键方法的源码:

/**
 * 内部类ListItr实现了ListIterator接口,因此除了具有Iterator接口的方法之外还具有更多的方法。
 * 例如add(),set()这里先不介绍,先来看看Iterator接口的正向迭代方法
 */
private class ListItr implements ListIterator<E> {
    /**
     * 最后被返回的节点
     */
    private Node<E> lastReturned;
    /**
     * 下一个被返回的节点
     */
    private Node<E> next;
    /**
     * 下一个被返回的节点的索引
     */
    private int nextIndex;
    /**
     * 预期结构修改计数 expectedModCount = modCount,用于检测"并发修改"异常
     */
    private int expectedModCount = modCount;

    /**
     * 构造器
     * @param index 表示从列表迭代器返回的第一个元素的索引,即开始迭代的元素的索引
     */
    ListItr(int index) {
        //获取下一个要被返回的节点,如果index == size 那说明下一个将要被返回的节点是null(因为索引是从0开始计数的);
        //否则便是使用node(index)方法查找下一个被返回的节点
        next = (index == size) ? null : node(index);
        //获取下一个要被返回的节点的索引,让其等于index
        nextIndex = index;
    }

    /**
     * 是否具有下一个节点
     * @return  true-有 false-无
     */
    public boolean hasNext() {
        //如果下一个要被返回的节点的索引小于size,那么就返回true,表示还存在没有迭代的节点
        // 否则返回false,表示已经迭代完毕
        return nextIndex < size;
    }

    /**
     * 获取下一个节点
     * @return 下一个节点的值
     */
    public E next() {
        /*检测"并发修改"*/
        checkForComodification();
        /*检测是否还存在可以获取的节点*/
        if (!hasNext())
            throw new NoSuchElementException();
        //设置最后被返回的节点指向next节点
        lastReturned = next;
        //next节点指向next节点的下一个节点
        next = next.next;
        //下一个被返回的节点的索引自增1
        nextIndex++;
        //返回最后被返回的节点的值
        return lastReturned.item;
    }

    /**
     * 移除下一个节点
     */
    public void remove() {
        /*检测"并发修改"*/
        checkForComodification();
        /*检测最后被返回的节点是否为null,当创建了迭代器对象直接使用该方法时,lastReturned就指向null
         * 因此不能先于next()方法使用,也不能连续使用remove()方法*/
        if (lastReturned == null)
            throw new IllegalStateException();
        /*获取最后被返回的节点的下一个节点*/
        Node<E> lastNext = lastReturned.next;
        /*移除lastReturned节点和链表之间的引用关系,后续的节点链接到原节点的位置*/
        unlink(lastReturned);
        /*如果下一个节点等于最后被返回的节点*/
        if (next == lastReturned)
            //那么下一个节点的引用指向最后被返回的节点的下一个节点,实际上这是为倒序迭代遍历服务的(previous),正序遍历使用不到
            next = lastNext;
        else
            //否则下一个被返回的节点的索引自减1,实际上这是为正序迭代遍历服务的(next),倒序遍历使用不到
            nextIndex--;
        //将最后被返回的节点的引用指向null
        lastReturned = null;
        //由于调用了外部类的unlink方法,因此modCount肯定自增了1,为了保证不出现"ConcurrentModificationException"异常,最后将expectedModCount也自增1
        expectedModCount++;
    }

    /**
     * 检测"并发修改",如果出现则抛出ConcurrentModificationException异常
     */
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

4.2.2 ListIterator实例解析

下面结合源码以及上面的使用案例来介绍LinkedList中的ListItertor迭代器的反向遍历-删除是如何工作的。

  1. 准备工作
    1. 首先创建了一个链表,它的元素节点是1——2——3。
    2. 然后创建一个ListItertor迭代器实例,实际上是返回的ListItr对象。根据源码,new ListItr(index)中index在本案例中指定传入的是3,那么在ListItr对象的构造器中,由于index和size都等于3,因此next=null,nextIndex=3,此时lastReturned=null,expectedModCount被初始化等于modCount。
      然后开始循环,首先是hasPrevious ()方法,明显nextIndex=3>0是成立的,因此进入循环体中。
  2. 第一轮循环
    1. 接下来是previous()方法,该方法获取上一个元素(因为是倒序遍历,即重尾到头),首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后由于next等于null,那么设置lastReturned = next = last节点,这里的last节点就是3节点。nextIndex–之后变成2。然后返回lastReturned的值,即返回3。
    2. 这里打断一下,如果后续没有remove方法,即循环中只有previous()遍历的方法,此时进入第二轮循环,nextIndex=2>0,然后继续执行previous()方法,这一次由于next不等于nulll,那么设置lastReturned = next = next.prev节点,这里的next.prev节点就是2节点。nextIndex–之后变成1。然后返回lastReturned的值,即返回2。然后继续循环,直到1节点遍历完毕,此时nextIndex=0,不满足大于0的条件,此时循环迭代遍历结束!因此反向循环遍历的原理也还是很简单,它维护了一个nextIndex遍历一次就自减一次,当等于0时,表示集合反向迭代遍历完毕!
    3. 现在回来,我们接着案例讲,接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的previous方法中lastReturned=3节点,因此不为null。然后获取lastNext=lastReturned.next=null,然后调用unlink方法解除lastReturned指向的3节点与链表的关系,此时链表节点变成了1——2。然后判断next是否等于lastReturned,这里说明一下,如果是倒序遍历,那么就会返回true,正序遍历就会返回false,我们本次的遍历明显是倒序遍历,因此肯定返回true,然后next = lastNext=null,然后lastReturned = null,最后expectedModCount++,因为unlink方法中modCount会自增1,为了防止出现ConcurrentModificationException异常,因此这里expectedModCount也而要自增1。
    4. 到此第一轮循环完毕,循环完毕之后我们看到next返回3,即2索引处的3节点的值,remove则是把3节点移除链表,最后nextIndex=2,next=null,lastReturned=null,expectedModCount还是等于modCount。
  3. 然后开始第二轮循环
    1. 首先是hasNext()方法,显nextIndex=2>0还是成立的,因此进入循环体中。
    2. 接下来是previous()方法,该方法获取上一个元素(因为是倒序遍历,即重尾到头),首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后由于next等于null,那么设置lastReturned = next = last节点,由于last节点3被删除了,这里的last节点就是2节点。nextIndex–之后变成1。然后返回lastReturned的值,即返回2。
    3. 接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的previous方法中lastReturned=2节点,因此不为null。然后获取lastNext=lastReturned.next=null,然后调用unlink方法解除lastReturned指向的2节点与链表的关系,此时链表节点变成了1。然后判断next是否等于lastReturned,肯定返回true,然后next = lastNext=null,然后lastReturned = null,最后expectedModCount++,让expectedModCount和modCount始终相等。
    4. 到此第二轮循环完毕,循环完毕之后我们看到next返回2,即1索引处的2节点的值,remove则是把2节点移除链表,最后nextIndex=1,next=null,lastReturned=null,expectedModCount还是等于modCount。
  4. 然后开始第三轮循环
    1. 首先是hasNext()方法,显nextIndex=1>0还是成立的,因此进入循环体中。
    2. 接下来是previous()方法,该方法获取上一个元素(因为是倒序遍历,即重尾到头),首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后由于next等于null,那么设置lastReturned = next = last节点,由于last节点2被删除了,这里的last节点就是1节点。nextIndex–之后变成0。然后返回lastReturned的值,即返回1。
    3. 接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的previous方法中lastReturned=1节点,因此不为null。然后获取lastNext=lastReturned.next=null,然后调用unlink方法解除lastReturned指向的1节点与链表的关系,此时链表没有了节点。然后判断next是否等于lastReturned,肯定返回true,然后next = lastNext=null,然后lastReturned = null,最后expectedModCount++,让expectedModCount和modCount始终相等。
    4. 到此第三轮循环完毕,循环完毕之后我们看到next返回1,即0索引处的1节点的值,remove则是把1节点移除链表,最后nextIndex=0,next=null,lastReturned=null,expectedModCount还是等于modCount。
  5. 然后开始第四轮循环
    1. 首先是hasNext()方法,这里nextIndex=0,明显nextIndex=0>0不成立了,因此循环结束,此时链表节点已经被倒序遍历、删除完毕,再次打印链表,只会返回“[]”。

总结:我们看到,LinkedList的ListIterator迭代器的反序遍历-删除思想和ArrayList的迭代器的遍历-删除思想是一致的,即始终定位到链表头部,遍历一个删除一个,后续的节点自动成为头节点,而循环的条件始终是nextIndex=0和size的比较,删除一次size减少1,当size等于0时,自然循环结束,遍历-删除的操作结束,可以看到它的原理还是比较简单的!

5 总结

LinkedList是基于链表的线性表Java实现,它的原理还是比较简单的,实际上线性表的原理都不怎么难。

LinkedList是基于链表实现的双端队列,在JDK1.6的时候,添加了集合类ArrayDeque,该类同样实现了Deque接口,即也作为一个“双端队列”,可以被用来实现Stack(栈)或者Queue(队列)。

ArrayDeque内部是采用可变数组实现的双端队列,采用两个外部引用来保持队列头结点和尾节点的访问,同时删除队列头尾元素时不会移动其他元素,而是移动引用的位置,即形成一个环形队列,能够复用数组空间(允许队头索引比队尾索引值更大)。相比于使用链表实现的双端队列LinkedList综合效率更好!同时如果用于实现栈,那么相比于Stack的综合效率同样更好!但是ArrayDeque不支持null元素!

我们后续将会介绍的更多集合,比如Stack、TreeMap、HashMap,LinkedHashMap、HashSet、TreeSet等基本集合以及JUC包中的高级并发集合。如果想学习集合源码的关注我的专栏更新!

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

  • 3
    点赞
  • 7
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:博客之星2020 设计师:CSDN官方博客 返回首页
评论

打赏作者

刘Java

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值