源码|jdk源码之LinkedList与modCount字段

链表是对上一篇博文所说的顺序表的一种实现。java

与ArrayList思路大相径庭,链表的实现思路是:node

  1. 不一样元素其实是存储在离散的内存空间中的。
  2. 每个元素都有一个指针指向下一个元素,这样整个离散的空间就被“串”成了一个有顺序的表。

从链表的概念来说,它能够算是一种递归的数据结构,由于链表拿掉第一个元素剩下的部分,依然构成一个链表。数据结构

<!-- more -->多线程

时间空间复杂度

  1. 经过索引定位其中的一个元素。因为不能像ArrayList那样直接经过计算内存地址偏移量来定位元素,只能从第一个元素开始顺藤摸瓜来数,所以为O(n)。
  2. 插入元素。实际上插入元素须要看状况:函数

    • 若是指定链表中某个元素将其插之其后,那么首先得找出该元素对应的节点,仍是O(n)。
    • 若是可以直接指定节点往其后插入(如经过迭代器),那么仅仅须要移动指针便可完成,O(1)。
  3. 移除元素。移除和插入相似,得看提供的参数是什么。若是提供的是元素所在的节点,那么也只须要O(1)。

LinkedList的继承结构

img

首先继承结构和ArrayList相似,实现了List接口。
可是,它继承的是继承了AbstractList类的AbstractSequentialList类,
这个类的做用也是给List中的部分函数提供默认实现,只是这个类对链表这种List的实现提供了更多贴合的默认函数实现。源码分析

还有能够注意到,LinkedList实现了Deque接口,这也很显然,链表这种结构自然就适合当作双端队列使用。优化

LinkedList源码分析

节点定义

先来看链表的节点定义:this

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

能够看到,链表节点除了保存数据外,还须要保存指向先后节点的指针。
这里,链表即有后继指针也有前驱指针,所以这是一个双向链表。spa

一组节点之间按顺序用指针指起来,就造成了链表的链状结构。线程

属性和构造函数

transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;

三个属性,first和last分别指向链条的首节点和尾节点。
这样有个好处,就是链表便可以使用头插法也能够采用尾插法。

size属性跟踪了链表的元素个数。虽说遍历一遍链表也能统计到元素个数,
可是那是O(n)的费时操做。
所以,咱们能够发现链表的size方法是O(1)的时间复杂度。

public LinkedList() {
    }

LinkedList的代码很简单,构造函数空空如也。
空表中,first和last字段都为null。

get和set方法

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

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

    Node<E> node(int index) {
        // assert isElementIndex(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;
        }
    }

get和set的思路都是先根据索引定位到链表节点,而后得到或设置节点中的数据,这抽象出了node函数,根据索引找到链表节点。

node的思路也很显然,遍历一遍便可获得。
这里作了一点优化,咱们能够发现LinkedList的实现是一个双向链表,而且LinkedList持有了头尾指针。
那么,根据索引和size就能够知道该节点是在链表的前半部分仍是后半部分,
从而决定从头节点开始遍历仍是从尾节点开始遍历,这样最多遍历 N / 2次便可找到。

添加/删除

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

    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

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

    void linkBefore(E e, Node<E> succ) {
        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++;
    }

添加/删除的思路都相似,删除的代码就不贴了。若是可以提供须要被操做的节点,就能直接移动下指针,O(1)完成。不然就须要遍历找到这个节点再操做。
须要关注两点:

  1. 有的操做是操做头指针,有的操做是操做尾指针。可是无论操做哪个,都须要维护另一个指针及size的值。
  2. 若是是删除,删除后及时把相关节点的item字段置为null,以帮助gc能更快的释放内存。

modCount字段分析

以前阅读ArrayList的代码时发现了modCount这一字段,它是定义在AbstractList类中的。以前不知道它起到什么做用,此次给弄明白了。

迭代器

迭代器迭代中表被修改

考虑如下这段代码:

List<Integer> list = new LinkedList<>();
    Iterator<Integer> it = list.listIterator();
    list.add(1);
    it.next();

在迭代器建立以后,对表进行了修改。这时候若是操做迭代器,则会获得异常java.util.ConcurrentModificationException
这样设计是由于,迭代器表明表中某个元素的位置,内部会存储某些可以表明该位置的信息。当表发生改变时,该信息的含义可能会发生变化,这时操做迭代器就可能会形成不可预料的事情。
所以,果断抛异常阻止,是最好的方法。

实际上,这种迭代器迭代过程当中表结构发生改变的状况,更常常发生在多线程的环境中。

记录表被修改的标记

这种机制的实现就须要记录表被修改,那么思路是使用状态字段modCount
每当会修改表的操做执行时,都将此字段加1。使用者只须要先后对比该字段就知道中间这段时间表是否被修改。

如linkedList中的头插和尾插函数,就将modCount字段自增:

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

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

迭代器

迭代器使用该字段来判断,

private class ListItr implements ListIterator<E> {
        private Node<E> lastReturned;
        private Node<E> next;
        private int nextIndex;
        private int expectedModCount = modCount;

        ListItr(int index) {
            // assert isPositionIndex(index);
            next = (index == size) ? null : node(index);
            nextIndex = index;
        }

        public boolean hasNext() {
            return nextIndex < size;
        }

        public E next() {
            checkForComodification();
            if (!hasNext())
                throw new NoSuchElementException();

            lastReturned = next;
            next = next.next;
            nextIndex++;
            return lastReturned.item;
        }

        /* ... */

        public void set(E e) {
            if (lastReturned == null)
                throw new IllegalStateException();
            checkForComodification();
            lastReturned.item = e;
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

迭代器开始时记录下初始的值:

private int expectedModCount = modCount;

而后与如今的值对比判断是否被修改:

final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

这是一个内部类,隐式持有LinkedList的引用,可以直接访问到LinkedList中的modCount字段。

相关文章
相关标签/搜索