Java基础集合类(一):ArrayList详解

ArrayList详解

1、简介

ArrayList是我们比较常用的一个Java集合类,内部是使用Object数组来存储元素,允许存储null元素,在添加元素时,会根据元素的个数来自动增加数组的大小。值得注意的是ArrayList是非线程安全的,可以使用Collections.synchronizedList方法把ArrayList对象转换成线程安全的对象,这个方法实际上是把ArrayList的每一个方法都放到synchronized关键字修饰的语句块中,因此在平时使用中,如果读多写少的话,使用ReentrantReadWriteLock可重入的读写锁来控制对ArrayList的读写,效率会更高一些。

由于ArrayList内部是使用Object数组来存储数据的,而在添加元素时扩容是将数组容量增加到原大小的1.5倍,因此ArrayList的容量是大于或者等于当前元素数量的,在ArrayList内部是使用size属性来记录当前元素数量,扩容有可能会造成一定的空间浪费,特别是在元素数量比较大的情况下,如下图:



数组由10扩容到15,添加一个元素后便不再添加,则后面的4个空间就浪费掉了。

ArrayList的类继承结构如下:




2、构造器

ArrayList有3个构造器,一个无参构造器和两个有参构造器。

2.1、ArrayList()

首先初始化一个空的Object数组,在首次添加元素时,使用Arrays.copyOf方法把原始的空数组拷贝成一个大小为10的数组。关键代码如下:
    private void grow(int minCapacity/*这个就是初始化的容量10*/) {
        //初始化的数组大小为0
        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);
    }

2.2、ArrayList(int initialCapacity)

这个是指定初始容量的构造器,只要初始容量大于0,则初始化指定容量大小的数组,关键代码如下:
    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);
        }
    }

2.3、ArrayList(Collection<? extends E> c)

这个是在初始化列表的同时把一个集合添加到ArrayList中,首先把c转为Object[],如果c的class不是Object[].class,则使用Arrays.copyOf方法把c拷贝为Object[]并重新赋值给ArrayList,关键代码如下:
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //若入参集合中元素为空,则初始化一个空的数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

3、关键方法

在讲述关键方法前,说一个比较重要的属性modCount,这个是从父类AbstractList继承过来的属性,是用来记录列表结构变更的次数,结构变更即那些更改列表size的方法,这个属性主要是在迭代过程中判断当前列表的结构有没有被变更过,若变更过则抛出ConcurrentModificationException异常。

3.1、add(E e)

添加指定元素到列表的末尾,modCount加1,首先确保列表当前的容量(即Object数组的长度)是大于或者等于size+1的,
    private void ensureExplicitCapacity(int minCapacity/*传入的是size+1*/) {
        modCount++;

        // 若数组容量小于size+1,则需要增长容量
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

否则,首先计算新容量为原始容量的1.5倍,若新的容量还是小于传入的size+1,则使用size+1作为新容量,再判断新容量是否大于Integer.MAX_VALUE - 8(即允许的列表最大容量),若大于并且为负数,说明已经超出了整数范围,抛出OutOfMemoryError错误,最后把原始的Object数组使用Arrays.copyOf拷贝为一个长度为新容量的数组。
    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);
    }
在容量确保完成后,在elementData的size位置赋值指定的元素,并把size加1。

3.2、add(int index, E element)

这个重载的add方法与3.1的add方法很像,这个是可以在指定索引插入元素的方法,首先看一幅图:


上述的这种使用System.arraycopy移位方法在后面的很多方法中都使用的到。首先校验索引index范围必须是[0,index],modCount加1,然后确保容量满足size+1,不满足则扩容为原始容量的1.5倍,然后就按照上图的步骤来插入元素,最后size加1。关键代码如下:

    public void add(int index, E element) {
        rangeCheckForAdd(index);//校验index范围

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);//从index元素开始一直到末尾,整体后移一位
        elementData[index] = element;
        size++;
    }

3.3、addAll(Collection<? extends E> c)

把指定集合中的所有元素追加到ArrayList的末尾,把c转为Object数组a,modCount加1,首先确保容量满足size+a.length,不满足则扩容,然后使用System.arraycopy把数组a拷贝到elementData的末尾,最后size累加上a.length。关键代码如下:
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

3.4、addAll(int index, Collection<? extends E> c)

在指定的索引位置插入指定集合中的所有元素,看下图:

首先检查index范围必须在[0,size]之间,modCount加1,确保容量满足size+c的大小,然后就是按照上图来进行移动,最后size加上c的大小,关键代码如下:
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

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

3.5、clone()

克隆出一个ArrayList,注意是浅克隆,ArrayList中的元素与原始列表中的元素是共享的。

3.6、indexOf(Object o)

若o为null,则循环elementData,找出第一个null并返回所在索引,不为空,则循环elementData,找出第一个equals入参o的索引并返回,没找到则返回-1。

3.7、contains(Object o)

就是使用indexOf来判断元素是否存在,indexOf返回不为-1即元素存在,否则元素不存在。

3.8、ensureCapacity(int minCapacity)

增加ArrayList的容量,只要minCapacity大于当前的ArrayList容量,就会使ArrayList扩容,并且会使modCount加1。推荐用法:在需要存入大量的元素前,根据元素个数和当前ArrayList的size来调用此方法,以确保ArrayList容量能满足待存入的大量元素,这样就可以减少在添加元素时的扩容,从而提高效率。

3.9、forEach(Consumer<? super E> action)

这个是在jdk1.8添加的新方法,主要是用来循环ArrayList,使用每一个元素作为入参来调用action,配合jdk1.8的特性,可以很方便的使用方法引用和Lambda表达式来遍历执行ArrayList,例如:
    @Test
    public void test03() {
        List<Integer> a = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
        //使用方法引用
        a.forEach(System.out::println);
        System.out.println("-----------------------");
        //使用lambda表达式
        a.forEach(x -> System.out.println(x));
    }

3.10、get(int index)

index必须小于size,返回elementData数组中index索引的元素。

3.11、iterator()

返回ArrayList自己实现的迭代器,这个迭代器相比AbstractList中的实现是优化过的。此迭代器实现了remove方法,可以在迭代过程中移除元素。在迭代期间会校验modCount是否被修改了,若被修改了则会抛出ConcurrentModificationException异常。

3.12、listIterator()

返回ArrayList自己实现的列表迭代器,这个相比普通迭代器可以实现在迭代过程中,指针的向前移动,并且能够替换和添加元素。

3.13、remove(int index)

关键步骤是使用System.arraycopy把ArrayList中index下一位至最后的元素,向左移一位,关键代码如下:

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

3.14、removeAll(Collection<?> c)

这个实现还是比较巧妙的,使用两个指针,r:表示当前迭代的元素索引,w:当前被保留的元素存储的索引,整个过程不创建新的数组,并且使用很少次数的System.arraycopy来移动数组元素。大概步骤:迭代ArrayList,若元素在c中存在,则不保留,否则存入到w所在位置上,并且w加1,其实就是把所有保留下来的元素集中到elementData前面,然后把w以后的所有元素设置为null,帮助垃圾回收,关键代码如下:

    private boolean batchRemove(Collection<?> c, boolean complement/*removeAll方法传入的是false*/) {
        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前部
                    elementData[w++] = elementData[r];
        } finally {
            //r!=size是因为有可能前面调用c.contains时发生异常
            if (r != size) {
                //发生异常后需要把当前迭代位置r以后的元素全部保留下来,否则会导致ArrayList中的元素丢失
                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;
    }

3.15、removeIf(Predicate<? super E> filter)

这个是jdk1.8添加的新方法,移除ArrayList中所有符合filter条件的元素,这个实现也比较的巧妙,首先循环ArrayList,然后使用BitSet来记录下符合filter条件的元素位置,然后再根据BitSet把需要保留的元素集中到elementData的前部,最后再把elementData后面不需要的元素设置为null,帮助垃圾回收,管家代码如下:
    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        // figure out which elements are to be removed
        // any exception thrown from the filter predicate at this stage
        // will leave the collection unmodified
        int removeCount = 0;
        final BitSet removeSet = new BitSet(size);
        final int expectedModCount = modCount;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i];
            if (filter.test(element)) {
                removeSet.set(i);
                removeCount++;
            }
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

        // shift surviving elements left over the spaces left by removed elements
        final boolean anyToRemove = removeCount > 0;
        if (anyToRemove) {
            final int newSize = size - removeCount;
            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                i = removeSet.nextClearBit(i);
                elementData[j] = elementData[i];
            }
            for (int k=newSize; k < size; k++) {
                elementData[k] = null;  // Let gc do its work
            }
            this.size = newSize;
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            modCount++;
        }

        return anyToRemove;
    }

3.16、sort(Comparator<? super E> c)

内部是使用Arrays.sort来实现的。

3.17、subList(int fromIndex, int toIndex)

用来截取列表,实际上返回的是ArrayList内部实现的另外一个列表SubList,这个SubList跟普通的ArrayList没什么区别,值得注意的是,返回的子列表与原列表是共享elementData的,也就是说修改子列表的元素(包括增加和移除),效果会体现到主列表上,反之亦然。

3.18、trimToSize()

整理ArrayList的大小,由于capacity>=size,因此可能会有一定的空间浪费,使用此方法可以把多余不用的空间移除掉,减少空间占用。

4、总结

ArrayList的实现还是比较简单的,内部使用数组来存储元素,在存储元素的集合中,数组应该是效率比较高的。由于每次扩容会扩展为原始容量的1.5倍,在扩容后如果没有添加更多的元素,可能会造成一定的空间浪费。内部大量使用System.arraycopy来移动数组元素,由于这个方法是native本地方法,相对来说效率会高一些。在插入元素时,会首先右移元素,然后再插入,这样会造成每一次插入都会发生至少一次移动元素的操作,因此大量的插入操作会增加额外的资源消耗,在随机插入方面,相比ArrayList,LinkedList的效率会更高一些。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值