
定义
ArrayList的本质是顺序表,内部实现是使用数组存储的,集合扩容时会创建更大的数组空间,把原来的数据复制到新数组中。ArrayList支持对元素的快速随机访问,但是插入和删除时速度会很慢,因为这个过程可能要移动其他的元素。
默认大小
1 | /** |
初始化
1 | private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //用于默认初始化状态的校验 |
当没有指定容量时,内部数组指定为空数组,当add的时候才会扩容。
一般明确集合数据量范围的话,我们必须初始化容量,不然遇到大数据量时,会进行多次的扩容,进行数组的复制,降低系统性能。那么,数组是如何扩容的呢?
扩容
1 | // 当size+1大于length的时候,对容器进行1.5倍扩容 |
当添加100个元素时,容器的扩容跟踪为:
1 | (new)0——(add first)10—— (add 11)15 --(add 16)22——(add 23)33——(add 34)49...... |
这里如果估量一下,如果有1000个元素,会进行多少次扩容呢?
73——109——163——244——366——549——823——(add 824)1234。 故需要13次扩容
Arrays.copyOf方法实现
1 | public static <T> T[] copyOf(T[] original, int newLength) { |
最大容量
1 | private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //去掉低三位 |
增添
1 | /** |
System.arraycopy本地方法参数说明
- src:原数组
- srcPos:原数组开始复制的下标
- dest:目标数组
- destPos:目标数组开始下标
- length:要复制的元素个数
删除
普通remove
1 | /** |
快速remove
1 | /** |
遍历
为什么遍历删除指定对象时,不能边遍历,边使用list.remove()和list.add()做删除和增加操作?
验证
1 | // output:Exception in thread "main" java.util.ConcurrentModificationException |
因为forEach遍历,编译后是使用内部迭代器Iterator的next方法遍历的,在List中维护了一个modCount字段,表示修改的次数,而Iterator内部也维护了一个字段expectedModCount,每次遍历的时候,会去比较这两个字段的值是否相等,就是checkForComodification()
方法,如果不等,就会抛出ConcurrentModificationException
异常
而使用fori遍历做删除,add的话,有可能会出现问题,如下例,为安全起见一般不建议这么用。
1 | //output:[Student{name='male'}, Student{name='female'}, Student{name='female'}] |
Iterator遍历
1 | public Iterator<E> iterator() { |
实例
1 | Iterator iterator = branchList.iterator(); |
fail-fast
在ArrayList设计了一个内部List类,为什么要设计这个类?
1 | public List<E> subList(int fromIndex, int toIndex) { |
由于SubList提供了add()、remove()、addAll()方法的实现,均加了checkForComodification()
方法的校验,且对他们的操作会影响外部类,所以使用的时候特别注意。这样一种设计使用了适配器模式,当我们对子集合进行修改、插入、删除操作时,可以操控父集合的元素。但一定要注意,当我们对声明子集合后,再对父集合进行了修改,那么子集合的遍历与修改都会报fail-fast异常。
1 | private static void testFailfast() { |
这个案例告诉我们
- 当取完子对象后,父对象与子对象有了联系,就不能随意的改变了,当父对象更改时,再对子对象操作,就会报fail-fast错误。
- 当建立联系后,子对象的更改会影响到父对象,彼此影响,所以使用的时候,一定不要同时对两个对象做修改操作。
asList & toArray
由于ArrayList底层是由数组实现的,那么,肯定是存在数组转集合和集合转数组的方法。我们常使用Arrays.asList(T [])将数组转换为List集合,注意使用该方法时,不能对子集合进行add、remove、clear等操作,因为,返回的ArrayList是一个内部类,没有提供以上方法的实现。
1 | public static <T> List<T> asList(T... a) { |
如果使用了,就会报fail-fast错误。如果一定要使用修改的话,一般我们是这么声明的。
1 | String[] strs = new String[2]; |
ArrayList集合本身提供了一个集合转数组的方法,就是toArray方法。当我们使用这个方法的时候,一定要注意一些小坑。我们先来看一个例子,猜测一下会打印什么?
1 | private static void testArrayList() { |
最终的打印结果为:
[male, male, female, female]
[null, null]
[male, male, female, female, null]
分析一下,第一条结果很明确,能完成复制,当我使用无参的方法时,该方法返回的是Object[]
,不能转化为String[]
,会报ClassCastException异常
。第二条当数组声明的长度比集合小的时候,返回全是空。当大于的时候,能进行复制,其余的默认为null,这是什么导致的呢,我们来看看源码。
无参方法实现
1 | public Object[] toArray() { |
无参方法返回Object数组
有参方法实现
1 | public <T> T[] toArray(T[] a) { |
当小于集合大小时,我们看到这里返回了一个复制好的数组,但是入参数组a被忽略了,所以a还是空,如果声明另一个数组要接受的话,是有值的。
我们看看Arrays.copyOf()方法是如何实现的
1 | public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { |
可以看到,这里copy new了一个空的数组,长度为newLength,然后调用本地方法进行了复制,返回。
当我们集合转数组的时候,要注意,将数组的长度声明为list.size(),这性能往往高于大于时候的长度。