zoukankan      html  css  js  c++  java
  • java集合之List源码解析

    List是java重要的数据结构之一,我们经常接触到的有ArrayList、Vector和LinkedList三种,他们都继承来自java.util.Collection接口,类图如下

    接下来,我们对比下这三种List的实现和不同:

    一、基本实现

    1、ArrayList和Vector使用了数组实现,可以认为它们封装了对内部数组的操作;它们两个底层的实现基本可以认为是一致的,主要的一点区别在于对多线程的支持上面。ArrayList没有对内部的方法做线程的同步,它不是线程安全的,而Vector内部做了线程同步,是线程安全的。

    2、LinkedList使用了双向链表数据结构,与基于数组实现的ArrayList和Vector相比,这是一种不同的实现方式,这也决定了他们不同的应用场景。LinkedList链表由一系列列表项构成,一个表项包含三个部分:元素内容、前驱表项和后驱表项,如下图所示

         

    在JDK的实现中,增加了两个节点指针first、last分别指向首尾节点

     

    二、不同之处

    在这里我们主要对比下ArrayList与LinkedList的不同之处

    1、增加元素到列表尾端:

    在ArrayList中增加元素到列表尾端

        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // 确保内部数组有足够的空间
            elementData[size++] = e; //将元素加入到数组的末尾,完成添加
            return true;
        }

    在这个过程当时,add的性能主要是由ensureCapacityInternal方法的实现,我们继续往下跟踪代码

        private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
    
        private static int calculateCapacity(Object[] elementData, int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
        }
    
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
    
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }

    calculateCapacity方法会根据你对ArrayList初始化的不同,对当前elmentData这个初始化数组进行非空判断。如果它是一个空数组,则返回ArrayList默认容量(10)和所需最小容量(minCapacity)比较的最大值,如果不为空则直接返回所需最小容量。接下来在ensureExplicitCapacity方法中判断,如果所需容量大于当前对象数组的长度则调用grow方法对数组进行扩容。

    在这里我们可以看到如果ArrayList容量满足需求时,add()其实就是直接对数组进行赋值,性能很高。而当ArraList容量无法满足要求扩容时,需要对之前的数组进行复制操作。因此合理的数组大小有助于减少数组的扩容次数,如果使用时能够预估ArrayList数组大小,并进行初始化,指定容量大小对性能会有所提升。

    在LinkedList中增加元素到列表尾端

        //尾端插入,即将节点值为e的节点设置为链表的尾节点
        void linkLast(E e) {
            final Node<E> l = last;
            //构建一个前驱prev值为l,节点值为e,后驱next值为null的新节点newNode
            final Node<E> newNode = new Node<>(l, e, null);
            //将newNode作为尾节点
            last = newNode;
            //如果原尾节点为null,即原链表为null,则链表首节点也设置为newNode
            if (l == null)
                first = newNode; 
            else  //否则,原尾节点的next设置为newNode
                l.next = newNode;
            size++;
            modCount++;
        }

    LinkedList由于使用了链表结构,因此不需要维护容量的大小,这是相比ArrayList的优势。但每次元素的增加都需要新建一个node对象,并进行更多的赋值操作。在大数据量频繁的调用过程中,对性能会有所影响。

    2、增加元素到任意位置:

    void add(int index, E element)

     由于实现上的不同,ArrayList和LinkedList在这个方法上存在存在一定的性能差异。由于ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该位置后的所有元素需要重新排列,因此效率会比较低。

    ArrayList代码实现如下:

        public void add(int index, E element) {
            rangeCheckForAdd(index);
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //数组复制
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            elementData[index] = element;
            size++;
        }

    可以看到,ArrayList每次插入操作,都会进行一次数组复制。并且插入的元素在List中位置越靠前,数组重组的开销也越大。

    再开LinkedList代码实现

        public void add(int index, E element) {
            checkPositionIndex(index);
    
            if (index == size)
                linkLast(element);
            else
                linkBefore(element, node(index));
        }
    
        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;
            }
        }
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        //指定节点的前驱prev
        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++;
    }

    LinkedList中定位一个节点需要遍历链表,如果新增的位置处于List的前半段,则从前往后找;若其位置处于后半段,则从后往前找。因此指定操作元素的位置越靠前或这靠后,效率都是非常高效的。但如果位置越靠中间,需要遍历半个List,效率较低。因此LinkedList中定位一个节点需要遍历链表,所以下标有关的插入、删除时间复杂度都变为O(n) ;

    3、删除任意位置元素

     public E remove(int index) 

    对ArrayList来说,remove()方法和add()方法是相同的,在删除指定位置元素后,都要对数组进行重组。代码如下

        public E remove(int index) {
            rangeCheck(index);
    
            modCount++;
            E oldValue = elementData(index);
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                //移动数组
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
    
            return oldValue;
        }

    可见,在进行一次有效删除后,都要进行数组的重组。并且跟add()指定位置的元素一样,删除元素的位置越靠前,重组时的开销就越大,删除的元素位置越靠后,开销越小

    再看LinkedList中代码的实现如下

        public E remove(int index) {
            checkElementIndex(index);
            return unlink(node(index));
        }
    
       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;
            }
        }
    
      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;
    } 

    可见跟之前的插入任意位置一样,LinkedList中定位一个节点需要遍历链表,效率跟删除的元素的具体位置有关,所以删除任意位置元素时间复杂度也为O(n) ;

    4、随机访问

      public E get(int index)

    首先看ArrayList的实现代码如下

        public E get(int index) {
            rangeCheck(index);
    
            return elementData(index);
        }
    
        @SuppressWarnings("unchecked")
        E elementData(int index) {
            return (E) elementData[index];
        }

    可见ArrayList随机访问是直接读取数组第几个下标,效率很高。

    LinkedList实现代码如下

        public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
        }
    
        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;
            }
        }

    相反LinkedList随机访问,每次都需要遍历半个List确定元素位置,效率较低。

    5、总结

    通过比较与分析ArrayList与LinkList两种不同实现的的List的功能代码后,我个人感觉两种List的具体使用真的要看实际的业务场景,有些具体的功能如新增删除等操作根据实际情况,效率不可一概而论。在这里进行简单的分析只是为了个人加强理解,如有不正确的地方还望指出与海涵。

     参考资料:《Java程序性能优化》

    关注微信公众号,查看更多技术文章。

     

  • 相关阅读:
    896. Monotonic Array单调数组
    865. Smallest Subtree with all the Deepest Nodes 有最深节点的最小子树
    489. Robot Room Cleaner扫地机器人
    JavaFX
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
  • 原文地址:https://www.cnblogs.com/dafanjoy/p/9690568.html
Copyright © 2011-2022 走看看