码迷,mamicode.com
首页 > 其他好文 > 详细

List接口

时间:2018-02-14 00:44:02      阅读:82      评论:0      收藏:0      [点我收藏+]

标签:order   个数   构造   返回   生成   赋值   cap   nod   结果   

  为方便开发人员进行程序开发,JDK提供了一组主要的数据结构实现,如List,Map,Set。网上有许多优秀的源码解析,就不再做多余分析。本节主要讨论List结构的使用方法和优化技巧。

  List是最重要的数据结构之一。常见又是最重要的三种List实现:ArrayList,Vector,LinkedList。三种List均来自AbstratList的实现,而AbstratList直接实现List接口,并扩展自AbstratCollection。

  其中ArrayList和Vector使用了shuzu实现。可以认为ArrayList或者Vector封装了对内部数组的操作。比如向数组中添加、删除、插入新的元素或者数组的扩展和重定义。对ArrayList或者Vector的操作等价对内部对象数据的操作。

  ArrayList和Vector几乎使用了相同的算法,他们唯一区别可以认为是对多线程的支持。ArrayList没有仁和一个方法做线程同步,因此不是线程安全。Vector中绝大部分方法都做了线程同步,
是一种线程安全的实现。因为ArrayList和Vector的性能相差无几。从理论上将没有实现线程同步的ArrayList要稍好于Vector。但实际表现并不明显。

  LinkedList使用了循环双向链表数据结构。与基于数组的List相比,这是两种截然不同的实现技术,这也决定了他们将适用于完全不同的工作场景。以下是ArrayList和LinkedList的不同比较。

1.增加元素到列表尾端

  在ArrayList中增加元素到队列尾端的代码如下:

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

  ArrayList中的add()方法的性能取决于grow(),实现如下(基于JDK1.8)

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);  // 扩容到原来的1.5倍
        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);
    }

  可以看到,只要ArrayList的当前容量足够大,add()操作的效率是非常高的。

  当ArrayList对容量的需求超过当前数组的大小时,才需要进行扩容。扩容过程中会进行大量的数组复制操作。而数组复制时,最终将调用System.arraycopy()方法,因此add()操作的效率还是相当高的。

  LinkedList的add()操作实现如下,他也将任意元素增加到队列的尾端:

public boolean add(E e) {
        linkLast(e);   // 将元素添加直尾端
        return true;
    }

  我们再深入linkLast()方法:

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

  可见,LinkedList由于使用了链表的结构,因为不需要维护容量的大小。从这点上说,它比ArrayList有一定的性能优势,然而每次元素增加都需要新建一个Node对象,并进行更多的赋值操作。在频繁的系统调用中,对性能产生一定的影响。

   分别使用ArrayList和LinkedList运行一下代码(-Xmx512M-Xms512M):

Object obj = new Object();
for(int i = 0; i < 500000; i++)
{ // 循环50万次
    list.add(obj);
}

  使用-Xmx512M Xms512M的目的是屏蔽GC对程序执行 速度测量的干扰(稳定的堆空间会减少GC次数,但也会增加每次GC时长)。ArrayList相对耗时16ms,而LinkedList相对耗时31ms。可见,不间断地生成新的对象还是占用了一定的系统资源。

2.增加元素到列表任意位置

  除了提供了增加元素到List的尾端,List接口还提供了在任意位置插入元素的方法:

void add(int index, E element);

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

public void add(int index, E element) {
        rangeCheckForAdd(index); //  检测是否存在越界风险

        ensureCapacityInternal(size + 1);  // 增加长度
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

  可以看到,每次插入操作,都会进行一次数组复制。而这个操作在增加元素到List尾端的时候是不存在的。大量的数组重组操作会导致系统性能底下。并且插入的元素在List中的位置越靠前,数组的重组开销也越大。

  而LinkedList此时就显示出了优势:

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

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

  可见,对LinkedList来说,在List尾端插入数据与在任意位置插入数据是一样的。并不会因为插入的位置靠前而导致插入方法的性能降低。如果在系统应用中,List对象需要经常在任意位置插入元素,则可以考虑用LinkedList代替ArrayList以提高系统性能。

3.删除任意位置元素

  对于元素删除,List接口提供了在任意位置删除元素的方法:

E remove(int index);

  对于ArrayList来说,remove()方法和ad()是雷同的,在任意位置移除元素后,都要进行数组的重组,ArrayList的实现如下:

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; // 最后一个位置设置为null

        return oldValue; // 返回删除元素
    }

  可以看到,在ArrayList的每一次有效元素删除操作后,都要进行数组的重组。并且删除的位置越靠前开销越大。

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

  在LinkedList的实现中,首先要通过循环找到要删除的位置处于List的前半段,则从前往后找;如其处于后半段,则从后往前找。因此,无论删除靠前还是靠后的元素都非常高效。但要删除较中间的元素几乎要遍历完半个List,在List拥有大量元素的情况下,效率很低。下面是测试代码,所有数据均以一个拥有10万个元素的List上进行,分别对ArrayList和LinkedList从LIst的头部、中间和尾部进行删除。

  从头部删除元素代码:

while(list.size() > 0)
{
    list.remove(0);
}

  从List中间删除元素代码:

while(list.size() > 0)
{
    list.remove(list.size() >> 1);
}

  从List尾部删除元素:

while(list.size() > 0)
{
    list.remove(list.size() - 1);
}

  从如下表中可以看到,对于ArrayList从尾部删除元素是效率很高,符合上文分析,从头部删除元素时相当费时;而LinkedList从头尾删除元素时效率相差无几,但是从List中间删除元素时性能非常糟糕。

List类型/删除位置 头部 中间 尾部
ArrayList 6203  3125 16
LinkedList 15 8781 16

4.容量参数

  容量参数是ArrayList和Vector等基于数组的List的特有性能参数,它表示初始化的数组大小。由上文分析可知,当ArrayList所存储的元素数量超过其已有大小时,它便会进行扩容,数组的扩容会导致整个数组进行一次内存复制。因此合理的数组大小有助于减少数组扩容的次数,从而提高系统性能。

  ArrayList默认大小为10,每次扩容后数组大小是原来大小的1.5倍。ArrayList也提供了一个可以指定初始数组大小的构造函数。此处未贴上源码,如有需要可自行去看两处构造函数。现以构造一个拥有100万元素的List为例。当使用默认初始大小时,消耗的相对时间为125ms左右,当直接指定其数组大小为100万时,构造相同的ArrayList仅相对耗时16ms。

  若指定JVM参数-Xms512M-Xms512M,再进行相同的测试。使用默认大小耗时47ms,指定ArrayList初始容量为100万时,相对耗时16ms。可见通过提升堆内存大小,减少使用初始容量大小时的GC次数,ArrayList扩容时的数组复制,依然占用了较多的CPU时间。

  因为,能有效的评估ArrayList的数组大小初始值的情况下,指定容量大小对其性能有较大的提升。

5.遍历列表

  遍历列表操作也是常用的列表操作之一。在JDK1.5之后,至少有三种方式:ForEach操作、迭代器和for循环。以下代码实现了三种遍历方式:

        String tmp;
        long start = System.currentTimeMillis();
        // foreach遍历
        for (String s : list)
        {
            tmp = s;
        }
        System.out.println("foreach spend:" + (System.currentTimeMillis() - start));
        
        // iterator遍历
        start = System.currentTimeMillis();
        Iterator<String> it = list.iterator();
        while(it.hasNext())
        {
            tmp = it.next();
        }
        System.out.println("iterator spend:" + (System.currentTimeMillis() - start));
        
        // for循环
        start = System.currentTimeMillis();
        int size = list.size();
        for(int i = 0; i < size; i++)
        {
            tmp = list.get(i);
        }
        System.out.println("for spend:" + (System.currentTimeMillis() - start));            

   构造一个拥有100万数据的ArrayList和等价的LinkedList,使用以上代码测试,测试结果相对耗时如下:

List类型 ForEach操作 迭代器 for循环
ArrayList 63ms 47ms 31ms
LinkedList 63ms 47ms

  可以看到,最简单的ForEach循环并没有很好的性能表现,综合性能不如普通的迭代器,而使用for循环通过随机访问遍历列表时,ArrayList表现很好,但是LinkedList的表现却无法让人接受。因为LinkedList在进行随机访问时,总会进行一次列表的遍历操作。

  对ArrayList这些基于数组的实现来说,随机访问速度是很快的,在遍历这些List对象时,可以优先考虑随机访问。但对于基于LinkedList的链表实现,随机访问非常差,应尽量避免使用。

 

List接口

标签:order   个数   构造   返回   生成   赋值   cap   nod   结果   

原文地址:https://www.cnblogs.com/huanglog/p/8445854.html

(0)
(0)
   
举报
评论 一句话评论(0
0条  
登录后才能评论!
© 2014 mamicode.com 版权所有 京ICP备13008772号-2
迷上了代码!