
ArrayList源码分析 原创
春节不停更,此文正在参加「星光计划-春节更帖活动」
日积月累,水滴石穿 😄
前言
本文环境 jdk 1.8
ArrayList
这个集合容器我相信是 Java 工程师在工作当中用的最常用的集合了。但用的多并不代表就很了解它,对它的了解程度应该只有这样:ArrayList
和 LinkedList
有什么区别。
那本文就来分析一下ArrayList
的源码,深入的理解ArrayList
实现原理。让你回答面试题的时候,让面试官觉得你不同于那些妖艳贱货。
定义
ArrayList
本身直接继承AbstractList
,实现List
、 RandomAccess
、 Cloneable
、 Serializable
接口。
- RandomAccess:标记接口,标注该类可随机访问
- Cloneable:可以克隆拷贝
- Serializable:可序列化
构造函数
在使用 ArrayList
的时候,都是用使用构造函数进行初始化的。分别来介绍一下它的几个构造函数。
无参构造
- 使用
- 源码
elementData
和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
都是成员变量,一起来看看:
看到上述无参构造方法的源码,可以知道当我们 new 一个无参 ArrayList
的时候,内部使用了一个空的数组赋值给了用于保存数据的数组。
ArrayList(int initialCapacity)
- 使用
- 源码
该构造函数可以传入一个 int 值,如果该值大于 0,则创建一个长度为该值的数组;
如果该值等于 0,则使用一个空数组;如果该值小于 0,则抛出IllegalArgumentException
异常。
ArrayList(Collection c)
- 使用
- 源码
该构造函数可以传入一个 Collection
,将传入的集合转换为数组,然后将该数组赋值给成员变量elementData
,将elementData
的长度赋值给成员变量 size
,判断是否不等于 0 ,如果传入的集合中有元素该判断是成立的,然后再判断elementData.getClass() != Object[].class
,这不会成立的。所以结束方法。如果等于零则将EMPTY_ELEMENTDATA
赋值给 elementData
。
在这段源码中又有两个新的成员变量:
add方法
add(E e)
在集合末端添加元素
- 源码
add
方法中就三行代码,代码很简单,就只分析 ensureCapacityInternal
方法。
- ensureCapacityInternal
在方法中调用了ensureExplicitCapacity
方法,而ensureExplicitCapacity
方法调用了calculateCapacity
方法。
- calculateCapacity
判断元素集合是不是等于空数组,如果是则返回 DEFAULT_CAPACITY
。DEFAULT_CAPACITY
的默认值为 10。使用无参构造方式进行实例化ArrayList
,ArrayList
默认的容量长度为 10。
- ensureExplicitCapacity
将 10 传入ensureExplicitCapacity
方法,而这时elementData.length
的长度为0
,所以会调用扩容方法。
- grow
grow
这个方法是ArrayList
中最重要的方法。都说ArrayList
是基于数组实现的,但又不同于数组,ArrayList
可以动态扩容,就是使用grow
实现的。
如果我们使用无参构造方法进行实例化 ArrayList
,即使只添加一个元素都会进行扩容操作。
Arrays.copyOf
单独拎出来实践一下。
是不是对此方法有点理解了。
add(int index, E element)
在指定下标添加元素
- 源码
相对于add(E e)
方法,指定index
下标添加元素的 add
方法稍微复杂点。
- 检查下是否越界
- 是否需要扩容
- 进行数组元素移动
- 将新元素放置到空的
elementData[index]
位置 size
自加
进行元素移动的时候,调用了System.arraycopy
方法,这是一个被 native
修饰的静态方法。
参数含义如下:
- src:源数组
- srcPos:源数组中要复制的起始位置,从哪个下标开始复制。
- dest:目标数组
- destPos:目标数组放置复制数据的起始位置,从哪个下标开始放。
- length:复制的长度
总结
通过阅读上述两个add
的源码,可以总结出以下几点:
- 使用无参构造方法实例化
ArrayList
,默认的容量为 10。 - 扩容之后的容量为旧容量的1.5倍。
- 如果元素个数确定的情况下,尽量使用
ArrayList(int initialCapacity)
构造方法进行实例化ArrayList
,可以减少扩容次数。 - 指定下标插入元素时,需要进行元素的移动。而直接添加元素是放置在元素的末端。
ArrayList
的元素可以为null
。
还有两个可以添加元素的方法addAll
,方法跟上面逻辑差不多,就不再单独拎出来了,各位小伙伴可以自己去看看。相信我,很简单。
remove 方法
remove(int index)
删除指定下标的元素,时间复杂度为 o(1)。
- 源码
- 检查下标是否越界,越界则抛出
IndexOutOfBoundsException
- 获得指定下标的元素
- 计算出需要移动的次数,如果大于 0 ,说明要删除的元素不是数组中最后一位,则调用
System.arraycopy
方法进行元素移动。
- 将最后一个元素赋值为
null
, 可以被gc回收。 - 返回被删除的元素
remove(Object o)
按照传入的元素删除,删除匹配到的第一个元素。时间复杂度为 o(n)。
- 源码
- 当传入元素的值为空时,遍历数组删除第一个为空的元素,并返回 true。
- 当传入元素的值不为空时,遍历数组删除第一个与传入元素值相等的元素,并返回 true。
- 上述两个逻辑都不符合,返回
false
。
注意:remove 方法并不会改变元素容量的长度,即 elementData.length,只会修改 size 的值
。比如 elementData
中有 15 个元素,在调用 remove
方法之前 elementData.length = 15,size = 15
,调用 remove
方法之后elementData.length = 15,size = 14
。
set 方法
修改指定下标元素的值。
- 检查下标是否越界,越界则抛出
IndexOutOfBoundsException
- 获得指定下标的元素
- 将传入的值放到指定的 index 上
- 将
oldValue
返回
get方法
返回指定下标的元素,时间复杂度为 o(1)。
- 检查下标是否越界,越界则抛出
IndexOutOfBoundsException
- 获得指定下标的元素
clear 方法
将数组中所有元素置为 null,但并不会改变数组容量的大小;
并将 size
置为 0。
contains 方法
判断集合中是否包含指定元素
方法内部调用 indexOf
方法,
- 当传入元素的值为空时,遍历数组,返回第一个等于 null 的元素下标。
- 当传入元素的值不为空时,遍历数组,返回第一个与传入元素值相等的元素下标。
- 上述两个逻辑都不符合,返回 -1。
trimToSize 方法
能有效的节约内存。将数组容量的大小变为实际数据的个数,例如:数组容量为10,但只有前三个元素有值,其他为空,那么调用该方法之后,数组的容量变为3。
- 源码
ArrayList 与 Vector 有什么区别
相同点
都是基于数组实现,保证元素的有序行,可以基于下标查询,并且默认容量都是 10。
不同点
- 1、ArrayList 是线程不安全的,而 Vector 中的方法都被
synchronized
标注,是线程安全的。 - 2、在进行扩容时,ArrayList 是扩容 1.5 倍,而 Vector 是默认是扩容 2 倍,但也可以指定扩容数量。
