哈喽,大家好,我是指北君。
说到集合类,java.utils.ArrayList类可能是大家日常用的最多的类,这个容器,可能对于很多Java Coder 来说,这个集合可以一把梭,但是对于它是怎么实现的,你真的明白吗?不知道不要紧,善解人意的指北君写下了这篇文章,包你一看就明白了。
1、ArrayList 定义
ArrayList 是一个用数组实现的集合,支持随机访问,元素有序且可以重复。
1 |
|
①、实现 RandomAccess 接口
这是一个标记接口,一般此标记接口用于 List 实现,以表明它们支持快速(通常是恒定时间)的随机访问。该接口的主要目的是允许通用算法改变其行为,以便在应用于随机或顺序访问列表时提供良好的性能。
比如在工具类 Collections(这个工具类后面会详细讲解)中,应用二分查找方法时判断是否实现了 RandomAccess 接口:
1 |
|
②、实现 Cloneable 接口
这个类是 java.lang.Cloneable,前面我们讲解深拷贝和浅拷贝的原理时,我们介绍了浅拷贝可以通过调用 Object.clone() 方法来实现,但是调用该方法的对象必须要实现 Cloneable 接口,否则会抛出 CloneNoSupportException异常。
Cloneable 和 RandomAccess 接口一样也是一个标记接口,接口内无任何方法体和常量的声明,也就是说如果想克隆对象,必须要实现 Cloneable 接口,表明该类是可以被克隆的。
③、实现 Serializable 接口
这个没什么好说的,也是标记接口,表示能被序列化。
④、实现 List 接口
这个接口是 List 类集合的上层接口,定义了实现该接口的类都必须要实现的一组方法,如下所示,下面我们会对这一系列方法的实现做详细介绍。
2、字段属性
1 |
|
3、构造函数
1 |
|
此无参构造函数将创建一个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 声明的数组,注意此时初始容量是0,而不是大家以为的 10。
注意:根据默认构造函数创建的集合,ArrayList list = new ArrayList();此时集合长度是0.
1 |
|
初始化集合大小创建 ArrayList 集合。当大于0时,给定多少那就创建多大的数组;当等于0时,创建一个空数组;当小于0时,抛出异常。
1 |
|
这是将已有的集合复制到 ArrayList 集合中去。
4、添加元素
通过前面的字段属性和构造函数,我们知道 ArrayList 集合是由数组构成的,那么向 ArrayList 中添加元素,也就是向数组赋值。我们知道一个数组的声明是能确定大小的,而使用 ArrayList 时,好像是能添加任意多个元素,这就涉及到数组的扩容。
扩容的核心方法就是调用前面我们讲过的Arrays.copyOf 方法,创建一个更大的数组,然后将原数组元素拷贝过去即可。下面我们看看具体实现:
1 |
|
如上所示,在通过调用 add 方法添加元素之前,我们要首先调用 ensureCapacityInternal 方法来确定集合的大小,如果集合满了,则要进行扩容操作。
1 |
|
在 ensureExplicitCapacity 方法中,首先对修改次数modCount加一,这里的modCount给ArrayList的迭代器使用的,在并发操作被修改时,提供快速失败行为(保证modCount在迭代期间不变,否则抛出ConcurrentModificationException异常,可以查看源码865行),接着判断minCapacity是否大于当前ArrayList内部数组长度,大于的话调用grow方法对内部数组elementData扩容,grow方法代码如下:
1 |
|
对于 ArrayList 集合添加元素,我们总结一下:
①、当通过 ArrayList() 构造一个空集合,初始长度是为0的,第 1 次添加元素,会创建一个长度为10的数组,并将该元素赋值到数组的第一个位置。
②、第 2 次添加元素,集合不为空,而且由于集合的长度size+1是小于数组的长度10,所以直接添加元素到数组的第二个位置,不用扩容。
③、第 11 次添加元素,此时 size+1 = 11,而数组长度是10,这时候创建一个长度为10+10*0.5 = 15 的数组(扩容1.5倍),然后将原数组元素引用拷贝到新数组。并将第 11 次添加的元素赋值到新数组下标为10的位置。
④、第 Integer.MAX_VALUE - 8 = 2147483639,然后 2147483639%1.5=1431655759(这个数是要进行扩容) 次添加元素,为了防止溢出,此时会直接创建一个 1431655759+1 大小的数组,这样一直,每次添加一个元素,都只扩大一个范围。
⑤、第 Integer.MAX_VALUE - 7 次添加元素时,创建一个大小为 Integer.MAX_VALUE 的数组,在进行元素添加。
⑥、第 Integer.MAX_VALUE + 1 次添加元素时,抛出 OutOfMemoryError 异常。
注意:能向集合中添加 null 的,因为数组可以有 null 值存在。
1 |
|
5、删除元素
①、根据索引删除元素
1 |
|
remove(int index) 方法表示删除索引index处的元素,首先通过 rangeCheck(index) 方法判断给定索引的范围,超过集合大小则抛出异常;接着通过 System.arraycopy 方法对数组进行自身拷贝。关于这个方法的用法可以参考这篇博客。
②、直接删除指定元素
1 |
|
remove(Object o)方法是删除第一次出现的该元素。然后通过System.arraycopy进行数组自身拷贝。
6、修改元素
通过调用 set(int index, E element) 方法在指定索引 index 处的元素替换为 element。并返回原数组的元素。
1 |
|
通过调用 rangeCheck(index) 来检查索引合法性。
1 |
|
当索引为负数时,会抛出 java.lang.ArrayIndexOutOfBoundsException 异常。当索引大于集合长度时,会抛出 IndexOutOfBoundsException 异常。
7、查找元素
①、根据索引查找元素
1 |
|
同理,首先还是判断给定索引的合理性,然后直接返回处于该下标位置的数组元素。
②、根据元素查找索引
1 |
|
注意:indexOf(Object o) 方法是返回第一次出现该元素的下标,如果没有则返回 -1。
还有 lastIndexOf(Object o) 方法是返回最后一次出现该元素的下标。
8、遍历集合
①、普通 for 循环遍历 前面我们介绍查找元素时,知道可以通过get(int index)方法,根据索引查找元素,那么遍历同理:
1 |
|
②、迭代器 iterator 先看看具体用法:
1 |
|
在介绍 ArrayList 时,我们知道该类实现了 List 接口,而 List 接口又继承了 Collection 接口,Collection 接口又继承了 Iterable 接口,该接口有个 Iterator
1 |
|
该方法是返回一个 Itr 对象,这个类是 ArrayList 的内部类。
1 |
|
注意在进行 next() 方法调用的时候,会进行 checkForComodification() 调用,该方法表示迭代器进行元素迭代时,如果同时进行增加和删除操作,会抛出 ConcurrentModificationException 异常。比如:
1 |
|
解决办法是不调用 ArrayList.remove() 方法,转而调用 迭代器的 remove() 方法:
1 |
|
注意:迭代器只能向后遍历,不能向前遍历,能够删除元素,但是不能新增元素。
③、迭代器的变种 forEach
1 |
|
这种语法可以看成是 JDK 的一种语法糖,通过反编译 class 文件,我们可以看到生成的 java 文件,其具体实现还是通过调用 Iterator 迭代器进行遍历的。如下:
1 |
|
④、迭代器 ListIterator 还是先看看具体用法:
1 |
|
还能一边遍历,一边进行新增或者删除操作:
1 |
|
也就是说相比于 Iterator 迭代器,这里的 ListIterator 多出了能向前迭代,以及能够新增元素。下面我们看看具体实现:
对于 Iterator 迭代器,我们查看 JDK 源码,发现还有 ListIterator 接口继承了 Iterator:
public interface ListIterator
extends Iterator
该接口有如下方法:
我们看在 ArrayList 类中,有如下方法可以获得 ListIterator 接口:
1 |
|
这里的 ListItr 也是一个内部类。
1 |
|
### 9、SubList 在 ArrayList 中有这样一个方法:
1 |
|
作用是返回从 fromIndex(包括) 开始的下标,到 toIndex(不包括) 结束的下标之间的元素视图。如下:
1 |
|
这里出现了 SubList 类,这也是 ArrayList 中的一个内部类。
注意:返回的是原集合的视图,也就是说,如果对 subList 出来的集合进行修改或新增操作,那么原始集合也会发生同样的操作。
1 |
|
想要独立出来一个集合,解决办法如下:
List
subList = new ArrayList<>(list.subList(0, 1));
10、size()
1 |
|
注意:返回集合的长度,而不是数组的长度,这里的 size 就是定义的全局变量。
11、isEmpty()
1 |
|
返回 size == 0 的结果。
12、trimToSize()
1 |
|
该方法用于回收多余的内存。也就是说一旦我们确定集合不在添加多余的元素之后,调用 trimToSize() 方法会将实现集合的数组大小刚好调整为集合元素的大小。
注意:该方法会花时间来复制数组元素,所以应该在确定不会添加元素之后在调用。
13、小结
好了,这就是JDK中java.util.ArrayList 类的介绍。
我是指北君,操千曲而后晓声,观千剑而后识器。感谢各位人才的:点赞、收藏和评论,我们下期更精彩!