目录
一、概述
ArrayList
是基于数组实现,底层的数据结构是顺序表(物理内存上连续),并且支持动态扩容。因而相较于数组而言,因为其支持自动扩容,成为我们开发中最常用的集合之一。
类图
从以上类图中我们能得知:ArrayList
实现了四个接口和继承了一个抽象类:
List
接口,主要提供数组的添加、删除、修改、遍历等操作Cloneable
接口,表示ArrayList支持克隆RandomAccess
接口,表示ArrayList支持快速地随机访问Serializable
接口,表示ArrayList支持序列化功能AbstractList
抽象类,主要提供迭代遍历等操作
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// ......
}
二、源码解读
注:不同jdk版本的源码会有一定差异,不过大致上是相同的,我使用的是 openjdk version “1.8.0_342”。
1、成员变量
//Default initial capacity.数组默认大小
private static final int DEFAULT_CAPACITY = 10;
// 空队列
private static final Object[] EMPTY_ELEMENTDATA = {};
// 如果使用默认构造方法,则默认对象内容是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 用于存储数据
transient Object[] elementData;
// 当前队列有效数据长度
private int size;
// 数组最大值
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
在ArrayList
的源码中,主要有上述的几个成员变量:
elementData
:动态数组,也就是我们存储数据的核心数组(ArrayList底层就是使用的数组)DEFAULT_CAPACITY
:数组默认长度size
:记录有效数据长度,size()
方法直接返回该值MAX_ARRAY_SIZE
:数组最大长度,如果扩容超过该值,则设置长度为Integer.MAX_VALUE
EMPTY_ELEMENTDATA
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
:两个空数组
2、构造方法
ArrayList
中提供了三种构造方法:
ArrayList()
:指向全局空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
ArrayList(int initialCapacity)
:如果初始容量大于0,则创建指定长度的数组。如果等于0,则指向全局空数组EMPTY_ELEMENTDATA
ArrayList(Collection<? extends E> c)
:将集合c转为数组,如果数组为空,则指向EMPTY_ELEMENTDATA
第一种构造函数 ArrayList()
/**
* Constructs an empty list with an initial capacity of ten.
* 构造一个初始容量为10的空列表。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
从源码中的注释中所知:此构造函数是构造一个初始容量为10的空列表。细心一点可以发现:从上文可知DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是一个空的数组对象。
不过这里并不是一个错误,而是确实了一些补充描述,当为指定初始化大小的时候,ArrayList
首先是初始化一个空的数组,也就是此DEFAULTCAPACITY_EMPTY_ELEMENTDATA
。但是在首次添加元素的时候,ArrayList
会初始化一个容量为10的数组(后文会提到)。
这样做的目的是为了节省内存空间,如果在一些场景下某数组并未使用的话,那么不会造成不必要的空间浪费。
第二种构造函数 ArrayList(int initialCapacity)
根据指定大小初始化ArrayList
中数组的大小,根据传入的initialCapacity
的值去初始化容量创建elementData
数组。(注意:在创建ArrayList时,我们应该尽可能使用此构造函数创建,这样能够避免容量浪费)
如果初始容量大于0,则创建指定长度的数组。如果等于0,则指向全局空数组EMPTY_ELEMENTDATA
源码如下:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 初始化容量大于0,创建Object数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 初始化容量为 0 时,使用 EMPTY_ELEMENTDATA 对象
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 容量参数异常
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
第三种构造函数 ArrayList(Collection<? extends E> c)
此构造方式是传入集合c
作为ArrayList
中的elementData
public ArrayList(Collection<? extends E> c) {
// 把集合c转换成数组数组对象,再赋值给此中转数组a
Object[] a = c.toArray();
// 将a的长度赋值给size,并判断是否不等于为0
if ((size = a.length) != 0) {
// 这里没太明白为什么需要一次校验 懂的大佬麻烦评论区解读一下
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
3、添加元素类方法
添加元素类方法核心主要有以下四个:
public boolean add(E e)
:在数组后面顺序新增一个元素public void add(int index, E element)
:在指定下标位置添加元素public boolean addAll(Collection<? extends E> c)
:添加一个集合c的所有元素public boolean addAll(int index, Collection<? extends E> c)
:在指定下标位置添加一个集合c的所有元素
第一种 public boolean add(E e)
在数组后面顺序新增一个元素
public boolean add(E e) {
// 确保内部容量
ensureCapacityInternal(size + 1);
// 这个操作相当于 element[size] = e, size++
// 此处是给elementData数组
elementData[size++] = e;
// 返回添加成功
return true;
}
1、在这里我们重点关注ensureCapacityInternal(size + 1)
方法,size
变量是当前队列有效数据长度。此方法是为了确保有足够的内部容量来存储新的元素,如果当前容量不足这个(size + 1)的大小,那么会触发扩容机制。
继续向下解读,看看此ensureCapacityInternal
方法:
private void ensureCapacityInternal(int minCapacity) {
// 确保数组容量,如不足则触发扩容机制
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
2、还是嵌套调用方法,首先是调用calculateCapacity(elementData, minCapacity)
方法,再将此方法的返回值当做ensureExplicitCapacity
方法的参数传入。elementData
即是用于存储数据的数组(上文也有说到,下文不再赘述),minCapacity
意为:最小容量(即是size + 1)。
继续向下解读:
// calculateCapacity 意为:计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果elementData数组为空(DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个空数组且上文有提到)
// 则返回DEFAULT_CAPACITY 与 minCapacity其中最大者
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// DEFAULT_CAPACITY (默认容量)定义:private static final int DEFAULT_CAPACITY = 10;
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 如果不是则直接返回需要的最小容量
return minCapacity;
}
3、回到ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
方法。
在上文中所学习到calculateCapacity(elementData, minCapacity)
返回的两种情况:1是 DEFAULT_CAPACITY
与 minCapacity
其中最大者 2是 返回需要的最小容量。
继续向下解读:
private void ensureExplicitCapacity(int minCapacity) {
// 这是一个自增操作,增加了列表的修改计数。这个计数是用来跟踪列表被修改了多少次,这对于一些并发控制是非常有用的。(可以暂时不用关注)
modCount++;
// 最小容量 减去 elementData长度 是否>0
// 如果大于0 则触发扩容机制,如果不大于0 则直接返回
// 个人觉得写为:minCapacity > elementData.length 更好理解
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
4、最核心的扩容方法
继续向下解读:
/**
* 扩容
* 旧容量经过运算扩展为1.5后与最小容量minCapacity进行比较
* 如果大于则采用旧容量扩展1.5倍后的大小,否则采用最小容量minCapacity
*/
private void grow(int minCapacity) {
// 旧容量 == elementData数组长度
int oldCapacity = elementData.length;
// 新容量 == 旧容量 + (旧容量向右移1位)
// 粗糙地说 大约是原容量的1.5倍数
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果计算出的新容量 还是小于 指定的最小容量 则将此最小容量赋给新容量
// 个人觉得写为:写为 newCapacity < minCapacity 更好理解
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量超过了数组的最大限制 则调用hugeCapacity(minCapacity)继续扩容
// private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 根据新的容量 newCapacity 来创建一个新的数组,并将原数组 elementData 的元素复制到新数组中。
elementData = Arrays.copyOf(elementData, newCapacity);
}
5、hugeCapacity(minCapacity)
继续向下解读:
private static int hugeCapacity(int minCapacity) {
// 判断入参minCapacity是否小于0
if (minCapacity < 0)
throw new OutOfMemoryError();
// 如果最小容量 > MAX_ARRAY_SIZE 则返回 Integer.MAX_VALUE
// 如果最小容量 < MAX_ARRAY_SIZE 则返回 MAX_ARRAY_SIZE
// private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
// public static final int MAX_VALUE = 0x7fffffff;
// private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
一句话概括:首先是通过calculateCapacity(elementData, minCapacity)
方法计算出本次新增元素所需要的最小容量;再者再判断if (minCapacity - elementData.length > 0)
最小容量是否大于数组长度,如果大于则触发扩容机制;最后将原有数组的元素拷贝到新数组上,然后往后追加新的元素。
第二种 public void add(int index, E element)
在指定下标位置添加元素
public void add(int index, E element) {
// rangeCheckForAdd 意为:范围添加检查
// 是一个辅助方法,用于检查插入位置的索引是否合法,即是否在有效范围内。如果索引不合法,会抛出一个 IndexOutOfBoundsException 异常。
rangeCheckForAdd(index);
// 上文已经介绍过 确保数组容量,如不足则触发扩容机制
ensureCapacityInternal(size + 1);
// 使用 System.arraycopy 方法进行数组元素的移动。该方法会将从 index 开始的元素向后移动一位,为新元素腾出插入位置。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将 element 插入到指定的 index 位置,即 elementData[index] = element。
elementData[index] = element;
// 增加 size 的计数,表示列表的大小增加了一个元素
size++;
}
一句话概括:首先校验插入位置参数的合法性;其次确保数组容量,如不足则触发扩容机制;再者使用System.arraycopy 方法进行数组元素的移动;最后插入元素并进行size++操作。
第三种 public boolean addAll(Collection<? extends E> c)
添加一个集合c的所有元素
public boolean addAll(Collection<? extends E> c) {
// 将集合 c 转化为一个数组 并赋值给临时中转数组 a
Object[] a = c.toArray();
// 获取中转数组 a 的长度
int numNew = a.length;
// 上文已经介绍过 确保数组容量,如不足则触发扩容机制
// 注意一下的是 传入的最小容量是 size + numNew
ensureCapacityInternal(size + numNew);
// 将数组 a 中的元素拷贝到 elementData 数组中。拷贝的起始位置是 size,即将元素添加到列表的尾部
System.arraycopy(a, 0, elementData, size, numNew);
// 增加 size 的计数,表示列表的大小增加了 numNew 个元素
size += numNew;
// 返回一个布尔值,表示是否有新增的元素被添加到了列表中。
// 如果 numNew 不为 0,即集合 c 非空且有元素被添加到列表中,则返回 true,否则返回 false
return numNew != 0;
}
这里不过多赘述,同上述的添加操作类似。
第四种 public boolean addAll(int index, Collection<? extends E> c)
在指定下标位置添加一个集合c的所有元素
public boolean addAll(int index, Collection<? extends E> c) {
// 同上 不过多赘述
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
// 计算要移动的元素数量 numMoved,即从插入位置 index 开始到列表末尾的元素数量
int numMoved = size - index;
// 如果 numMoved 大于 0,表示需要将一部分元素向后移动,以为新元素腾出插入位置
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
// 将数组 a 中的元素拷贝到 elementData 数组中的指定位置 index。
System.arraycopy(a, 0, elementData, index, numNew);
// 增加 size 的计数,表示列表的大小增加了 numNew 个元素
size += numNew;
// 表示是否有新增的元素被添加到了列表中。如果 numNew 不为 0,即集合 c 非空且有元素被添加到列表中,则返回 true,否则返回 false
return numNew != 0;
}
3、删除元素类方法
删除元素类方法核心主要有以下四个:
public E remove(int index)
:移除指定下标位置的元素,并返回该元素public boolean remove(Object o)
:移除指定元素,并返回是否成功public boolean removeAll(Collection<?> c)
:批量移除集合与集合c中所共有的元素protected void removeRange(int fromIndex, int toIndex)
:批量移除指定的多个元素
第一种 public E remove(int index)
移除指定下标位置的元素,并返回该元素
public E remove(int index) {
// 校验此index索引是否在合理范围内,如果不在,这个方法将抛出一个IndexOutOfBoundsException
rangeCheck(index);
modCount++;
// 获取了指定索引处的元素值,并将其赋值给oldValue
// elementData方法其实就是 return (E) elementData[index]
E oldValue = elementData(index);
// 每删除一个元素,都需要对原有数组进行移动,因此这里也能表现出ArrayList,不适用于删除操作较多的场景
// 计算需要移动的元素数量
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将列表的大小减一,并将最后一个元素的值置为null。
// 这是为了垃圾回收
// 个人觉得写为:写为 elementData[size] = null; size --; 更好理解
elementData[--size] = null;
// 返回删除的元素
return oldValue;
}
一句话概括:首先涉及到index下标的,那肯定是对此index校验参数合法性;其次对元素进行移动;最后返回被删除的元素。
注:ArrayList删除操作,都会伴随着数组元素的移动操作,当数组较长的时候对性能的消耗较大,因此这里也能表现出ArrayList,不适用于删除操作较多的场景。
第二种 public boolean remove(Object o)
:
移除指定元素,并返回是否成功
public boolean remove(Object o) {
// 情况1:o的值为null
if (o == null) {
// 遍历elementData
for (int index = 0; index < size; index++)
if (elementData[index] == null) {\
// 匹配成功 使用fastRemove方法删除此元素
fastRemove(index);
return true;
}
}
// 情况2:o的值不为null
else {
// 遍历elementData
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
// 匹配成功 使用fastRemove方法删除此元素
fastRemove(index);
return true;
}
}
return false;
}
继续向下解读:
// 真实执行删除操作的方法·
private void fastRemove(int index) {
modCount++;
// 计算需要移动的元素数量
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;
}
第三种 public boolean removeAll(Collection<?> c)
:
批量移除集合与集合c中所共有的元素
public boolean removeAll(Collection<?> c) {
// 对集合c进行null校验 如果为null 抛出空指针异常
Objects.requireNonNull(c);
// 调用真实的删除方法
return batchRemove(c, false);
}
继续向下解读:
/**
* complement 补充信息,这个方法在removeAll和retainAll中都被使用到,该字段用以确认是remove还是retain
* 当complement为false的时候,用以删除 当complement为true的时候,用以保留
* 这个方法的设计非常灵性 值得一学
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
// 定义一个变量指向 elementData
final Object[] elementData = this.elementData;
// r 是遍历elementData的参数
// w 是记录删除后的index
int r = 0, w = 0;
// modified标志,如果进行了修改则返回true,否则返回false
boolean modified = false;
try {
// 遍历 elementData
for (; r < size; r++)
// removeAll是false retainAll的是true
// 如果集合c中包含elementData[r] 则为true 反之false
// 情况1、如果为true 则不等于complement,进入下一次循环
// 情况2、如果为false 则等于complement,进入循环体
if (c.contains(elementData[r]) == complement)
// 将集合c中不包含elementData[r]的元素(也就是不需要移除的元素) 赋值给 elementData[w++]
// elementData[w] = elementData[r]; w++;
elementData[w++] = elementData[r];
} finally {
// 如果r != size, 证明上面的遍历提前结束了 意思就是遇到了异常情况
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// 如果 w != size 则表示有移除的元素
if (w != size) {
// 清空数组后面的无用index的元素 垃圾回收
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
第四种 protected void removeRange(int fromIndex, int toIndex)
:
批量移除指定的多个元素,注意不包括 toIndex位置的元素
/**
* 移除 [fromIndex, toIndex) 范围内的元素
* @param fromIndex 包括
* @param toIndex 不包括
*/
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
// toIndex后面的元素都需要被移动到前面
int numMoved = size - toIndex;
// 因此可以看到为啥子右边是),因为它并没有 + 1
System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved);
// 计算新的数组长度
int newSize = size - (toIndex - fromIndex);
// 数组往前移动后,后面所有空闲的位置都设为null
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
// 更新新的长度
size = newSize;
}
三、总结
本文主要解读了ArrayList 属性、构造函数、新增元素、删除元素的源码。其他方法的源码大同小异,感兴趣的同学自行阅读。
属性
ArrayList核心属性有2个:
Object[] elementData
:底层数组 用来存储元素int size
:用来记录数组的有效元素个数
构造方法
构造方法有三个
- 无参构造:
ArrayList()
- 根据指定大小初始化
ArrayList
中数组的大小:ArrayList(int initialCapacity)
- 传入集合
c
作为ArrayList
中的elementData
:ArrayList(Collection<? extends E> c)
注:无参构造方法首次初始化的容量是0,只有在第一次添加元素的时候,出发扩容机制。容量变为10.
添加元素方法
ArrayList
是支持动态扩容的数组,所以新增元素的时候会确保数组的容量是否充足,如果不够的话会触发扩容机制。
删除元素方法
ArrayList
每次删除元素的时候都会伴随着大量数据的移动,因此我们能看出 ArrayList
并不是那么适用于新增、删除比较频繁的场景。
扩容
ArrayList添加元素前会检查数据容量,如果不足的话会触发扩容机制
- 在使用无参构造器初始化的时候,首次添加元素时会直接扩容到10的容量
- 其他情况下,会直接扩容到旧容量的大约1.5倍,如果最小容量 大于 本次1.5倍的扩容,那么本次扩充的容量会变为本次最小容量。
加餐1:ArrayList和LinkedList的区别
- 1、数据结构:
ArrayList
和LinkedList
都是线性结构,都继承自List
接口。不过LinkedList
还实现了Deque
接口,是基于链表的栈或队列。与之对应的是ArrayDeque
是基于数组的栈或队列。 - 2、线程安全:
ArrayList
和LinkedList
都是线程不安全。 - 3、底层实现:在底层实现上,
ArrayList
是基于动态数组,而LinkedList
是基于双向链表。导致它们很多区别都是因为底层实现的不同引发的。比如说:- 遍历速度:数组是一块连续的内存空间,基于局部性原理能够更好地命中CPU缓存行,而链表是离散的内存空间,对缓存行不友好。
- 访问速度:数组是一块连续的内存空间,支持O(1)时间复杂度随机访问,而链表需要O(n)时间复杂度查找元素。
- 添加与删除操作:在数组中进行添加或删除操作,平均时间复杂度是 O(n),因为牵涉到移动元素。而链表的添加或删除操作本身只是修改引用指向,只需要O(1)的时间复杂度(如果考虑查询节点的时间,复杂度分析上依然是O(n))
加餐2:为什么 ArrayList
属性要区分出 2 个空数组?
ArrayList()
:指向全局空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
ArrayList(int initialCapacity)
:如果初始容量大于0,则创建指定长度的数组。如果等于0,则指向全局空数组EMPTY_ELEMENTDATA
ArrayList(Collection<? extends E> c)
:将集合c转为数组,如果数组为空,则指向EMPTY_ELEMENTDATA
首先:无参构造函数应该使用默认行为,也就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
其次:设置初始化容量为0的数组,是开发者的意图,就是为了设定一个容量0的数组,不应该使用默认容量为0的数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,而是应该使用全局空数组EMPTY_ELEMENTDATA