JDK源码之集合(二)——LinkedList

LinkedList是一个结合了队列和链表的数据结构,分别实现了List和Deque接口,并继承了AbstractSequentialList。
JDK源码之集合(二)——LinkedList_第1张图片可以看到,LinkedList的集合继承体系主要有两个:Queue和List,分别包含了队列与链表的特征。同时它还包含了栈的特征。

LinkedList的成员变量

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

Node静态内部类包含三个成员:item,next和prev,分别表示当前结点的数据,后继结点与前驱结点。
依赖于这个Node类,LinkedList的成员变量如下:

    // list的大小
    transient int size = 0;

    // 头结点
    transient Node<E> first;

    //尾结点
    transient Node<E> last;

注意:三个变量都使用了transient修饰,说明默认序列化时不会序列化这些值。

LinkedList的构造方法

    // 空构造器
    public LinkedList() {
     
    }

    // 使用另一个集合来初始化当前list,调用空构造器,然后调用addAll方法来赋值
    public LinkedList(Collection<? extends E> c) {
     
        this();
        addAll(c);
    }

LinkedList作为链表的CRUD操作

单元素增加操作

    public boolean add(E e) {
     
        linkLast(e);
        return true;
    }

这里的主体就是调用了linkLast方法,说明默认的添加会将数据添加到链表尾部:

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

添加步骤如下:

  1. 获取尾结点last
  2. 创建一个新节点newNode,用传入的元素赋值,并将前驱赋值为last,后继赋值为null
  3. 让last指向newNode,作为新的尾结点
  4. 判断原list是否为空。如果为空,将头指针也赋值为last;否则将原尾结点的后继复制为新节点。
  5. 更新size和操作次数

单元素删除操作

    public E remove(int index) {
     
        checkElementIndex(index);
        return unlink(node(index));
    }
    
    /**
      * 如果不是有效的索引,则抛出异常
      */
    private void checkElementIndex(int index) {
     
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    /**
      * 判断当前索引是否存储了有效数据
      */
    private boolean isElementIndex(int index) {
     
        return index >= 0 && index < size;
    }
    
    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;
            // 将当前结点的前驱置为null
            x.prev = null;
        }
        // 如果后继为null,那么当前为尾结点
        if (next == null) {
     
            // 删除当前结点后,前驱变为尾结点
            last = prev;
        } else {
     
            // 否则,将后继的前驱置为当前结点的前驱
            next.prev = prev;
            // 当前结点的后继置为空
            x.next = null;
        }
        // 将当前结点的数据置为空(重要)
        // size自减
        x.item = null;
        size--;
        modCount++;
        // 返回被删除的结点元素
        return element;
    }

单元素删除的主要步骤如下:

  1. 判断当前索引是否是有效的索引;
  2. 获取当前索引的值以及前驱与后继;
  3. 判断边界情况:是否是头结点或尾结点;按照边界的情况分别更新头结点与尾结点;
  4. 如果是普通的结点,需要分别更新前驱与后继;
  5. size自减并返回删除的元素值;

修改某一索引处的值

    public E set(int index, E element) {
     
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

该方法比较简单,分为以下几个步骤:

  1. 判断索引是否越界;
  2. 获取索引处的结点;
  3. 更新索引的值;
  4. 返回旧值;

获取某一索引处的值

    public E get(int index) {
     
        checkElementIndex(index);
        return node(index).item;
    }

该方法除了判断越界以外,主体就是调用node方法获取索引的值:

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

这个方法分为两步:

  1. 如果索引位于list前一半,那么就从前向后遍历取值;
  2. 否则从后向前遍历取值

LinkedList作为队列的基本操作

入队操作

队列的插入操作是在队列的尾部进行插入,因此相当于重用了作为链表特征add(E e)方法:

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

出队操作

队列的删除操作是删除队列的头部,调用了链表的unlinkFirst或removeFirst方法:

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

这两个方法的区别主要在于:poll方法会在空list情况下做空判断,removeFirst在空list情况下会抛出一个NoSuchElementException的异常:

    public E removeFirst() {
     
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(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;
    }

获取队列头部元素但不删除

很简单,略过

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

LinkedList作为栈的操作

入栈出栈操作

这两个方法仅仅是分别调用了addFirst与removeFirst方法,对外呈现的效果不同而已。

    public void push(E e) {
     
        addFirst(e);
    }
    
    public E pop() {
     
        return removeFirst();
    }

序列化与反序列化

序列化

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
     
        s.defaultWriteObject();
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (Node<E> x = first; x != null; x = x.next)
            s.writeObject(x.item);
    }

序列化步骤如下:

  1. 写入默认的可序列化魔数
  2. 写入list的大小
  3. 循环依次写入list的每一个元素的值

反序列化

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
     
        s.defaultReadObject();
        int size = s.readInt();
        for (int i = 0; i < size; i++)
            linkLast((E)s.readObject());
    }

反序列化与序列化步骤相反:

读取默认的可序列化魔数
读取list的size并赋值
按次序读取元素并添加到list中

总结

LinkedList在外部表现上,同时具备了链表,栈和队列的特征。但在其内部实现上,主体采用了链表进行实现的。栈和队列的操作只是复用了链表的方法。

你可能感兴趣的:(java)