Java容器源码重点回顾——LinkedList

1. 概述

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

LinkedList是实现了List和Deque接口的双端链表。LinkedList的底层数据结构是链表,不支持随机读取,但是在插入和删除方面会比ArrayList来得更高效。因为LinkedList实现了Deque接口,这也是使它具有双端队列的特性。此外,LinkedList不是线程安全的,如果想要使得LinkedList变成线程安全的,可以调用类Collections中的synchronizedList静态方法:

List list = Collections.synchronizedList(new LinkedList<>());

2. 节点类

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

其实就是我们平常学数据结构的节点类,定义了节点的元素、前驱节点和后继节点。

3. 成员变量

    // 节点个数
    transient int size = 0;

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

    // 尾节点指针
    transient Node<E> last;

成员变量比较少,包括节点个数、头节点指针和尾节点指针。

4. 构造方法

    // 空构造方法
    public LinkedList() {
    }

    // Collections参数构造方法
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);  // 添加所有元素
    }

LinkedList的空构造方法是什么也不干,Collections参数构造方法则会调用addAll方法将元素全部添加到LinkedList中,这个方法我们后面会介绍。

5. add方法

    // 指定位置插入元素的add方法
    public void add(int index, E element) {
        checkPositionIndex(index);  // 检查插入位置是否合法

        if (index == size)   // 如果插入位置是链表的末尾
            linkLast(element);  // 调用linkLast方法
        else
            linkBefore(element, node(index)); // 插入位置是链表的中间
    }

    // 查找index位置的元素
    Node<E> node(int index) {
        if (index < (size >> 1)) {  // 如果index在链表的前半部分
            Node<E> x = first;
            for (int i = 0; i < index; i++)  // 从前往后查找
                x = x.next;
            return x;
        } else {  // 如果index在链表的后半部分
            Node<E> x = last; 
            for (int i = size - 1; i > index; i--)  // 从后往前查找
                x = x.prev;
            return x;
        }
    }

    // 插入元素到链表的尾部
    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++;
    }

    // 插入元素e到succ之前
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;  // succ的前驱节点
        final Node<E> newNode = new Node<>(pred, e, succ);  // 创建新节点
        succ.prev = newNode;  // 设置succ的前驱为newNode
        if (pred == null)  // 如果前驱节点是null,说明succ原来是头节点
            first = newNode;  // newNode成为新的头节点
        else
            pred.next = newNode;  // 否则,前驱节点的尾节点指向新节点
        size++;
        modCount++;
    }

在指定位置插入元素的add方法,首先是会判断要插入的元素是否是在链表的末尾,如果是的话就直接插入。如果不是插入在链表的末尾,就需要先调用node(index)方法得到原来index位置上的节点,然后再通过linkBefore方法插入到index位置上。

从上面的方法中看到,在LinkedList中完成插入,只需要涉及到index位置上的节点,完全不用移动其他节点,这也是LinkedList的优势,插入非常方便。

6. addAll方法

    //在链表末尾插入Collections
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);  // 检查插入位置是否合法

        Object[] a = c.toArray();  // 将Collections转换成array
        int numNew = a.length;  // array元素的个数
        if (numNew == 0)
            return false;

        Node<E> pred, succ;  // 定义前驱节点和后继节点
        if (index == size) {  // 如果插入的位置是链表末尾
            succ = null;  // 就没有后继节点
            pred = last;  // 前驱节点就是尾节点
        } else {
            succ = node(index);  // 后继就是index位置的节点
            pred = succ.prev;  // 前驱就是index位置的前驱节点
        }

        for (Object o : a) {  // 遍历array中的元素
            @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;   // 更新前驱
        }

        if (succ == null) {  // 如果没有后继
            last = pred;
        } else {  // 更新后继节点的状态
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

addAll方法中其实和普通的add方法没有什么区别,只是换成了Collections,里面包含多个元素。需要注意的是,方法首先是会将Collections转化成array,因为array具有随机访问的特点,比较方便。

7. remove方法

    // 删除指定位置元素的remove方法
    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;  // 前驱的后继节点指向next
            x.prev = null; // gc
        }

        if (next == null) {  // 如果没有后继
            last = prev;  // 前驱节点成为新的尾节点
        } else {
            next.prev = prev;  // 后继节点的前驱指向prev
            x.next = null;  // gc
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

remove方法也不难,将当前节点的前驱和后继关联起来,然后将当前节点的前驱和后继置为null即可。如下图所示:
Java容器源码重点回顾——LinkedList_第1张图片

8. set方法

    public E set(int index, E element) {
        checkElementIndex(index);  // 检查index范围是否合法
        Node<E> x = node(index);  // 去的index位置的节点
        E oldVal = x.item;  // 旧值
        x.item = element;  // 替换新值
        return oldVal;
    }

set方法是链表中修改元素的方法,比较简单。

9. peek和getFirst方法

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

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

peek和getFirst方法都能获取链表的队首元素,区别在于如果队列为空,peek元素不会报错,而getFirst方法会报错。

参考文章:
搞懂 Java LinkedList 源码

你可能感兴趣的:(Java容器源码,java,链表,数据结构)