java集合分析(7):ArrayList

来源:互联网 发布:电脑装linux系统 编辑:程序博客网 时间:2024/06/11 11:35

在前面中我们分析了几篇有关集合的类,这一篇我们来看看在我们开发当中经常用到集合类就是ArrayList.

什么是ArrayList

ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
ArrayList继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能.
ArrayList实现了Cloneable接口,即覆盖了函数clone(),能被克隆.
ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
和Vector不同,ArrayList中的操作不是线程安全的。所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。

RandomAccess

ArrayList实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问.

public interface RandomAccess {}

RandomAccess 是一个空的接口,它用来标识某个类是否支持 随机访问(随机访问,相对比“按顺序访问”)。一个支持随机访问的类明显可以使用更加高效的算法。

本篇中ArrayList支持随机访问, 它的数据结构使得 get(), set(), add()等方法的时间复杂度都是更有效率;

而我们知道 LinkedList结构是链表,它不支持随机访问,只能按序访问,因此在一些操作添加删除上性能略逊一筹。

通常在操作一个 List 对象时,通常会判断是否支持随机访问,也就是* 是否为 RandomAccess 的实例*,从而使用不同的算法。

比如遍历,实现了 RandomAccess 的集合使用 get():

for (int i=0, n=list.size(); i < n; i++)          list.get(i);

比用迭代器更快:

  for (Iterator i=list.iterator(); i.hasNext(); )      i.next();

实现了 RandomAccess 接口的类有:
ArrayList, AttributeList, CopyOnWriteArrayList, Vector, Stack 等

ArrayList 的成员变量

1.底层数据结构,数组:

 transient Object[] elementData;

由于数组类型为 Object,所以允许添加 null 。

transient 关键字

  • Java的Serializable提供了一种序列化对象实例的机制。当序列化对象时,可能有一个特殊的对象数据成员,我们不想用Serializable机制来保存它。为了在一个特定对象的一个域上关闭Serializable,可以在这个域前加上关键字transient。
  • transient是Java语言的关键字,用来表示一个域不是该对象序列化的一部分。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。

2.默认容量

 private static final int DEFAULT_CAPACITY = 10;

3.空的对象数组

 private static final Object[] EMPTY_ELEMENTDATA = {};

4.元素数量

private int size;  

构造方法

1.带有容量initialCapacity的构造方法

  public ArrayList(int initialCapacity) {        super();         // 如果初始化时ArrayList小于0          if (initialCapacity < 0)          // 则抛出IllegalArgumentException异常              throw new IllegalArgumentException("Illegal Capacity: "+                                               initialCapacity);        //指定容量,创建elementData对象数组        this.elementData = new Object[initialCapacity];}  

2.不带参数的构造方法

  public ArrayList() {        super();        //将空数组赋值给elementData         this.elementData = EMPTY_ELEMENTDATA;    }

3.带参数Collection的构造方法

 //直接创建和指定集合一样内容的ArrayList public ArrayList(Collection<? extends E> c) {        elementData = c.toArray();        size = elementData.length;        // c.toArray might (incorrectly) not return Object[] (see 6260652)        if (elementData.getClass() != Object[].class)             //使用 Arrays.copy 方法copy创建一个 Object 数组            elementData = Arrays.copyOf(elementData, size, Object[].class);    }

ArrayList 的关键方法

1.添加元素

 //添加元素 public boolean add(E e) {        //对数组的容量进行扩容          ensureCapacityInternal(size + 1);  // Increments modCount!!        // 将e赋值给elementData的size+1的位置。          elementData[size++] = e;        return true;    } //在ArrayList的指定位置添加元素element public void add(int index, E element) {       // 判断index是否越界            if (index > size || index < 0)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));        //对数组的容量进行扩容          ensureCapacityInternal(size + 1);  // Increments modCount!!      // 将elementData从index开始的size-index个元素复制到index+1至size+1的位置(即index开始的元素都向后移动一个位置)        System.arraycopy(elementData, index, elementData, index + 1,                         size - index);        elementData[index] = element;        size++;    }    //添加一个集合    public boolean addAll(Collection<? extends E> c) {        //将c转换为数组a          Object[] a = c.toArray();        int numNew = a.length;        //对数组的容量进行扩容          ensureCapacityInternal(size + numNew);  // Increments modCount        //将a的第0位开始拷贝至elementData的size位开始,拷贝长度为numNew          System.arraycopy(a, 0, elementData, size, numNew);        size += numNew;        return numNew != 0;    }    //在ArrayList的指定位置添加元素集合c    public boolean addAll(int index, Collection<? extends E> c) {        // 判断index是否越界          if (index > size || index < 0)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));        //将c转换为数组a          Object[] a = c.toArray();        int numNew = a.length;         //对数组的容量进行扩容          ensureCapacityInternal(size + numNew);  // Increments modCount        int numMoved = size - index;        if (numMoved > 0)            System.arraycopy(elementData, index, elementData, index + numNew,                             numMoved);        System.arraycopy(a, 0, elementData, index, numNew);        size += numNew;        return numNew != 0;    }

System.arraycopy()的作用是实现数组之间的复制;

public static void arraycopy(Object src,                             int srcPos,                             Object dest,                             int destPos,                             int length)

其中:src表示源数组,srcPos表示源数组要复制的起始位置,desc表示目标数组,length表示要复制的长度。

2.删除元素

//删除ArrayList指定位置的元素 public E remove(int index) {        // 判断index是否越界         if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));        modCount++;        // 读取旧值            E oldValue = (E) elementData[index];        // 获取index位置开始到最后一个位置的个数         int numMoved = size - index - 1;        if (numMoved > 0)        // 将elementData的第index+1位置开始拷贝至elementData的index位开始,拷贝长度为numMoved             System.arraycopy(elementData, index+1, elementData, index,                             numMoved);        //原数组中最后一个元素删掉        elementData[--size] = null; // clear to let GC do its work        return oldValue;    } //在ArrayList中移除对象为O的元素 public boolean remove(Object o) {        if (o == null) {           //挨个遍历找到目标            for (int index = 0; index < size; index++)                if (elementData[index] == null) {                    fastRemove(index);                    return true;                }        } else {            for (int index = 0; index < size; index++)                if (o.equals(elementData[index])) {                    fastRemove(index);                    return true;                }        }        return false;    }   //内部方法,“快速删除”,就是把重复的代码移到一个方法里    private void fastRemove(int index) {        modCount++;        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    } //删除所有元素  public boolean removeAll(Collection<?> c) {        return batchRemove(c, false);    }  //移除c中的所有元素  public boolean removeAll(Collection<?> c) {        return batchRemove(c, false);    }  //保留c中所有的元素   public boolean retainAll(Collection<?> c) {        return batchRemove(c, true);    }  //删除或者保留指定集合中的元素  private boolean batchRemove(Collection<?> c, boolean complement) {        final Object[] elementData = this.elementData;        int r = 0, w = 0;        boolean modified = false;        try {            for (; r < size; r++)                if (c.contains(elementData[r]) == complement)                    elementData[w++] = elementData[r];        } finally {            // Preserve behavioral compatibility with AbstractCollection,            // even if c.contains() throws.            if (r != size) {               //发生了异常,直接把 r 后面的复制到 w 后面                System.arraycopy(elementData, r,                                 elementData, w,                                 size - r);                w += size - r;            }            if (w != size) {                // clear to let GC do its work                // 清除多余的元素                for (int i = w; i < size; i++)                    elementData[i] = null;                modCount += size - w;                size = w;                modified = true;            }        }        return modified;    }    //清除所有元素   public void clear() {        modCount++;        // clear to let GC do its work        for (int i = 0; i < size; i++)            elementData[i] = null;        size = 0;    }

3.查询,修改,是否包含元素

    //获取指定位置的元素    public E get(int index) {        //判断index是否越界        if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));       // 返回ArrayList的elementData数组index位置的元素        return (E) elementData[index];    }    //修改指定位置的元素   public E set(int index, E element) {       //判断index是否越界        if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));        //旧值        E oldValue = (E) elementData[index];        //将element赋值给elementData[index]        elementData[index] = element;        return oldValue;    }    //是否包含对象o    public boolean contains(Object o) {        return indexOf(o) >= 0;    }    //对象o在ArrayList中的下标位置,如果存在返回位置i,不存在返回-1    public int indexOf(Object o) {        if (o == null) {            for (int i = 0; i < size; i++)                if (elementData[i]==null)                    return i;        } else {            for (int i = 0; i < size; i++)                if (o.equals(elementData[i]))                    return i;        }        return -1;    }

4.数组扩容

 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);        }    }    //得到最小扩容量    private void ensureCapacityInternal(int minCapacity) {        if (elementData == EMPTY_ELEMENTDATA) {           //最小容量取默认容量和当前元素个数的最大值            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);        }        ensureExplicitCapacity(minCapacity);    }   //判断是否需要扩容    private void ensureExplicitCapacity(int minCapacity) {     // 将“修改统计数”+1,该变量主要是用来实现fail-fast机制的         modCount++;        // overflow-conscious code        if (minCapacity - elementData.length > 0)            //扩容            grow(minCapacity);    }    /**      * 数组缓冲区最大存储容量      * - 一些 VM 会在一个数组中存储某些数据--->为什么要减去 8 的原因      * - 尝试分配这个最大存储容量,可能会导致 OutOfMemoryError(当该值 > VM 的限制时)      */      // MAX_VALUE为231-1,MAX_ARRAY_SIZE 就是获取Java中int的最大限制,以防止越界      private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;      private void grow(int minCapacity) {        // overflow-conscious code        // 获取到ArrayList中elementData数组的长度         int oldCapacity = elementData.length;        //扩容至原来的1.5倍          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:        // 调用Arrays.copyOf方法复制一个长度为newCapacity的elementData数组并返回        elementData = Arrays.copyOf(elementData, newCapacity);    }    private static int hugeCapacity(int minCapacity) {        if (minCapacity < 0) // overflow            throw new OutOfMemoryError();        return (minCapacity > MAX_ARRAY_SIZE) ?            Integer.MAX_VALUE :            MAX_ARRAY_SIZE;    }

5.数组转换:

  public Object[] toArray() {        return Arrays.copyOf(elementData, size);    }  //将ArrayList里面的元素赋值到一个数组中去 public <T> T[] toArray(T[] a) {        if (a.length < size)            // Make a new array of a's runtime type, but my contents:            return (T[]) Arrays.copyOf(elementData, size, a.getClass());        System.arraycopy(elementData, 0, a, 0, size);        if (a.length > size)            a[size] = null;        return a;    }

6.subList(int fromIndex, int toIndex)

 public List<E> subList(int fromIndex, int toIndex) {            subListRangeCheck(fromIndex, toIndex, size);            return new SubList(this, offset, fromIndex, toIndex);        }

7.trimToSize()

    public void trimToSize() {        modCount++;        if (size < elementData.length) {            elementData = Arrays.copyOf(elementData, size);        }    }

由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length很size相同,节省空间。

ArrayList的三种遍历

随机访问,通过索引值去遍历

由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。

   ArrayList<Integer> array = new ArrayList<Integer>();        array.add(1);        array.add(2);        array.add(1, 3);        for(int i = 0; i < array.size(); i++){            System.out.println(array.get(i));            if (i == 1) {                array.add(1);            }        }

打印结果: 1 3 2 1

通过迭代器遍历

Iterator<Integer> it = array.iterator();        while(it.hasNext()){            //array.add(4);  会导致modCount发生变化,从而导致next方法的checkModCount抛出异常            int value = it.next();            //array.remove(new Integer(value));同样会导致modCount发生变化            it.remove();//但是可以这样,因为Iterator的remove方法删除元素之后对modCount又进行了复原            System.out.println(value);        }        System.out.println(array.size());

打印结果:1 3 2 0

使用forEach循环(其实本质就是利用的Iterator循环)先是it.hasNext(),然后在将value = it.next(),所以执行这段代码时,会先打印第一个值,在进行第二轮循环的value = it.next()时抛出异常.

  for(Integer value : array){            array.add(4);//发生ConcurrentModificationException错误            //array.remove(value);同上            System.out.println(value);        }

下面我们来比较这3种遍历方式的效率

public class Test {    /**     * @param args     */    public static void main(String[] args) {        // 通过Iterator遍历ArrayList        iterator(getArrayListData());        // 通过快速随机访问遍历ArrayList        for1(getArrayListData());        // 通过forEach循环遍历ArrayList        for2(getArrayListData());    }    private static ArrayList getArrayListData() {        ArrayList<Integer> llist = new ArrayList();        for (int i = 0; i < 1000000; i++)            llist.add(i);        return llist;    }    /**     * 通过迭代器遍历ArrayList     */    private static void iterator(ArrayList<Integer> list) {        // 记录开始时间        long start = System.currentTimeMillis();        Iterator iter = list.iterator();         while (iter.hasNext()) {            iter.next();        }        // 记录结束时间        long end = System.currentTimeMillis();        long interval = end - start;        System.out.println("迭代器遍历:" + interval + " ms");    }    /**     * 通过快速随机访问遍历LinkedList     */    private static void for1(ArrayList<Integer> list) {        // 记录开始时间        long start = System.currentTimeMillis();        int size = list.size();        for (int i = 0; i < size; i++) {            list.get(i);        }        // 记录结束时间        long end = System.currentTimeMillis();        long interval = end - start;        System.out.println("快速随机访问:" + interval + " ms");    }    /**     * 通过forEach循环来遍历LinkedList     */    private static void for2(ArrayList<Integer> list) {        // 记录开始时间        long start = System.currentTimeMillis();        for (Integer integ : list){        }        // 记录结束时间        long end = System.currentTimeMillis();        long interval = end - start;        System.out.println("forEach循环:" + interval + " ms");    }}

运行结果:

这里写图片描述
通过上面运行结果可以看到,遍历ArrayList获取元素值时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器遍历效率最低。

总结:

  • ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用ensureCapacity方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。

  • ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。

  • ArrayList 不是线程同步的,所以在并发访问时,如果在迭代的同时有其他线程修改了 ArrayList, 会发生 ConcurrentModificationException 异常错误,也就是fail-fast。

参考:

http://blog.csdn.net/ljcITworld/article/details/52041836

http://blog.csdn.net/ljcITworld/article/details/52041836

原创粉丝点击