为了账号安全,请及时绑定邮箱和手机立即绑定

JDK源码分析(二)——LinkedList

标签:
Java

LinkedList

  JDK api对LinkedList的介绍:

Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null).

大意是LinkedList基于双向链表实现了List接口和Deque(double ended queue 双向队列)接口,允许插入任意对象,包括Null。LinkedList是一个在平常开发中经常使用的类,由于它是基于链表实现的,插入删除相对于ArrayList需要考虑动态扩容,复制原数组到新数组来说,非常简单。它实现了Deque接口,在我们需要进行先进先出FIFO操作时非常方便。

LinkedList继承结构

https://img1.sycdn.imooc.com//5b5c52ad0001a30d05900508.jpg

  LinkedList 是一个继承于AbstractSequentialList的双向链表,AbstractSequentialList 提供了一套基于顺序访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表(比如链表)的接口。实现了List接口,实现了Deque,Queue接口,能够进行队列先进先出FIFO操作。

LinkedList内部类Node

  内部类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;
        }
    }

LinkedList成员属性

    //表示链表的长度
    transient int size = 0;    //链表的第一个节点
    transient Node<E> first;    //链表的最后一个节点
    transient Node<E> last;

LinkedList构造方法

无参构造方法

  提供的无参构造方法是一个空方法,即size为0,first,last为null。因为LinkedList是链表结构,不必要像ArrayList去初始化底层数组。

    public LinkedList() {
    }

有参构造方法

  首先调用无参构造方法,再调用addAll方法,在后面详细介绍这个方法。

    public LinkedList(Collection<? extends E> c) {        this();
        addAll(c);
    }

重要方法

add(E e)

  add方法主要调用了linkLast方法,也就是从链表尾部插入元素,首先新建一个节点,它的前一个节点是链表的尾部节点,下一个节点为空,再将last指向这个新插入的节点,如果之前的last不为空(即链表不是第一次插入元素),我们把之前的last节点的next指向现在的last节点,如果为空,说明我们是第一次插入元素,之前的first,last都为null,那我们就把first也只想新插入的节点。下图展示了链表的结构和插入过程:
https://img1.sycdn.imooc.com//5b5c52b5000105e408260357.jpg

    public boolean add(E e) {
        linkLast(e);        return true;
    }    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++;
    }

add(int index, E element)

  把新元素插入链表指定索引位置index处。首先检查待插入位置index是否合法(0<=index<=size),如果index不合法,抛出异常。index在范围内,若index等于size,就表示要把这个新元素插入到LinkedList的末尾,调用linkLast方法;否则,说明要把这个新元素插入到LinkedList的中间,调用linkBefore方法,把元素插入到指定节点的前面,需要传入两个参数:插入数据e,节点,我们使用node(int index)找到这个节点,作为传入参数,linkBefore具体注释在代码里。
https://img1.sycdn.imooc.com//5b5c52bc0001cf8307560363.jpg
  由此可以看出,LinkedList插入元素相对简单,只需要找到插入元素的位置并改变指向前一个节点和指向后一个节点,就能够顺利的插入元素,这是由于链表的特性决定的。

    public void add(int index, E element) {
        checkPositionIndex(index);        if (index == size)
            linkLast(element);        else
            linkBefore(element, node(index));
    }    private void checkPositionIndex(int index) {        if (!isPositionIndex(index))            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }    private boolean isPositionIndex(int index) {        return index >= 0 && index <= size;
    }    void linkBefore(E e, Node<E> succ) {        // assert succ != null;
        //找到succ的上一个节点
        final Node<E> pred = succ.prev;        //新节点插入到前继节点pred,后继节点succ之间
        final Node<E> newNode = new Node<>(pred, e, succ);        //后继节点的prev指向新节点
        succ.prev = newNode;        //前继节点pred为空,则新节点作为首节点first,否则把前一个节点的next指向新节点
        if (pred == null)
            first = newNode;        else
            pred.next = newNode;
        size++;
        modCount++;
    }    Node<E> node(int index) {        // assert isElementIndex(index);
        //如果index<size/2,从前半部分找
        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;
        }
    }

addAll(Collection<? extends E> c)

  LinkedList的有参构造方法调用了addAll(Collection<? extends E> c)方法,将一个集合对象保存的元素插入到LinkedList的末尾。首先调用toArray方法将集合对象转换为数组,再找到插入位置的前继节点pred和后继节点succ,然后把数组中的元素依次连接到链表上去。

    public boolean addAll(Collection<? extends E> c) {        return addAll(size, c);
    }    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);        //Collection对象转换为Object数组
        Object[] a = c.toArray();        int numNew = a.length;        if (numNew == 0)            return false;        //pred插入位置的前继节点,succ插入位置
        Node<E> pred, succ;        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }        //将数组元素连接到链表上,每次通过节点Node构造方法指定前继节点
        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插入位置节点是否为空确定for循环最后一个元素的后继节点
        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

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

get(int index)

  通过索引找到这个位置节点,返回这个节点保存的数据。首先检查元素索引是否合法(注意与插入位置索引的范围区别,插入元素可以到size,作为最后一个元素,查找元素最大只能到size-1),索引合法,通过node(int index)方法找到这个元素。

    public E get(int index) {
        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;
    }

set(int index, E element)

  将指定索引位置处节点保存的值设置为新的传入的数据,并返回保存的老的数据。set方法的实现其实就比get方法多一步:把原来的数据替换为新的数据。

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

remove(int index)

  根据索引找到对应位置的节点,将节点移除,返回移除节点的数据。首先通过checkElementIndex检查传入的索引是否合法,索引在范围内,调用node(int index)找到这个节点,调用unlink移除这个节点,首先保存待移除节点x的下一个节点next,上一个节点prev,如果prev为空,说明待移除的节点是首节点,那么移除了这个节点,下一个节点就是首节点;prev不为空,将prev的next指向next节点,待移除节点x的prev置为空。如果next为空,说明待移除节点是末尾节点,那么移除了这个节点,上一个节点prev就是末尾节点了;若不为空,把next的prev指向prev节点,同时把待移除节点的next置为空。
  移除节点主要分为两部,把待移除节点的前后两个节点相互连接起来,把节点从链表移除;把待移除节点的prev,next置为空,和链表断除联系。正好是插入的逆过程。

    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;
            x.prev = null;
        }        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

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

Deque方法的实现

offer(E e)

  Deque接口继承了Queue接口,LinkedList实现了队列先进先出的特性,offer方法将元素插入到队列,调用了linkLast方法。

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

poll()

  从队列中取出元素,队列中取元素肯定是从队列的头部取元素,并返回节点保存的值。(f == null) ? null : unlinkFirst(f)三目表达式判断首节点是否为空,若为空直接返回null,不为空,调用unlinkFirst方法。通过调用unlinkFirst方法移除首节点。

    public E poll() {        final Node<E> f = first;        return (f == null) ? null : unlinkFirst(f);
    }    private E unlinkFirst(Node<E> f) {        // assert f == first && f != null;
        final E element = f.item;        //保存首节点的下一个节点next
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;        //next为空,那么总共只有一个节点,直接令last为空,不为空的话next就是首节点,把它的prev指向null
        if (next == null)
            last = null;        else
            next.prev = null;
        size--;
        modCount++;        return element;
    }

remove()

  从队列中取出元素,这个方法与poll方法唯一的差别是首节点是空的话remove方法会抛异常,poll方法不会。JDK的解释:Retrieves and removes the head of this queue. This method differs from poll only in that it throws an exception if this queue is empty.从队列取出元素还是建议用poll方法。

    public E remove() {        return removeFirst();
    }    public E removeFirst() {        final Node<E> f = first;        if (f == null)            throw new NoSuchElementException();        return unlinkFirst(f);
    }

peek()

  返回队列首节点保存的值,不从队列中移除首节点。只通过一个三目运算符就实现了功能。

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

push(E e)

  Deque接口代表着双端队列,那么意味着也能从队首插入元素,队尾取出元素。push方法表示将这个LinkedList作为一个先进后出的栈Stack使用。从栈的顶部插入元素,也就是从list的头部插入元素,调用了linkFirst方法。

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

pop()

  pop方法表示将这个LinkedList作为一个先进后出的栈Stack使用。从栈的顶部取出元素,调用了removeFirst方法。

    public E pop() {        return removeFirst();
    }    public E removeFirst() {        final Node<E> f = first;        if (f == null)            throw new NoSuchElementException();        return unlinkFirst(f);
    }

遍历

listIterator()

  通过iterator或listIterator方法返回一个ListIterator迭代器,迭代器是通过一个内部类ListItr实现ListIterator接口实现的。

    public ListIterator<E> listIterator(int index) {
        checkPositionIndex(index);        return new ListItr(index);
    }

ListItr内部类

  使用foreach遍历最终是通过迭代器来进行遍历的,由于LinkedList是线程不安全的,所以迭代时通过比较modCount值是否改变来判断是否有其他线程在使用LinkedList。

    private class ListItr implements ListIterator<E> {        //上一个迭代元素
        private Node<E> lastReturned;        //下一个要迭代的元素
        private Node<E> next;        //下一个迭代元素的下标
        private int nextIndex;        private int expectedModCount = modCount;        //调用iterator方法默认index为零,从头开始遍历
        ListItr(int index) {            // assert isPositionIndex(index);
            next = (index == size) ? null : node(index);
            nextIndex = index;
        }        //判断是否还有没有遍历完的元素,比较下一个元素的下标与size的大小关系
        public boolean hasNext() {            return nextIndex < size;
        }        //把lastReturned,next依次向后移,并返回之前next指向元素保存的数据
        public E next() {
            checkForComodification();            if (!hasNext())                throw new NoSuchElementException();

            lastReturned = next;
            next = next.next;
            nextIndex++;            return lastReturned.item;
        }        //与next配合使用
        public void remove() {
            checkForComodification();            if (lastReturned == null)                throw new IllegalStateException();

            Node<E> lastNext = lastReturned.next;
            unlink(lastReturned);            if (next == lastReturned)
                next = lastNext;            else
                nextIndex--;
            lastReturned = null;
            expectedModCount++;
        }        //比较是否有多个线程操作LinkedList,确保fail-fast机制
        final void checkForComodification() {            if (modCount != expectedModCount)                throw new ConcurrentModificationException();
        }

    }

总结

  LinkedList是基于双向链表实现的List,适用于频繁增加、删除元素的场景,LinkedList不是线程安全的,在多线程环境下不能
正常的使用。LinkedList的源码总体上分析比较简单,但是LinkedList的功能很多的,能做普通的List使用,LinkedList实现了双向队列Deque,也可以做先入先出的队列或先入后出的栈使用。

原文出处:http://www.cnblogs.com/rain4j/p/9327855.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消