JDK源码解析集合篇--LinkedList全解析

本文详细解析了LinkedList的实现原理,包括其作为双向链表的特性、增删查改操作,以及与ArrayList的对比。LinkedList适用于大量插入删除操作,而随机访问则推荐使用ArrayList。此外,文章强调了在遍历LinkedList时使用迭代器的重要性,以避免性能下降。

LinkedList是基于链表实现的,链表和数组是两种不同的线性物理存储结构,具体不再介绍链表的用法,这是基本的数据结构知识。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;
        }
    }

对于LinkedList的特性:
- LinkedList是否允许空 允许
- LinkedList是否允许重复数据 允许
- LinkedList是否有序 有序(按照插入的顺序)
- LinkedList是否线程安全 非线程安全

LinkedList的操作

由于LinkedList是由双向链表实现,且利用LinkedList封装了关于栈和队列的操作,所以里面有很多方法是封装给栈和队列来使用的。在JDK1.5后,引入了Queue接口,配合队列的操作以及并发包中队列内容的引入。
LinkedList的定义为:

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

我们可以看到它实现了Deque借口,此接口表示双向队列,其继承了Queue接口,LinkedList给出其实现。所以我们在要使用队列或栈这种数据结构时,可以使用此接口来操作。
这里写图片描述
上图可以用来进行队列的基本操作,再来看Deque定义的方法:
这里写图片描述
里面定义的push(),pop()方法就是关于栈的相应操作。栈和队列获得首个元素都可以利用peek()方法获得,为什么呢?因为队列是将后入队元素放在连接在队尾,而栈定义的是将后入栈的元素连接在队头。这是双向队列实现这两种数据结构的逻辑。
另外,在JDK1.5还引入了PriorityQueue,也就是我们常用的堆数据结构,最大最小堆一般是利用数组来实现的。关于PriorityQueue的介绍,在后边的并发包学习中再详细了解。可以参看:Java PriorityQueue工作原理及实现
接下来分析一下LinkedList的增删查的基本原理。

添加/插入元素

我们先来看LinkedList的相关属性,可看到,有first指针和last指针,分别指向LinkedList的第一个元素和最后一个元素,符合双向链表的实现。
这里写图片描述
因为是用链表实现的,所以不存在扩容问题。
添加元素:

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    /**
     * Links e as last element. 在链表尾部添加元素
     */
    void linkLast(E e) {
        //获取当前最后一个节点
        final Node<E> l = last;
        //新建节点
        final Node<E> newNode = new Node<>(l, e, null);
        //更新last
        last = newNode;
        //如果是第一个节点的话
        if (l == null)
            //就将新建节点赋值给first节点
            first = newNode;
        else
            //添加到尾部
            l.next = newNode;
        //更新节点数
        size++;
        modCount++;
    }

如何在链表头部插入元素,与上边的代码类似,这里就不再详细看了。
当然也可以在任意位置插入元素:

    public void add(int index, E element) {
        checkPositionIndex(index);
        //如果是插入在最后一个位置
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    /**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        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++;
    }

获取/改变元素

因为是有序的,所以可以通过index位置来获取元素,注意:这里并没有提供indexOf的方法。遍历list时,有一些注意事项,在后边再详细介绍。

    /**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        //检查index边界
        checkElementIndex(index);
        return node(index).item;
    }

    /**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(index);
        //如果 index在链表前半部分   从前往后 
        if (index < (size >> 1)) {
            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;
        }
    }

    /**
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
     //修改index处的元素值
    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

删除元素

   //返回删除的节点值
    public E remove(int index) {
        //检查index是否越界
        checkElementIndex(index);
        return unlink(node(index));
    }

    /**
     * Unlinks non-null node x.  
     */
    //删除某一节点
    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;
        }
        //如果删除的是尾节点
        if (next == null) {
            //删除尾节点
            last = prev;
        } else {
            //改变next的指向
            next.prev = prev;
            //切断它本身的指向
            x.next = null;
        }
        //将它本身的值设为null
        x.item = null;
        size--;
        modCount++;
        return element;
    }
//当然,LinkedList也可以实现,按照元素值来进行删除操作
    public boolean remove(Object o) {
        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;
    }

由于是双向链表的删除操作,上文中给出了经典的删除算法,两步判断删除。

LinkedList和ArrayList的对比

从我们对LinkedList和ArrayList的分析就知道它两个各自的优缺点。
1、顺序插入速度ArrayList会比较快,因为ArrayList是基于数组实现的,数组是事先new好的,只要往指定位置塞一个数据就好了;LinkedList则不同,每次顺序插入的时候LinkedList将new一个对象出来,如果对象比较大,那么new的时间势必会长一点,再加上一些引用赋值的操作,所以顺序插入LinkedList必然慢于ArrayList。
2.、因为实现底层数据结构的不同,其实也是数组和链表的区别,比如:在内存利用方面,链表不要求内存地址连续。基于这一点,因为LinkedList里面不仅维护了待插入的元素,还维护了Entry的前置Entry和后继Entry,如果一个LinkedList中的Entry非常多,那么LinkedList将比ArrayList更耗费一些内存
3、另外,我们都知道的区别是ArrayList是适合随机访问的,通过index直接返回数组对应的索引处的值。但LinkedList是要遍历前半部分或者后半部分获得的。
4、有些说法认为LinkedList做插入和删除更快,这种说法其实是不准确的:
(1)LinkedList做插入、删除的时候,慢在寻址(确定index的位置),快在只需要改变前后Entry的引用地址,不需要拷贝。
(2)ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址。
所以,如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,因为它是双向链表,所以在第2个元素后面插入一个数据和在倒数第2个元素后面插入一个元素在效率上基本没有差别,但是ArrayList由于要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList。
但是,如果我们涉及了大量的删除和插入操作,还是建议:使用LinkedList,因为一来LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;二来插入元素的时候,弄得不好ArrayList就要进行一次扩容,记住,ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作。

另外:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

我们可以看到,ArrayList还实现了RandomAccess接口。RandomAccess只是一个标志接口,和Cloneable、Serializable的效果都是一样的,只是来表明此类是可以支持快速随机访问的。
事实上,使用普通for循环来遍历ArrayList的速度要比使用迭代器的速度要快。但是对于LinkedList的遍历,使用普通for循环,则会非常慢。下边我们具体分析。

关于LinkedList的遍历

对于ArrayList的遍历,我们可以:

public static void main(String[] args)
{
    List<Integer> arrayList = new ArrayList<Integer>();

    for (int i = 0; i < 100; i++)
    {
        arrayList.add(i);
    }
    for (int i = 0; i < 100; i++)
    {
        System.out.println(arrayList.get(i));
    }
}

但对于LinkedList,如果也是这样使用普通for循环来遍历时,则速度会非常慢。这其实已经不是Java的问题,而是数据结构的问题了,我相信语言从Java换成其他的也都一样。
看到ArrayList的get方法只是从数组里面拿一个位置上的元素罢了。我们有结论,ArrayList的get方法的时间复杂度是O(1),O(1)的意思也就是说时间复杂度是一个常数,和数组的大小并没有关系,只要给定数组的位置,直接就能定位到数据。因为数组是地址连续的,存储的对象大小也是确定的,计算机做的只是算出起始地址–>去该地址中取数据而已,因此我们看到使用普通for循环遍历ArrayList的速度很快,也很稳定。
为什么对于LinkedList慢呢?
从上边的源码分析就知道了,使用get(i)操作时:
1、get(0),直接拿到0位的Node0的地址,拿到Node0里面的数据
2、get(1),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,拿到Node1里面的数据
3、get(2),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,从1位的Node1中找到下一个2位的Node2的地址,找到Node2,拿到Node2里面的数据。
也就是说,LinkedList在get任何一个位置的数据的时候,都会把前面的数据走一遍。随着LinkedList的容量越大,差距会越拉越大。LinikedList遍历的时间复杂度为O(N2)
但是利用迭代器(for循环)来进行遍历时:即安全又快速,因为它是记录地址来实现的。我们可以看迭代器的实现(ListIterator)。当然,ArrayList也最好用迭代器遍历,防止线程修改。

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 boolean hasPrevious() {
            return nextIndex > 0;
        }

        public E previous() {
            checkForComodification();
            if (!hasPrevious())
                throw new NoSuchElementException();

            lastReturned = next = (next == null) ? last : next.prev;
            nextIndex--;
            return lastReturned.item;
        }

        public int nextIndex() {
            return nextIndex;
        }

        public int previousIndex() {
            return nextIndex - 1;
        }

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

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

        public void add(E e) {
            checkForComodification();
            lastReturned = null;
            if (next == null)
                linkLast(e);
            else
                linkBefore(e, next);
            nextIndex++;
            expectedModCount++;
        }

        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            while (modCount == expectedModCount && nextIndex < size) {
                action.accept(next.item);
                lastReturned = next;
                next = next.next;
                nextIndex++;
            }
            checkForComodification();
        }

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

ArrayList有iterator和listIterator两种迭代器实现内部类,而LinkedList有listIterator这一种内部类实现,其调用的iterator返回的是其父类Abstract实现的ListIterator。

    //获取迭代器
    List<String> list = new ArrayList<String>();
    ListIterator<String> listIterator = list.listIterator();
    Iterator<String> Iterator = list.iterator();

可以for-each这样语法糖来遍历,其内部是利用迭代器实现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值