美文网首页Java 杂谈
Java集合源码系列-LinkedList

Java集合源码系列-LinkedList

作者: _kkk | 来源:发表于2019-02-03 21:59 被阅读24次

前言

这是Java集合源码系列的第三篇了,准备来介绍一下LinkedList这个类,LinkedList常与ArrayList放在一起比较,都是List接口的实现类,但是底层的数据结构不同,导致它们在一些操作上各有优缺点,需要根据应用场景灵活选用ListArrayList的细节上篇文章已经介绍过了,不了解的可以回过去看一下。LinkedList的底层实现是一个双向链表,同时也实现了Deque接口,因此也可以作为双向队列的一个实现,这是LinkedList相比ArrayList的另外一个重大特性。话不多说,直接进入源码分析阶段吧,按照惯例,依然是从使用集合的整个生命周期来作为行文路线,即创建-->插入-->获取-->删除。

创建

继承关系

LinkedList

底层数据结构--双向链表

前面介绍到LinkedList的底层数据结构是双向链表,那么先来看一下其实现。

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;
    }
}

非常的简单,与普通的双向链表完全一致,一个存储数据的item,以及两个分别指向前一个节点和后一个节点的指针。

构造函数

transient int size = 0; // number of elements in list
// 类中有两个指针变量,一个指向双向链表的头,一个指向尾
transient Node<E> first;
transient Node<E> last;
// 创建一个空的list
public LinkedList() {
}
// 从一个已有集合创建
public LinkedList(Collection<? extends E> c) {
    this();
// addAll就是将collection中所有元素添加到list中,具体在添加元素部分介绍
    addAll(c);
}

LinkedList的构造函数非常简单,而且成员变量的数目最少,只有3个,那么现在来看下如何添加元素吧。

添加元素

首先介绍下上面构造函数中提到的的addAll方法

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c); // size初始值为0
}
// 从指定Index处插入给定集合中的所有元素
public boolean addAll(int index, Collection<? extends E> c) {
// check index>=0 && index <= size
    checkPositionIndex(index);
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;
    Node<E> pred, succ;
    if (index == size) {
    // index为最后一个元素的后面一个时
        succ = null;
        pred = last;//pred指向List中最后一个节点
    } else {
    // index是中间元素时
    // node方法返回下标为index的节点,将在获取元素部分详细介绍
        succ = node(index);
    // pred指向succ的前一个节点
        pred = succ.prev;
    }
    // 新插入的元素在pred指针之后
    // 遍历数组a,将元素逐个插入,并指定新的first和last指针
    for (Object o : a) {
        E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }
    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }
    // 修改size的数值
   size += numNew;
  // 对于add, delete这样结构性的修改方式,modCount记录修改次数,用于迭代时检测list是否被修改,被修改则抛出异常,但是不保证一定抛出异常。需要用其他方式检测并发操作的正确性
    modCount++;
    return true;
}
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 生成错误信息
private String outOfBoundsMsg(int index) {
    return "Index: "+index+", Size: "+size;
}
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

上面的是addAll方法,应该是不常用的(我猜的)。那么现在来看下LinkedList作为list实现的常用添加方法add

public boolean add(E e) {
    linkLast(e);
    return true;
}
// 在双向链表末尾插入新元素
void linkLast(E e) {
// last指针指向双向链表最后一个元素,l指向last
    final Node<E> l = last;
// 新创建一个节点,其prev指针指向链表末尾
    final Node<E> newNode = new Node<>(l, e, null);
// 更新last指针
    last = newNode;
    if (l == null)
// 如果当前没有元素,插入的元素即为第一个元素,将first指针指向该node
        first = newNode;
    else
// 如果双向链表已经存在,将添加之前的链表的最后一个几点的next指针更新
        l.next = newNode;
// 修改size和modCount
    size++;
    modCount++;
}
// *******************************************************
// 指定下标处插入元素的add方法
public void add(int index, E element) {
// check index>=0 && index<=size
    checkPositionIndex(index);
    if (index == size)
// 等价于末尾插入
        linkLast(element);
    else
// 在链表前端节点插入,node方法返回index处的节点,后面详细介绍
        linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// succ为index所在位置的节点,原链表A->succ->B
// 插入后链表A->newNode->succ->B
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
// 因为是双向链表,succ.prev指针也需要更新
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}
// *******************************************************
// 接下来看看LinkedList作为deque的一些方法
public void addLast(E e) {
    linkLast(e);
}
// 入栈操作
public void push(E e) {
    addFirst(e);
}
public void addFirst(E e) {
    linkFirst(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++;
}

插入操作总的来说是先找到插入的位置,生成一个新的双向链表的Node节点,修改前后节点相应的指针。时间开销只需要一个遍历的开销,相比于ArrayList,ArrayList需要将index之后的元素进行位移,花费的开销更大。

获取元素

// 先介绍下前面出现过几次的node()方法
// node方法返回指定Index处的节点,根据index与first及last的距离,选择较近的一端进行遍历,总的复杂度为O(n),而ArrayList根据index获取元素的时间复杂度为O(1)
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;
    }
}
//根据指定下标获取元素
public E get(int index) {
//check index>=0 && index<size
    checkElementIndex(index);
    return node(index).item;
}
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}
// 作为队列,有getFirst和getLasr两个方法,直接返回内部成员变量first和last指针的值,非空才返回,为空抛异常
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}
// 判断是否包含某个元素
public boolean contains(Object o) {
    return indexOf(o) != -1;
}
// 返回第一个相等对象的index
public int indexOf(Object o) {
    int index = 0;
// 从first节点开始遍历,检验是否存在对象o
    if (o == null) {
// 支持查找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
    return -1;
}
//还有一个lastIndexOf,从后向前查找,实现原理相同

删除元素

//删除指定下标处的元素
public E remove(int index) {
//check index>=0 && index<size
    checkElementIndex(index);
    return unlink(node(index));
}
//删除指定对象,与indexOf的实现雷同
public boolean remove(Object o) {
// 从first向后遍历,支持null对象,找到该对象后调用unlink方法删除,删除成功返回true,否则false
    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;
}
E unlink(Node<E> x) {
    final E element = x.item;
// 获取即将删除节点的前一个节点和后一个节点
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    if (prev == null) {
// 被删除节点为first指向的节点,更新first指针为next
        first = next;
    } else {
// 被删除节点不是first指向的节点,将前一个节点的next指针更新,并把被删除节点的prev节点置空
        prev.next = next;
        x.prev = null;
    }
    if (next == null) {
// 被删除节点为last指向的节点,更新last指针为prev
        last = prev;
    } else {
// 被删除的节点不是last指向的节点,更新被删除节点的后一个节点的prev指针
        next.prev = prev;
        x.next = null;
    }
// 将x的各个成员变量置为null,方便GC,同时对size和modCount进行修改后返回被删除元素的值
    x.item = null;
    size--;
    modCount++;
    return element;
}
//修改指定下标处的值
public E set(int index, E element) {
// check index>=0 && index<size
    checkElementIndex(index);
//通过node方法获取index下标对应的节点,将其item变量更新
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}
//删除所有元素
public void clear() {
//从first向后遍历,将当前节点的成员变量都置空
    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,size恢复为空值
    first = last = null;
    size = 0;
    modCount++;
}
// *****************************************************
// 作为deque的一些删除方法
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
//更新first指针,并判断是否还存在元素
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}
//removeLast与removeFirst实现相似,不再重复
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
    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;
}

deque方法介绍

上面的分析差不多已经介绍完大部分代码了,其实可以发现,有很多代码的逻辑都是相似的。LinkedList作为队列的实现类,还有很多针对队列的实现方法,但其实只是原有代码的再封装,我把之前没提到的一些方法放在这一部分一起介绍了。

//返回队列第一个元素的值
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}
//返回队列第一个元素的值,与peek的区别在于,element方法在第一个元素不存在时会抛出异常
public E element() {
    return getFirst();
}
//返回队列第一个元素,并删除
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
//返回队列第一个元素,并删除,为空抛异常
public E remove() {
    return removeFirst();
}
//末尾添加
public boolean offer(E e) {
    return add(e);
}
//头节点添加
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}
//末尾添加
public boolean offerLast(E e) {
    addLast(e);
    return true;
}
//弹出list的第一个元素
public E pop() {
    return removeFirst();
}

总结

LinkedList的大部分代码都介绍完了,可以看出来比较简单。LinkedList底层实现为双向链表,除了可以作为List外,还可以作为队列和栈的实现类。因为是双向链表,所以对频繁的插入和删除操作较为友好,但是对频繁的根据下标获取的场景不够友好,需要权衡一下。才疏学浅,如有错误,欢迎指出

往期回顾

欢迎关注我的公众号

欢迎关注我的公众号,会经常分享一些技术文章和生活随笔~

技术旅途

相关文章

网友评论

    本文标题:Java集合源码系列-LinkedList

    本文链接:https://www.haomeiwen.com/subject/xueusqtx.html