原创

Java集合框架——LinkedList

本文基于JDK1.8,代码中方法头的注释都是对照源码翻译过来的
自顶向下阅读

类头

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    ...
    }
  • LinkedList继承了AbstractSequentialList抽象类,是一个双向链表,可以被当作堆栈、队列或双端队列进行操作。在AbstractSequentialList封装好了一些方法和抽象方法,当继承此抽象类时,只需要重写其中的抽象方法,而不需要重写全部方法
  • 实现List接口(提供List接口中所有方法的实现)
  • 实现了Deque接口,实现了Deque所有的可选的操作
    实现Cloneable接口,支持克隆,可以调用clone()进行拷贝
  • 实现java.io.Serializable接口,支持序列化

核心

LinkedList内部定义了一个Node类,Node是双向的,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;
    }
}
  • E item:值域,节点的值
  • Node next:next域,当前节点的后一个节点的引用(可以理解为指向当前节点的后一个节点的指针)
  • Node prev:prev域,当前节点的前一个节点的引用(可以理解为指向当前节点的前一个节点的指针)

成员变量

//记录LinkedList的大小
transient int size = 0;

//指向头节点
transient Node<E> first;

//指向尾结点
transient Node<E> last;
  • 使用transient关键字,避免序列化

构造方法

1.

/**
 * 默认构造器,创建一个空的列表
 */
public LinkedList() {
}

2.

/**
 * 按照集合的迭代器返回的顺序,构造包含指定集合的元素的列表
 *
 * @param  c 将其元素放入列表中的集合
 * @throws NullPointerException 如果指定集合为空抛出异常
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
  • 首先调用一下空的构造器创建一个链表,然后在此构造器中调用了addAll(Collection<? extends E> c)方法,后面插入会讲到

工具方法

这些方法都是private私有或protected的,不提供给用户使用,只用作LinkedList公有方法功能的实现
下面这些方法都是头节点、尾结点成对的,可以对照看,只要弄懂一个,其他的理解很快

1. 把元素作为链表的第一个元素

注意:只有在链表首部和尾部插入才有头节点和尾结点的变化,如果在链表中插入则没有

/**
 * 链接e作为第一个元素
 */
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++;
}
  • 由于将指定元素作为头节点,所以需要将原来的头节点保存下来,将头节点赋给一个新节点f,注意这里是等号直接赋值,赋的是地址,f的改变会直接作用到头节点first,头节点也会改变,在LinkedList中有很多这种赋值操作,一定要搞清楚,不然就会有f的改变为什么会改变整个LinkedList的结构等疑惑
  • 承接上文,再创建一个新节点newNode,将其值域设为参数e,next域指向f,然后将自身设为头节点
  • 进行判断,如果f为空,说明头节点为空,整个链表为空,已经将头节点设为newNode,只需要将新节点设为尾结点即可;若不为空,此时newNode已经是头节点,且next(下一个节点地址)指向f(正数第二个节点),但LinkList是双向链表,所以下一个节点的prev(前一个节点地址)域需要指向头节点
    然后链表大小加1,修改次数加1

2. 把元素作为链表的最后一个元素

/**
 * 链接e作为最后一个元素
 */
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++;
}
  • 和linkFirst(E e)方法原理类似
  • 先保存尾结点,在构造一个新的节点,prev域指向尾结点,再将新节点newNode作为尾结点
  • 再进行判断,l为空,空链表,头节点first也指向新节点;不为空,l节点(也就是倒数第二个节点)next域指向尾结点,将链表链接起来即可

3. 删除链表第一个节点(节点不为空)

/**
 * 解开非空第一节点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;
}
  • 定义一个element保存f节点值域的值,定义一个新节点next指向f节点的下一节点,将f的item域和next域设为空,由于删除了f,f的下一节点就变为了头节点,所以first = next
  • 再进行判断,若next为空,空链表,尾结点last设为null;若不为空,头节点prev域设为空即可(头节点没有前一个节点)
  • 最后返回删除的节点的值域element
  • 总结:f节点item、next域为空,头节点prev域为空

4. 删除链表最后一个节点(节点不为空)

/**
 * 解开非空最后节点l
 */
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}
  • 定义element保存l节点值域的值,定义一个新节点prev指向l节点的前一个节点,将l的item域和prev域设为空,由于删除了最后一个节点l,l的前一个节点就变为尾结点,所以last = prev
  • 再判断,空链表,头节点为空;不为空,尾结点的next域设为空(尾结点没有下一个节点)
  • 最后返回删除的节点的值域element
  • 总结:l节点item、prev为空,尾节点next域为空

5. 在非空节点succ之前插入元素e

/**
 * 在非空节点succ之前插入元素e
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}
  • 先定义一个新节点pred将succ的前一个节点保存下来,在succ节点之前插入,插入后,待插入元素e节点位于succ节点和succ前一个节点pred之间,所以构造一个待插入新节点newNode,前一个节点为pred,下一个节点为succ
  • 此时只是newNode节点指向前一个节点pred和下一个节点succ,但LinkedList为双向链表,需要下一个节点前驱(prev域)指向newNode,所以succ.prev = newNode
  • 再判断pred是否为空,为空,新节点设为头节点;不为空,前一个节点pred的后驱(next域)指向新节点,这样,就成功插入元素了

6. 删除指定节点(节点非空)

/**
 * 解开非空节点x
 */
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;
}
  • 前面的删除方法要么是删除第一个节点,要么是删除最后一个节点,而此方法是删除指定结点,实现方式略有区别
  • 首先,和前面类似,删除某一节点,需要将指定节点的前一节点和下一节点保存下来,代码中将节点的值也保存下来是作为方法的返回值
  • 然后判断前一节点prev是否为空,为空,则x为头节点,删除之后,x下一节点next为头节点,所以first = next;不为空,将prev的后驱(next域)指向x下一节点next,这里链接第一部分(前一节点指向后一节点)
  • 再判断后一节点next是否为空,为空,x为尾结点,删除之后,x前一节点prev为尾结点,所以last = prev;不为空,将next前驱(prev域)指向x前一节点prev,这里链接第二部分(后一节点指向前一节点)
  • 最后将item值域设为空,链表大小减一,modCount加1,返回删除节点的值
    由于LinkedList是双向的,需要双向操作,所以在方法中通过两个if判断来分别进行链表的链接操作,这里可以自己画图理解
  • 最后返回删除的节点的值域element

7. 得到指定位置节点

/**
 * 返回指定元素索引处的(非空)节点
 */
Node<E> node(int index) {
    // assert isElementIndex(index);

    /*
    判断index位于链表的前半部分还是后半部分,从而判断从哪个方向遍历
    用循环方式遍历到index那个位置,得到该位置的元素并返回
    LinkedList获取元素采用的是遍历链表的方式,虽然最多只会循环列表大小的一半,但性能也比较低的
    */
    if (index < (size >> 1)) { //size >> 1 等同于size/2
        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;
    }
}

8. 索引检查

方法内容简单明了,看注释很容易理解

/**
 * 说明参数是否为现有元素的索引
 */
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

/**
 * 说明参数是否是迭代器或添加操作的有效位置的索引
 */
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}
  • 注意isElementIndex()和isPositionIndex()的不同,isPositionIndex()是判断迭代器或添加操作的位置是否有效,所以多了个等于的判断,若index=size,则是在链表末尾插入
  • isElementIndex(int index)是链表元素索引的检查,所以链表的索引一定是小于size元素个数的
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

9. 异常信息提示

/**
 * 构造IndexOutOfBoundsException异常详细信息
 * 在错误处理代码的许多可能的重构中,这个“大纲”对服务器和客户端VM都是最好的
 */
private String outOfBoundsMsg(int index) {
    return "Index: "+index+", Size: "+size;
}

到这里大部分的工具方法就讲完了,接下来的公有方法大多是调用上面的方法来实现相关功能的

公有方法

因为有些方法具体分析在上面有提到,接下来已经讲过的方法就不一一赘述了,结合注释很容易明白

1. 插入

1.1 addAll(Collection<? extends E> c):boolean

/**
 * 按照指定集合的迭代器返回的顺序,将指定集合中的所有元素追加到该列表的末尾。如果操作过程中指定的集合被修改,则此操作的行为未定义(注意,如果指定的集合是这个列表,则它将是非空的。)
 *
 * @param c 包含要添加到此列表的元素的集合
 * @return {@code true} 如果此列表由于调用而更改
 * @throws NullPointerException 如果指定集合为空
 */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

/**
 * 将指定集合中的所有元素插入到该列表中,从指定位置开始。将当前在该位置的元素(如果有的话)和任何后续元素向右移动(增加它们的索引)。新元素将以指定集合的迭代器返回的顺序出现在列表中。
 *
 * @param index 从指定集合插入第一个元素的索引
 * @param c 包含要添加到此列表的元素的集合
 * @return {@code true} 如果此列表由于调用而更改
 * @throws IndexOutOfBoundsException
 * @throws NullPointerException 如果指定集合为空
 */
public boolean addAll(int index, Collection<? extends E> c) {
    //先进行边界检查
    checkPositionIndex(index);

    //将集合c转化为数组,判断数组长度是否符合要求
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    //定义了两个节点,pred(待添加节点的前一个节点)、succ(待添加位置index的节点)
    Node<E> pred, succ;

    //构造器调用进入if代码块中
    //if判断,如果index == size,说明将元素插入到链表末尾(构造器如此,链表为空),succ没有意义,设为空,待插入节点pred设为尾结点;否则插入位置位于链表中,通过方法E node(int index)得到索引位置的节点,使pred指向succ的前一个节点
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }

    //通过for循环遍历数组,每次遍历都新建一个节点,节点中存储数组元素的值,使该节点的前驱(prev域)指向pred节点,进行单向链接,再进行if判断,如果pred为空,则为空链表,头节点指向新节点;若不为空,pred节点后驱(next域)指向新节点,对应new Node<>(pred, e, null)进行链表双向链接
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }

    //再进行succ判断,经过前面的if判断,succ只有为null和node(index)两种情况。为空,尾结点last指向pred;不为空,pred后驱(next域)指向succ,succ的前驱指向pred,就将集合元素成功插入指定位置了
    //注意:只有在链表首部和尾部元素的改变才有头节点和尾结点的变化,如果在链表中插入则没有
    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

    //最后链表大小加1,modCount加1,返回修改结果true
    size += numNew;
    modCount++;
    return true;
}
  • 在构造器中就调用了addAll()方法
  • 可以看到,addAll(int index, Collection<? extends E> c)方法才是构造器的底层实现
    方法内容有点多,将分析拆分开来写在方法注释中

1.2 add():boolean

/**
 * 将指定的元素追加到列表的末尾
 *
 * <p>该方法相当于addLast
 *
 * @param e 要追加到这个列表的元素
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

2. 删除指定元素(且只删除第一次出现的指定的元素),如果指定的元素在集合中不存在,则返回false,否则返回true)

/**
 * 如果存在,则从该列表中移除指定元素的第一次出现。如果该列表不包含元素,则其不变。更正式地,移除具有最低索引的元素
 * 
 * 如果这个列表包含指定的元素(或者等效地,如果这个列表由于调用而改变),则返回true
 *
 * @param o 要从该列表中移除的元素,如果存在
 * @return {@code true} 如果此列表包含指定元素
 */
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;
}

3. 得到第一个、最后一个元素

/**
 * 返回此列表中的第一个元素
 *
 * @return 列表中的第一个元素
 * @throws NoSuchElementException 如果这个列表是空的抛出异常
 */
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

/**
 * 返回此列表中的最后一个元素
 *
 * @return 此列表中的最后一个元素
 * @throws NoSuchElementException 如果这个列表是空的抛出异常
 */
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

4. 删除第一个、最后一个元素并返回

/**
 * 从这个列表中移除并返回第一个元素
 *
 * @return 列表中的第一个元素
 * @throws NoSuchElementException 如果这个列表是空的抛出异常
 */
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

/**
 * 从这个列表中移除并返回最后一个元素
 *
 * @return 列表中的最后一个元素
 * @throws NoSuchElementException 如果这个列表是空的抛出异常
 */
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

5. 在开头、末尾插入元素

/**
 * 在这个列表的开头插入指定的元素
 *
 * @param e 要添加的元素
 */
public void addFirst(E e) {
    linkFirst(e);
}

/**
 * 将指定的元素追加到列表的末尾
 *
 * <p>这个方法相当于add
 *
 * @param e 要添加的元素
 */
public void addLast(E e) {
    linkLast(e);
}

6. 清空链表

/**
 * 从该列表中移除所有元素
 * 此调用返回后,列表将为空
 */
public void clear() {
    // 清除节点之间的所有链接是"不必要的",但是:
    // - 如果丢弃的节点驻留一代以上,则有助于生成GC
    // - 即使有一个可达迭代器,也可以释放内存
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    size = 0;
    modCount++;
}
  • 遍历整个LinkedList,把每个节点都置空。最后要把头节点和尾节点设置为空,siz也设置为空,但modCount仍自增

    7. 判断是否包含某一元素

/**
 * 如果这个列表包含指定的元素,则返回true
 *
 * @param o 元素在列表中的存在将被测试
 */
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

/**
 * 返回此列表中指定元素的第一次出现的索引,或如果该列表不包含元素,则为-1
 *
 * @param o 要搜索的元素
 * @return 此列表中指定元素的第一次出现的索引,或如果该列表不包含元素,则为-1
 */
public int indexOf(Object o) {
    int index = 0;
    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) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}
  • contains(Object o)底层调用indexOf(Object o)方法
  • 在indexOf(Object o)方法中,先判断obejct是否为空,分为两种情况:
    然后在每种情况下,从头节点开始遍历LinkedList,判断是否有与object相等的元素,如果有,则返回对应的位置index,如果找不到,则返回-1

8. 获取对应索引节点的值

/**
 * 返回该列表中指定位置的元素
 *
 * @param index 返回元素的索引
 * @return 列表中指定位置的元素
 * @throws IndexOutOfBoundsException
 */
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

9. 转化为数组

9.1 转化为数组(不含参数)

/**
 * 返回包含该列表中所有元素的数组(从第一个元素到最后一个元素)
 *
 * <p>返回的数组将是“安全的”,因为该列表没有维护它(换句话说,这个方法必须分配一个新的数组)。因此,调用方可以修改返回的数组
 *
 * <p>该方法作为基于数组和基于集合的桥梁
 *
 * @return 以适当顺序包含该列表中的所有元素的数组
 */
public Object[] toArray() {
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;
}

9.2 转化为数组(含数组参数)

/**
 * 返回一个数组,该数组按适当的顺序(从第一个元素到最后一个元素)包含该列表中的所有元素;返回的数组的运行时类型是指定数组的运行时类型。如果列表适合于指定的数组,则返回该数组。否则,将使用指定数组的运行时类型和该列表的大小分配新的数组
 *
 * <p>如果列表适合于具有空闲空间的指定数组(即,数组具有比列表更多的元素),则紧跟在列表末尾的数组中的元素被设置为null
 * 如果调用者知道列表不包含任何空元素,则这对于确定列表的长度很有用
 *
 * <p>与toArray()方法一样,该方法在基于数组和基于集合的API之间起到桥梁的作用。此外,该方法允许对输出阵列的运行时类型进行精确控制,并且可以在某些情况下用于节省分配成本
 *
 * <p>假设x是一个已知的仅包含字符串的列表。下面的代码可以用来将列表转储到新分配的{@代码字符串}数组中:
 *
 * String[] y = x.toArray(new String[0]);</pre>
 *
 * 注意,toArray(new Object[0])在函数上与toArray()相同
 *
 * @param a 如果列表足够大,则要存储列表元素的数组;否则,将为此目的分配一个新的相同运行时类型的数组
 * @return 包含列表元素的数组
 * @throws ArrayStoreException 如果指定数组的运行时类型不是该列表中每个元素的运行时类型的超类型
 * @throws NullPointerException 如果指定的数组为空
 */
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
                            a.getClass().getComponentType(), size);
    int i = 0;
    Object[] result = a;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;

    if (a.length > size)
        a[size] = null;

    return a;
}
  • 在此方法中,通过反射创建一个数组,把集合中的所有元素添加到数组中,再将此数组返回

    总结

    由于源码方法太多,就不一一列举了,如果理解了核心方法,举一反三,相信理解其他的方法实现不会有什么问题
正文到此结束