java集合类(二)List之ArrayList

ArrayList概述:

  ArrayList是List接口基于数组的实现。它允许包括 null 在内的所有元素。每个ArrayList实例都有一个容量,该容量代表可以存储元素的多少。每个ArrayList实例都有一个初始大小,当然你也可以通过构造方法指定初始大小。随着向ArrayList中不断添加元素,其容量也自动增长。当元素数量达到当前容量最大值时会导致数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。 但是需要注意,此方法的实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。

底层探究

  • 1 初始容量
 private static final int DEFAULT_CAPACITY = 10;
  • 2 底层存储
/** *Object类型数组 */
transient Object[] elementData;
  • 3 构造方法
/** * 指定初始容量 */
 public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+                                   initialCapacity);
        }
    }

   /** * 默认构造,初始容量为10 */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

  /** * 使用一个集合初始化一个ArrayList */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
  • 4 存储
      ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection
//设置指定位置元素的值(可理解为更新操作)
set(int index, E element)
//往集合中添加元素,如果容量不足则扩容
add(E e)
//往指定位置插入元素,如果容量不足则先扩容。原先index及之后的元素均往后移动一位
add(int index, E element)
  • 5 删除
//删除指定位置的元素,原先index之后的元素均往前移动一位
public E remove(int index) ;
//移除此列表中首次出现的指定元素(如果存在),此操作需要遍历数组
public boolean remove(Object o) {

以上介绍了ArrayList的增删等操作,下面再来看下源码

仔细观察我们会发现还有两个空数组

    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这两个空数组有什么用呢?我们再把上面的两个构造方法拿来看下:

   public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

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

我们可以看到,当使用默认构造方法时,直接将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给了elementData,当使用指定容量进行构造ArrayList时,如果initialCapacity=0,将EMPTY_ELEMENTDATA赋值给了elementData,这两种方式构造出来的ArrayList初始容量均为0,那么为什么要使用两个不同的空数组呢?想知道答案我们来看下add(E e);方法:

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

从上面代码可以看到,在add一个元素时,才对容量进行了操作,为什么要把扩容操作放在这里呢?其实是防止你new出一个数组,但是不用,导致空间资源的浪费。回到上面的问题,我们看下ensureCapacityInternal方法:

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

看下这里有个if判断,当elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,对容量有一个赋值操作,minCapacity为DEFAULT_CAPACITY与minCapacity的最大值。显然当第一次add元素时minCapacity==1所以初始容量就为DEFAULT_CAPACITY也就是10了。所以使用两个不同的空数组的原因就是为了保证:当我们使用默认构造方法对ArrayList进行构造时,初始容量为10。

  下面再来看下ensureExplicitCapacitygrow方法:

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        //当最小所需容量大于当前数组容量时进行grow操作
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //先尝试新容量为旧熔炼个的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

  可以看出,当最小所需容量大于当前数组容量时进行grow操作。先尝试新容量为旧熔炼个的1.5倍,如果还不够,那么就使用minCapacity作为当前扩充的容量大小。

  此外有没有注意到ensureExplicitCapacity方法中有一个modCount++的操作,这个modCount是干什么用的呢?
  
  ArrayList的modCount是从类 java.util.AbstractList 继承的字段:

protected transient int modCount

这个字段代表已从结构上修改此列表的次数。从结构上修改是指更改列表的大小(也就是元素个数发生了变化),或者打乱列表,从而使正在进行的迭代产生错误的结果。
此字段由 iterator 和 listIterator 方法返回的迭代器和列表迭代器实现使用。如果意外更改了此字段中的值,则迭代器(或列表迭代器)将抛出 ConcurrentModificationException 来响应 next、remove、previous、set 或 add 操作。在迭代期间面临并发修改时,它提供了快速失败 行为,而不是非确定性行为。

大家可以测试下下面这段程序:

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer integer = iterator.next();
            if (integer == 10)
                list.remove(integer);  //注意这个地方,此处换成list.add(integer)结果是一样的
        }
    }

上面这段代码会抛出如下异常:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at Main.main(Main.java:12)

为何会报这个错?看下ArrayListIterator源码(有部分代码未贴出):

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

我们可以看到当做next()、remove()操作时,都要先checkForComodification()而这个方法做的事情就是判断modCount 与expectedModCount是否相等。不相等就报错。

你可能感兴趣的:(java,list)