Java集合源码分析-LinkedList

上一篇文章Java集合源码分析-ArrayList分析了ArrayList的源码,这篇文章分析下LinkedList源码并进行一些对比和总结。

LinkedList类图

《Java集合源码分析-LinkedList》 LinkedList类图

LinkedList的类图和上篇的ArrayList的类图很相似,只是支持随机访问的
RandomAccess改成了支持队列的
Deque,其次多继承了一个类
AbstractSequentialList
RandomAccess接口里面是空的,只是一个标记接口(Marker),只要集合实现这个接口,就能支持快速随机访问。可以看下
Collections类中的
binarySearch方法:

    public static <T> int binarySearch(List<? extends Comparable<? super T>> var0, T var1) {
        return !(var0 instanceof RandomAccess) && var0.size() >= 5000 ? iteratorBinarySearch(var0, var1) : indexedBinarySearch(var0, var1);
    }

由此可以看出,二分查找根据是否是RandomAccess接口来实行indexedBinarySerach(list,key)还是iteratorBinarySerach(list,key)方法,前者是ArrayList使用的二分查找,后者是LinkedList使用的二分查找,ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快。引用1

LinkedList数据结构

上面的类图中我们可以看到,LinkedList的成员变量很少且只有三个:代表节点个数的size、头结点first和尾节点last。Java中的LinkedList的数据结构是一个双向非循环链表,由内部类Node定义:

    private static class Node<E> {
        E item;
        LinkedList.Node<E> next;
        LinkedList.Node<E> prev;

        Node(LinkedList.Node<E> var1, E var2, LinkedList.Node<E> var3) {
            this.item = var2;
            this.next = var3;
            this.prev = var1;
        }
    }

《Java集合源码分析-LinkedList》 数据结构

构造器

构造器很简单,只提供了两个,一个是无参构造器,一个是带有一个Collection参数的构造器:

    public LinkedList() {
        this.size = 0;
    }

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

LinkedList核心操作
LinkedList核心操作包括:

  1. size():int
  2. indexOf(val:Object):int
  3. get(pos:int):E
  4. set(pos:int, val:E)
  5. add(val:E):boolean
  6. add(pos:int, val:E)
  7. remove(val:int):E
  8. remove(ob:Object):boolean
  9. clone():Object

size():int

    public int size() {
        return this.size;
    }

indexOf(val:Object):int

    public int indexOf(Object var1) {
        int var2 = 0;
        LinkedList.Node var3;
        if (var1 == null) {
            for(var3 = this.first; var3 != null; var3 = var3.next) {
                if (var3.item == null) {
                    return var2;
                }
                ++var2;
            }
        } else {
            for(var3 = this.first; var3 != null; var3 = var3.next) {
                if (var1.equals(var3.item)) {
                    return var2;
                }

                ++var2;
            }
        }
        return -1;
    }

这个方法是从头结点开始遍历查找,还有一个对应的方法lastIndexOf(ob:Object):int是从尾节点开始遍历查找。

get(pos:int):E

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

第二行是进行越界检查,第三行是一个for循环进行下标遍历查找。

set(pos:int, val:E)

    public E set(int var1, E var2) {
        this.checkElementIndex(var1);
        LinkedList.Node var3 = this.node(var1);
        Object var4 = var3.item;
        var3.item = var2;
        return var4;
    }

和get方法类似,首先进行越界检查,然后进行下标遍历查找。

add(val:E):boolean

    public boolean add(E var1) {
        this.linkLast(var1);
        return true;
    }

    LinkedList.Node<E> node(int var1) {
        LinkedList.Node var2;
        int var3;
        if (var1 < this.size >> 1) {
            var2 = this.first;

            for(var3 = 0; var3 < var1; ++var3) {
                var2 = var2.next;
            }
            return var2;
        } else {
            var2 = this.last;

            for(var3 = this.size - 1; var3 > var1; --var3) {
                var2 = var2.prev;
            }
            return var2;
        }
    }

    void linkLast(E var1) {
        LinkedList.Node var2 = this.last;
        LinkedList.Node var3 = new LinkedList.Node(var2, var1, (LinkedList.Node)null);
        this.last = var3;
        if (var2 == null) {
            this.first = var3;
        } else {
            var2.next = var3;
        }
        ++this.size;
        ++this.modCount;
    }

这个是向尾部添加节点,不需要遍历。

add(pos:int, val:E)

    public void add(int var1, E var2) {
        this.checkPositionIndex(var1);
        if (var1 == this.size) {
            this.linkLast(var2);
        } else {
            this.linkBefore(var2, this.node(var1));
        }
    }
    void linkBefore(E var1, LinkedList.Node<E> var2) {
        LinkedList.Node var3 = var2.prev;
        LinkedList.Node var4 = new LinkedList.Node(var3, var1, var2);
        var2.prev = var4;
        if (var3 == null) {
            this.first = var4;
        } else {
            var3.next = var4;
        }
        ++this.size;
        ++this.modCount;
    }

这个是向指定位置插入节点,需要遍历。

remove(val:int):E

    public E remove(int var1) {
        this.checkElementIndex(var1);
        return this.unlink(this.node(var1));
    }

删除指定位置的节点。

remove(ob:Object):boolean

    public boolean remove(Object var1) {
        LinkedList.Node var2;
        if (var1 == null) {
            for(var2 = this.first; var2 != null; var2 = var2.next) {
                if (var2.item == null) {
                    this.unlink(var2);
                    return true;
                }
            }
        } else {
            for(var2 = this.first; var2 != null; var2 = var2.next) {
                if (var1.equals(var2.item)) {
                    this.unlink(var2);
                    return true;
                }
            }
        }
        return false;
    }

删除指定节点。

clone():Object

    public Object clone() {
        LinkedList var1 = this.superClone();
        var1.first = var1.last = null;
        var1.size = 0;
        var1.modCount = 0;
        for(LinkedList.Node var2 = this.first; var2 != null; var2 = var2.next) {
            var1.add(var2.item);
        }
        return var1;
    }

克隆出一个新的LinkedList,注意是浅拷贝。

遍历与性能

和ArrayList一样,LinkedList也是使用三种遍历方式,而且LinkedList的迭代器也是fail-fast的,foreach遍历和Iterator遍历的时候进行增删等操作也会和ArrayList一样抛出ConcurrentModificationException异常,而下标遍历方式不存在这个问题。但是和ArrayList不同的是,LinkedList的下标遍历十分低效,get方法会从头遍历直到index下标,查找一个元素时间复杂度为O(n),遍历的时间复杂度就达到了O(n2),而foreach遍历和Iterator遍历的时间复杂度是O(N)。关于ArrayList和LinkedList的几种循环遍历方式及性能对比分析,这篇文章真是用了心,总结的非常好,非常值得一看。
ArrayList:

  1. 下标遍历最高效,foreach遍历和Iterator遍历性能略差,但是差距不是很明显;
  2. 访问数组中第 n 个数据的时间花费是 O(1),但是要在数组中查找一个指定的数据则是 O(N)
  3. 当向数组中插入或者删除数据的时候,最好的情况是在数组的末尾进行操作,时间复杂度是 O(1) ,但是最坏情况是插入或者删除第一个数据,时间复杂度是 O(N) 。在数组的任意位置插入或者删除数据的时候,后面的数据全部需要移动,移动的数据还是和数据个数有关所以总体的时间复杂度仍然是 O(N)。

LinkedList:

  1. foreach遍历和Iterator遍历性能最高效,下标遍历十分低效,而且随着数据量的增大,下标遍历与foreach遍历和Iterator遍历性能差距巨大,因为时间复杂度是O(n2)
  2. 访问数据需要遍历链表,因此时间复杂度是O(N)
  3. 插入数据的时候,如果插入头结点或者尾节点,可以使用addFirst()、push()addLast()方法,那么不需要遍历链表,只需要修改指针,时间复杂度是O(1),如果向指定位置插入数据的话,则需要遍历链表,时间复杂度是O(N),总的来说时间复杂度是O(N)。
  4. 删除数据的时候,如果删除头结点或者尾节点,可以使用removeFirst()removeLast()方法,那么不需要遍历链表,只需要修改指针,时间复杂度是O(1),如果删除指定位置或者删除指定节点的话,则需要遍历链表,时间复杂度是O(N),总的来说时间复杂度是O(N)。

List集合总结

总结参考自:java集合系列——List集合总结(六)
List继承自Collection,是有序的列表。
实现类有ArrayList、LinkedList、Vector、Stack等

  1. ArrayList基于数组实现,可以动态的增加容量,每次增加后的容量是原来的1.5倍
  2. LinkedList是基于双向非循环链表实现
  3. Vector是基于数组实现的,是一个矢量队列,是线程安全的!
  4. Stack是基于数组实现的,是栈,它继承与Vector,特性是FILO(先进后出)!

使用场景:
应用中如果使用到队列,栈,链表,首先可以想到使用List。不同的场景下面使用不同的工具,效率才能更高!

  1. 当集合中对插入元素数据的速度要求不高,但是要求快速访问元素数据,则使用ArrayList!
  2. 当集合中对访问元素数据速度不做要求不高,但是对插入和删除元素数据速度要求高的情况,则使用LinkedList!
  3. 当集合中有多线程对集合元素进行操作时候,则使用Vector!但是现在Vector现在一般不再使用,如需在多线程下使用,可以用CopyOnWriteArrayList,在java.util.concurrent包下。
  4. 当集合中有需求是希望后保存的数据先读取出来,则使用Stack!

后面的文章会分析Java的Set集合源码。

最后附一张来自引文的微缩版Java集合框架图片:

《Java集合源码分析-LinkedList》 微缩版Java集合框架

    原文作者:宛丘之上兮
    原文地址: https://www.jianshu.com/p/2108084a27f7
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞