jdk1.7 ArrayList源码浅析

参考:http://www.cnblogs.com/xrq730/p/4989451.html(借鉴的有点多,哈哈)

  首先介绍ArrayList的特性:

     1、允许元素为空、允许重复元素

     2、有序,即插入顺序和访问顺序一致

     3、非同步

ArrayList实现原理为动态数组

首先看构造方法:

  public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

 public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

第一个构造方法为指定大小进行初始化,第二个private static final Object[] EMPTY_ELEMENTDATA = {}; 相当于构造一个空的数组。

主要分析add()方法,其他应该可以类比。

有两个方法add(E e)和add(int index , E element)

add(E e)的方法简单来讲就是:

1、确保数组容量,其中就包括是否扩容然后将数组整个复制。同时modCount(用来记录集合被修改的次数)值加一 

2、将元素添加到数组中

  public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

ensureCapacityInternal从字面上理解就是确保内部容量

   private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
DEFAULT_CAPACITY等于10。AarryList在用无参的构造方法创建时,是没有为里面的数组成员分配空间的,
只有在进行add操作后才会分配空间,并会和10比较。再调用ensureExplicitCapacity来分配具体的空间,
并且在第一次add时会直接分配10个空间。
  private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

在add操作时,若集合所需的最小容量大于当前数组长度(数组长度计算包括了为null的元素),就需要扩容。

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

关于扩容:

1、如果一次性扩容扩得太大,必然造成内存空间的浪费。

2、如果一次性扩容扩得不够,那么下一次扩容的操作必然比较快地会到来,这会降低程序运行效率,要知道扩容还是比价耗费性能的一个操作

 

newCapacity代表具体扩容成多大。第4行就相当于 int newCapacity=oldCapacity+ordCapacity/2(大约1.5倍)。用移位操作可以在数组容量大时节省点时间。

第一个if只在初次分配10个空间时执行。第二个if,hugeCapacity方法主要就是个三元运算符,MAX_ARRAY_SIZE=整型最大值-8(这个设计不懂)

  private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

疑问:为什么不直接赋值为Integer.MAX_VALUE。

关于Arrays.copyOf方法其实就是调用的System.arraycopy,而后者又是加了native关键字,底层应该是c++之类的语言去实现整个数组的复制。

设计上面,Arrays.copyOf分了一个泛型方法和一个专门处理基本类型如int、float的方法。

在明确数组大小时可以手动为其扩容,来减少扩容的次数。

 public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != EMPTY_ELEMENTDATA)
            // any size if real element table
            ? 0
            // larger than default for empty table. It's already supposed to be
            // at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

 

add(int index, E element)方法:

1、判断插入位置是否合法

2、确保数组容量,其中就包括是否扩容然后将数组整个复制。同时modCount(用来记录集合被修改的次数)值加一。

3、将插入位置之后的元素复制到其加1之后的位置去(说的有点绕啊o(╯□╰)o),也就是从插入位置整体向后移一个单位。

4、将元素放到指定位置

 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的优缺点

总结一下ArrayList的优缺点。ArrayList的优点如下:

1、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快

2、ArrayList在对末尾元素进行删除和添加时效率高也就是add(E element)方法,因为不用元素复制(移位)。

不过ArrayList的缺点也十分明显:

1、在调用add(int index, E element),涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

因此,ArrayList比较适合顺序添加、随机访问的场景

ArrayList和Vector的区别

ArrayList是线程非安全的,这很明显,因为ArrayList中所有的方法都不是同步的,在并发下一定会出现线程安全问题。那么我们想要使用ArrayList并且让它线程安全怎么办?一个方法是用Collections.synchronizedList方法把你的ArrayList变成一个线程安全的List,比如:

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++)
{
    System.out.println(synchronizedList.get(i));
}

另一个方法就是Vector,它是ArrayList的线程安全版本,其实现90%和ArrayList都完全一样,区别在于:

1、Vector是线程安全的,ArrayList是线程非安全的

2、Vector可以指定增长因子,如果该增长因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小*2,源代码是这样的:

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                 capacityIncrement : oldCapacity);

LinkedList和ArrayList的对比 

1、顺序插入速度ArrayList会比较快,因为ArrayList是基于数组实现的,数组是事先new好的,只要往指定位置塞一个数据就好了;LinkedList则不同,每次顺序插入的时候LinkedList将new一个对象出来,如果对象比较大,那么new的时间势必会长一点,再加上一些引用赋值的操作,所以顺序插入LinkedList必然慢于ArrayList

2、基于上一点,因为LinkedList里面不仅维护了待插入的元素,还维护了Entry的前置Entry和后继Entry,如果一个LinkedList中的Entry非常多,那么LinkedList将比ArrayList更耗费一些内存

3、ArrayList使用for循环遍历快,因为是通过数组索引直接遍历,每次get的时间复杂度为O(1)
LinkedList使用foreach循环遍历快,因为使用普通for循环会每次从前一个节点拿后一个节点地址,相当于从头遍历一遍,每次get的时间复杂度为O(N)

4、有些说法认为LinkedList做插入和删除更快,这种说法其实是不准确的:

(1)LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Entry的引用地址

(2)ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址

所以,如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,因为它是双向链表,所以在第2个元素后面插入一个数据和在倒数第2个元素后面插入一个元素在效率上基本没有差别,但是ArrayList由于要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList

从这个分析看出,如果你十分确定你插入、删除的元素是在前半段,那么就使用LinkedList;如果你十分确定你删除、删除的元素在比较靠后的位置,那么可以考虑使用ArrayList。如果你不能确定你要做的插入、删除是在哪儿呢?那还是建议你使用LinkedList吧,因为一来LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;二来插入元素的时候,弄得不好ArrayList就要进行一次扩容,ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作。

 

posted @ 2017-06-16 10:55  leftcity  阅读(1366)  评论(0编辑  收藏  举报