概要
ArrayList 是一个动态数组,它是线程不安全的,允许元素为 null。它的底层数据结构是数组,ArrayList 实现了 List<E>, RandomAccess, Cloneable, java.io.Serializable 接口,其中 RandomAccess 代表了其拥有随机快速访问的能力,ArrayList 可以以 O(1) 的时间复杂度去根据下标访问元素。
时间、空间效率
因为数组内存的连续,可以根据下标以 O1 的时间改查元素,因此时间效率很高
同样也因为数组要占据一块连续的内存空间,所以它也有数组的缺点——空间效率不高。
性能
当集合中的元素超出容量时,会进行扩容操作,扩容操作是一个性能消耗较大的地方,所以如果能预知数据的规模,最好在初始化时通过 public ArrayList(int initialCapacity) 构造方法指定 ArrayList 的大小,来构造 ArrayList 实例,以减少扩容次数,提高效率。
在添加大量元素前,应用程序也可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量,这可以减少递增式再分配的数量。 不过该方法是 ArrayList 中添加的,List 中没有该方法。所以如果声明的类型为 List 的话,需要进行强转。((ArrayList)list).ensureCapacity(number);
当每次修改结构时(添加或者删除元素),都会修改 modCount。
成员变量
|
|
构造方法
ArrayList 提供了三种方式的构造器,可以构造一个默认初始容量为 10 的空列表、构造一个指定初始容量的空列表以及构造一个包含指定 collection 的元素的列表,这些元素按照该 collection 的迭代器返回它们的顺序排列的。
|
|
这里要注意的是第三个构造方法中对数组元素类型的判断
|
|
虽然表面上看起来,c.toArray() 会返回一个 Object[] 对象数组,但是它指向的实际类型并不一定是 Object[],这样当我们调用 objList[i] = new Object(); 就会报错 。 比如说如果我们有 1 个 ListObject[] objectArray = stringList.toArray()的时候, objectArray 只能存放 String 类型的数据而不能存储其他类型的对象。
增
ArrayList 提供了 add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)这些添加元素的方法。
|
|
|
|
删
ArrayList 提供了根据下标或者指定对象两种方式的删除功能。
|
|
|
|
|
|
|
|
|
|
性能
注意:从数组中移除元素的操作,也会导致被移除的元素以后的所有元素的向左移动一个位置。
查/获取
获取此列表中指定位置上的元素。
|
|
小结、对比
改
|
|
对比
调整数组容量
扩容
|
|
每次数组容量的增长大约是其原容量的 1.5 倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造 ArrayList 实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用 ensureCapacity 方法来手动增加 ArrayList 实例的容量。
「压缩」
ArrayList 还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过 trimToSize 方法来实现。代码如下:
|
|
Fail-Fast 机制
ArrayList 也采用了快速失败的机制,通过记录 modCount 参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。具体介绍请参考我之前的文章深入 Java 集合学习系列:HashMap 的实现原理 中的 Fail-Fast 机制。
- 快速失败:当你在迭代一个集合的时候,如果有另一个线程正在修改你正在访问的那个集合时,就会抛出一个 ConcurrentModification 异常。
- 在 java.util 包下的都是快速失败。
- 安全失败:你在迭代的时候会去底层集合做一个拷贝,所以你在修改上层集合的时候是不会受影响的,不会抛出 ConcurrentModification 异常。
- 在 java.util.concurrent 包下的全是安全失败的。
即 抛异常是快速失败(util 包下都是快速失败),不抛异常是安全失败。 Java 版本越往后越「安全」,concurrent 包下面全部为安全失败
总结
可以看到核心操作在于增加和删除元素。
- 增删改查中, 增导致扩容,则会修改 modCount,删一定会修改。 改和查一定不会修改 modCount。
- 扩容操作会导致数组复制,批量删除会导致找出两个集合的交集,以及数组复制操作,因此,增、删都相对低效。 而 改、查都是很高效的操作。
- 因此,结合特点,在使用中,以 Android 中最常用的展示列表为例,列表滑动时需要展示每一个 Item(element)的数组,所以 查 操作是最高频的。相对来说,增操作只有在列表加载更多时才会用到 ,而且是在列表尾部插入,所以也不需要移动数据的操作。而删操作则更低频。 故选用 ArrayList 作为保存数据的结构。
- 和
Vector的区别,Vector内部也是数组实现的,区别在于Vector在 API 上都加了synchronized所以它是线程安全的,以及Vector扩容时,是翻倍 size,而ArrayList`是扩容 50%。
参考资料与学习资源推荐
若本文中有不正确的结论、说法,请大家提出,共同探讨,共同进步,谢谢!