深入浅出 JAVA 之集合 - ArrayList
文章目录
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。
系统环境
- JDK 版本: OpenJDK 8
一、ArrayList 简介
ArrayList 是 Java 中常见的一种动态数组实现,它继承自 AbstractList 类并实现了 List 接口。ArrayList 是一种基于动态数组的数据结构,它可以随意扩展容量,支持快速随机访问,插入和删除元素也比较高效。在使用上,ArrayList 与普通数组的用法基本相同,可以通过下标来访问元素,并提供了一系列操作方法,如添加、删除、查找等。
二、ArrayList 主要特性
ArrayList 的主要特性如下:
- 可变性: ArrayList 中的元素可以被修改、添加或删除。
- 顺序访问: ArrayList 中的元素是按照添加顺序存储的,因此可以通过迭代器来对元素进行顺序访问。
- 随机访问: ArrayList 中的元素是存储在连续的内存空间中的,因此可以通过下标进行随机访问,访问速度较快。
- 动态扩容: ArrayList 可以根据需要动态地扩展数组的大小。当向 ArrayList 中添加元素时,如果当前的数组大小不够存储元素,ArrayList 会自动进行扩容,以容纳更多的元素。
需要注意的是,由于 ArrayList 是基于数组实现的,因此在进行添加或删除操作时,需要对数组进行拷贝和移动,因此在频繁进行添加或删除操作时,性能会有所下降。此外,由于 ArrayList 是非线程安全的,因此在多线程环境下需要进行同步或使用线程安全的 List 类。
三、ArrayList 的常见操作方法有哪些
以下是 ArrayList 常见操作方法的介绍:
- add(E e): 在 ArrayList 的末尾添加一个元素 e。
- add(int index, E e): 在 ArrayList 的指定位置 index 插入一个元素 e。
- get(int index): 获取 ArrayList 中指定位置 index 的元素。
- set(int index, E e): 将 ArrayList 中指定位置 index 的元素替换为 e。
- remove(int index): 删除 ArrayList 中指定位置 index 的元素。
- size(): 获取 ArrayList 中元素的个数。
- clear(): 清空 ArrayList 中所有的元素。
- isEmpty(): 判断 ArrayList 是否为空。
- contains(Object o): 判断 ArrayList 是否包含元素 o。
- indexOf(Object o): 获取 ArrayList 中元素 o 的第一次出现位置的下标。
- lastIndexOf(Object o): 获取 ArrayList 中元素 o 的最后一次出现位置的下标。
- toArray(): 将 ArrayList 转化为数组。
四、ArrayList 的优缺点是什么
ArrayList 的优点和缺点如下:
ArrayList 的优点:
- 随机访问: ArrayList 的底层是一个数组,因此它支持随机访问,可以通过下标进行快速定位元素。
- 高效的插入和删除操作: 对于在末尾进行添加操作的场景,ArrayList 的效率非常高。在数据量较小的情况下,插入和删除操作也比较高效。
- 自动扩容: ArrayList 在初始化时可以指定初始容量,当数组空间不足时,会自动扩容为原来的 1.5 倍,从而可以在不频繁进行扩容操作的同时,更好地利用内存空间。
ArrayList 的缺点:
- 插入和删除操作效率低: 对于插入和删除操作,如果不在末尾进行操作,ArrayList 的效率会比较低。因为这涉及到数组元素的移动操作,如果元素数量很大,移动操作的代价就会比较高。
- 不支持自动收缩: 虽然 ArrayList 支持自动扩容,但是它并不支持自动收缩,因此如果不手动调用 trimToSize() 方法,内存空间可能会浪费。
- 多线程不安全: ArrayList 不是线程安全的,如果需要在多线程环境下使用,需要额外考虑线程安全问题。
五、ArrayList 如何保证线程安全
在多线程环境下,如果多个线程同时对 ArrayList 进行修改,可能会导致不一致的结果。为了保证线程安全,可以使用以下几种方式:
- 使用 Collections 工具类提供的线程安全的 List 方法,如 Collections.synchronizedList(),将 ArrayList 转化为线程安全的 List。这种方法的缺点是效率较低,因为每次对 List 进行修改时都需要获得锁。
- 使用并发包中提供的线程安全 List,如 CopyOnWriteArrayList、ConcurrentLinkedQueue 等。
- 在多线程环境下,可以使用锁机制对 ArrayList 进行加锁,保证同一时间只有一个线程能够对 ArrayList 进行修改。这种方式比较灵活,可以根据实际情况进行加锁,但需要开发者自己管理锁。
六、ArrayList 底层结构
ArrayList 底层结构是一个动态数组,它是通过 Object[] 数组来存储元素的,如下:
1transient Object[] elementData;
在初始化时 ArrayList 会创建一个初始大小为 10 的数组来存储元素,当向 ArrayList 中添加元素时,如果当前数组已满 ArrayList 会自动进行扩容,将原有数组中的元素复制到新的数组中,并且将新元素添加到新数组中。通常情况下 ArrayList 的扩容大小为原始数组的一半。
需要注意的是 ArrayList 的底层结构是一个数组,因此在进行添加或删除操作时,需要对数组进行拷贝和移动操作,这可能会影响其性能。此外,由于数组的大小是固定的,因此在使用 ArrayList 时需要注意容量的大小,以免浪费内存或导致扩容过程中的性能问题。
七、ArrayList 源码 - 属性
1/**
2 * 默认初始容量
3 */
4private static final int DEFAULT_CAPACITY = 10;
5
6/**
7 * 创建空 ArrayList 实例所使用的空数组
8 */
9private static final Object[] EMPTY_ELEMENTDATA = {};
10
11/**
12 * 创建空 ArrayList 实例,且设置容量为默认大小 10 的时候所使用的空数组
13 */
14private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
15
16/**
17 * 存储集合中元素的数组 (使用transient关键字修饰的,因此在进行序列化时该属性不会被序列化)
18 */
19transient Object[] elementData;
20
21/**
22 * 集合的大小 (即集合中的元素数量)
23 */
24private int size;
上述过程中的 EMPTY_ELEMENTDATA
和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
常量都是一个空数组,它们的作用都是为了优化性能 ArrayList 类的性能,当我们创建一个新的空的 ArrayList 对象时,会使用 EMPTY_ELEMENTDATA
或者 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
数组来作为 elementData 数组,这样就可以避免不必要再重头开始创建一个新数组了,省去了内存分配和数组复制的操作,从而提高程序的性能。
这俩个属性所用在的地方和作用有所区别,比如:
- 当创建 ArrayList 集合且指定初始容量,设置初始容量为 0,这时就会使用
EMPTY_ELEMENTDATA
数组,后续在添加元素时不会进行扩容操作。 - 当创建 ArrayList 集合且不指定初始容量时,就默认使用
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
数组,后续在添加元素时会先执行扩容操作对当 ArrayList 集合进行扩容。
八、ArrayList 源码 - 关键方法
8.1 构造方法 ArrayList
(1) 无参构造方法
1/**
2 * 无参构造方法,创建一个空数组
3 */
4public ArrayList() {
5 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
6}
(2) 支持配置集合容量的构造方法
1/**
2 * 支持配置初始容量的构造方法,创建一个指定容量的空数组
3 *
4 * @param initialCapacity 初始容量
5 * @throws IllegalArgumentException 如果设置的初始容量为负数,则抛出非法参数异常
6 */
7public ArrayList(int initialCapacity) {
8 // 如果设置的初始容量大于 0,则创建一个 initialCapacity 长度的 Object 数组赋值给 ArrayList 对象的 elementData 属性
9 if (initialCapacity > 0) {
10 this.elementData = new Object[initialCapacity];
11 }
12 // 如果设置的初始容量等于 0,则将预定义的空数组 EMPTY_ELEMENTDATA 数组赋给 ArrayList 对象的 elementData 属性
13 else if (initialCapacity == 0) {
14 this.elementData = EMPTY_ELEMENTDATA;
15 }
16 // 如果设置的初始容量小于 0,则抛出非法参数异常
17 else {
18 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
19 }
20}
(3) 支持根据已有 Collection 集合创建新集合的构造方法
1/**
2 * 支持根据已有数组创建新集合的构造方法
3 *
4 * @param c 已有的 Collection 集合
5 * @throws NullPointerException 如果传入的集合为空,则抛出空指针异常
6 */
7public ArrayList(Collection<? extends E> c) {
8 // 将输入的 Collection 集合 c 转换为一个数组赋给 elementDat 变量
9 elementData = c.toArray();
10 // 将 elementData 数组长度作为集合大小,然后判断大小是否不为 0
11 if ((size = elementData.length) != 0) {
12 // 检查 elementData 的类型是否为 Object[] 数组
13 if (elementData.getClass() != Object[].class) {
14 // 如果是则将使用 Arrays.copyOf() 方法创建一个新数组,并将其类型设置为 Object[]
15 elementData = Arrays.copyOf(elementData, size, Object[].class);
16 }
17 } else {
18 // 将 EMPTY_ELEMENTDATA 空数组作为集合的 elementData 数组,这里 EMPTY_ELEMENTDATA 是静态的提前
19 // 创建好的数组,这样避免重新创建数组,省去了内存分配和数组复制的操作,从而提高程序性能。
20 this.elementData = EMPTY_ELEMENTDATA;
21 }
22}
8.2 末尾位置插入元素方法 add(E e)
1/**
2 * 在集合末尾插入一个元素
3 *
4 * @param e 插入的元素
5 * @return 插入成功则返回 true
6 */
7public boolean add(E e) {
8 // 确保 ArrayList 中有足够的容量来存储新元素,如果没有足够的容量,就会通过扩容机制增加 ArrayList 的容量
9 ensureCapacityInternal(size + 1);
10 // 将元素插入到集合数组的末尾位置,然后使集合大小 size+1,以便下次调用 add 方法时可以在正确的位置添加新元素
11 elementData[size++] = e;
12 // 默认返回 true
13 return true;
14}
8.3 指定位置插入元素方法 add(int index, E element)
add(int index, E element)
1/**
2 * 指定位置插入元素方法
3 *
4 * @param index 插入的索引位置
5 * @param element 要插入的元素
6 * @throws IndexOutOfBoundsException 数组越界异常
7 */
8public void add(int index, E element) {
9 // 检查指定的索引位置是否超出了当前集合可以存储的范围
10 rangeCheckForAdd(index);
11 // 调用 ensureCapacityInternal() 方法确保集合内部数组具有足够的容量来存储新添加的元素
12 ensureCapacityInternal(size + 1);
13 // 将指定索引及其后面的元素向右移动一个位置,以为新元素腾出空间
14 System.arraycopy(elementData, index, elementData, index + 1, size - index);
15 // 将新元素添加到指定的索引位置上
16 elementData[index] = element;
17 // 集合大小+1
18 size++;
19}
ensureCapacityInternal(int minCapacity)
1/**
2 * 确保 ArrayList 中有足够的容量来存储新元素
3 *
4 * @param minCapacity 集合存储元素所需要的最小容量
5 */
6private void ensureCapacityInternal(int minCapacity) {
7 // 调用 calculateCapacity 方法计算 ArrayList 集合内部数组的最小容量
8 // 调用 ensureExplicitCapacity() 方法,将 calculateCapacity 方法计算的最小容量作为参数
9 // 如果 ArrayList 内部数组的容量小于指定的最小容量,则会通过扩容机制增加 ArrayList 的容量,以确保容量足够存储元素
10 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
11}
calculateCapacity(Object[] elementData, int minCapacity)
1/**
2 * 计算 ArrayList 集合内部数组的最小容量
3 *
4 * @param elementData 集合中存储元素的数组
5 * @param minCapacity 集合存储元素所需要的最小容量
6 * @return 计算后的集合所需的最小容量
7 */
8private static int calculateCapacity(Object[] elementData, int minCapacity) {
9 // 检查 elementData 是否为静态 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组
10 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
11 // 如果是则说 elementData 是空数组,则将最小容量和默认容量 DEFAULT_CAPACITY 进行比较,
12 // 将其中较大的值返回作为计算后的最小容量。
13 return Math.max(DEFAULT_CAPACITY, minCapacity);
14 }
15 // 如果不是则说明 elementData 不是空数组,则直接返回指定的最小容量
16 return minCapacity;
17}
ensureExplicitCapacity(int minCapacity)
1/**
2 * 确保 ArrayList 内部数组具有指定的最小容量
3 *
4 * @param minCapacity 集合存储元素所需要的最小容量
5 */
6private void ensureExplicitCapacity(int minCapacity) {
7 // 记录该 ArrayList 对象的结构修改次数,以便在遍历该对象时检查它是否被其他代码修改过。
8 modCount++;
9
10 // 判断集合存储元素所需要的最小容量是否大于集合 elementData 数组当前的容量,如果是则进行库容操作
11 if (minCapacity - elementData.length > 0) {
12 // 进行扩容操作
13 grow(minCapacity);
14 }
15}
8.4 集合进行扩容操作 grow(int minCapacity)
1/**
2 * 集合扩容,增加 ArrayList 内部数组的容量以便存储更多的元素。
3 *
4 * @param minCapacity 所需的最小容量
5 */
6private void grow(int minCapacity) {
7 // 记录集合旧容量到 oldCapacity 变量
8 int oldCapacity = elementData.length;
9 // 计算集合新容量到 newCapacity 变量,新容量大小为旧容量的 1.5 倍
10 int newCapacity = oldCapacity + (oldCapacity >> 1);
11 // 检查计算后的新容量是否大于等于所需的最小容量,如果是则直接使用新容量,否则将最小容量作为新容量
12 if (newCapacity - minCapacity < 0) {
13 newCapacity = minCapacity;
14 }
15 // 如果计算后的新容量大于 MAX_ARRAY_SIZE,会调用 hugeCapacity() 方法计算新的容量。
16 // 这是为了避免数组大小无限制地增长,因为数组大小不能超过 MAX_ARRAY_SIZE,否则会抛出 OutOfMemoryError 错误。
17 if (newCapacity - MAX_ARRAY_SIZE > 0) {
18 newCapacity = hugeCapacity(minCapacity);
19 }
20 // 将当前 ArrayList 内部数组 elementData 复制到一个新的数组中,其大小为计算后的新容量 newCapacity
21 elementData = Arrays.copyOf(elementData, newCapacity);
22}
具体来说该方法用于计算出集合所需的新的容量,确保容量足够存储所需的最小容量,避免数组大小超过 MAX_ARRAY_SIZE
值,并使用 Arrays.copyOf()
方法将当前内部数组 elementData
复制到一个新的数组中,以便存储更多的元素。
8.5 删除指定位置元素方法 remove(int index)
1/**
2 * 删除指定位置元素
3 *
4 * @param index 要删除元素位置的索引
5 * @return 删除的元素
6 * @throws IndexOutOfBoundsException 索引越界异常
7 */
8public E remove(int index) {
9 // 检查索引是否越界
10 rangeCheck(index);
11 // 记录修改次数
12 modCount++;
13 // 获取待删除元素的值
14 E oldValue = elementData(index);
15 // 计算待移动的元素个数
16 int numMoved = size - index - 1;
17 // 如果判断 numMoved 是否大于 0,如果是则说明有元素需要移动,则将数组元素向左移动
18 if (numMoved > 0) {
19 // 数组元素向左移动
20 System.arraycopy(elementData, index+1, elementData, index, numMoved);
21 }
22 // 将数组最后一个元素置空
23 elementData[--size] = null;
24
25 // 返回已经删除的元素
26 return oldValue;
27}
8.6 删除指定元素值的元素方法 remove(Object o)
1/**
2 * 删除指定元素值的元素
3 *
4 * @param o 要删除元素
5 * @return 如果待删除的指定元素存在则返回 true,否则返回 false
6 */
7public boolean remove(Object o) {
8 // 如果待删除元素为 null,则遍历集合,查找集合中为 null 的元素进行删除
9 if (o == null) {
10 for (int index = 0; index < size; index++) {
11 // 找到第一个为 null 的元素,然后进行快速删除,最后返回结果 true
12 if (elementData[index] == null) {
13 fastRemove(index);
14 return true;
15 }
16 }
17 }
18 // 如果待删除元素不为 null,则遍历集合,查找集合中和待删除元素相同的元素进行删除
19 else {
20 for (int index = 0; index < size; index++) {
21 // 找到第一个与待删除元素相同的元素,然后进行快速删除,最后返回结果 true
22 if (o.equals(elementData[index])) {
23 fastRemove(index);
24 return true;
25 }
26 }
27 }
28 // 如果没有找到和待删除元素相同的元素,则返回 false
29 return false;
30}
8.7 获取指定索引位置元素值方法 get(int index)
get(int index)
1/**
2 * 获取指定索引位置元素值
3 *
4 * @param index 要查找元素在集合中的位置索引
5 * @return 集合中指定位置的元素
6 * @throws IndexOutOfBoundsException 索引越界异常
7 */
8public E get(int index) {
9 // 检查索引是否越界
10 rangeCheck(index);
11 // 返回集合指定位置中的元素
12 return elementData(index);
13}
elementData(int index)
1/**
2 * 获取集合指定位置中的元素
3 *
4 * @param index 要查找元素在集合中的位置索引
5 * @return 集合中指定位置的元素
6 */
7E elementData(int index) {
8 return (E) elementData[index];
9}
8.8 设置指定索引位置元素值方法 set(int index, E element)
1/**
2 * 设置指定索引位置元素值
3 *
4 * @param index 需要替换的元素索引
5 * @param element 要存储在指定位置的元素
6 * @return 先前位于指定位置中的旧元素
7 * @throws IndexOutOfBoundsException 索引越界异常
8 */
9public E set(int index, E element) {
10 // 检查索引是否越界
11 rangeCheck(index);
12 // 获取集合指定位置中的旧元素,赋给变量 oldValue
13 E oldValue = elementData(index);
14 // 将传入的新元素插入到集合中的指定索引位置
15 elementData[index] = element;
16 // 返回集合中指定索引位置中原先的旧元素
17 return oldValue;
18}
8.9 查找指定元素的索引位置方法 astIndexOf(Object o)
1/**
2 * 查找集合中指定元素最后一次出现的位置
3 *
4 * @param o 待查找的元素
5 * @return 查找到的元素位置的索引,如果未找到则返回 -1
6 */
7public int lastIndexOf(Object o) {
8 // 如果待查找元素为 null,从后向前遍历,找到第一个为 null 的元素,然后返回该元素的下标
9 if (o == null) {
10 for (int i = size-1; i >= 0; i--) {
11 if (elementData[i]==null) {
12 return i;
13 }
14 }
15 }
16 // 如果待查找元素不为 null,从后向前遍历,找到第一个与待查找元素相等的元素,然后返回该元素的下标
17 else {
18 for (int i = size-1; i >= 0; i--) {
19 if (o.equals(elementData[i])) {
20 return i;
21 }
22 }
23 }
24 // 如果列表中不存在待查找元素,则返回 -1
25 return -1;
26}
九、ArrayList 中的疑问
9.1 ArrayList 的实现原理是什么?
ArrayList 是 Java 中的一个动态数组,可以存储任意类型的对象。其实现原理是基于数组的,当创建一个 ArrayList 对象时,实际上会创建一个 Object 类型的数组来存储数据。
在向 ArrayList 中添加元素时,如果数组已满,则会创建一个新的数组,并将原来的元素复制到新数组中,这个过程叫做扩容。ArrayList 默认的初始容量为 10,扩容时会将容量翻倍,因此每次扩容后容量都会变为原来的两倍。
在删除元素时,为了保证数组的连续性,会将待删除元素后面的所有元素向前移动一个位置。这个过程需要移动的元素个数是数组长度减去待删除元素的索引位置。
在查询元素时,ArrayList 直接根据索引访问数组元素,因此查询效率非常高。
总的来说,ArrayList 的实现原理是基于动态数组的,支持随机访问、快速添加和删除元素,但在进行扩容和删除操作时需要移动大量元素,会产生性能开销。
9.2 ArrayList 和数组有什么区别?
ArrayList 和数组都可以用于存储一组元素,但是它们在底层实现和使用方法上有一些不同之处。以下是 ArrayList 和数组的区别:
- 数组的长度是固定的,而 ArrayList 可以动态扩展。
- 数组可以包含基本类型和对象类型,而 ArrayList 只能包含对象类型。
- 数组的元素可以直接访问,而 ArrayList 必须通过get和set方法访问。
- 数组是数组类型, ArrayList 是一个类。
- 数组可以直接初始化,而 ArrayList 需要进行实例化后调用add方法添加元素。
- 数组的排序需要手动实现,而 ArrayList 可以调用Collections.sort方法进行排序。
总的来说, ArrayList 相比数组具有更好的灵活性和扩展性,但在某些场景下数组更加高效,比如在需要频繁的访问和修改元素时。另外,由于 ArrayList 是基于数组实现的,因此在需要存储大量数据时, ArrayList 的内存占用可能会比数组高。
9.3 ArrayList 和 LinkedList 集合有什么区别?
ArrayList 和 LinkedList 都是属于 List 集合,它们的最大区别在于底层的实现方式。具体而言,差异在以下几个方面:
- 随机访问速度不同: ArrayList 支持从任意位置访问集合元素,因为它是基于固定大小的数组存储元素的,索引访问速度较快。而 LinkedList 是通过链表的方式来存储元素,它的随机访问速度比 ArrayList 慢得多。
- 插入和删除速度不同: ArrayList 在中间位置插入或删除元素时,需要将之后的元素向后移动,而 LinkedList 不需要这样做,因为它只需要调整节点的指针即可。因此 LinkedList 的插入和删除速度比 ArrayList 快得多。
- 内存占用不同: 在同样数量的元素下,LinkedList 占用的内存通常比 ArrayList 多,因为每个节点都需要存储前后指针的信息。
- 迭代器效率不同: LinkedList 的迭代器在元素数量较大时的遍历效率要优于 ArrayList 的迭代器。
根据业务需求选择不同的集合类型,既能提高效率,又能优化内存占用。
9.4 ArrayList 和 HashSet 集合有什么区别?
ArrayList 和 HashSet 两种不同类型的集合,它们的主要区别在于其内部实现机制和集合特性。具体来说,它们的区别如下:
- 内部实现机制不同: ArrayList 是基于动态数组实现的,而 HashSet 则是基于哈希表实现的。
- 存储方式不同: ArrayList 按顺序存储元素,支持重复元素。 HashSet 不按顺序存储元素,不支持重复元素。
- 查找效率不同: ArrayList 使用索引进行元素查找,查找效率随着元素数量增多而降低。 HashSet 使用哈希函数进行元素查找,查找效率基本不受元素数量的影响。
- 迭代顺序不同: ArrayList 和 HashSet 迭代元素的顺序不同。 ArrayList 按照添加元素的顺序进行迭代,而 HashSet 没有固定的迭代顺序。
- 删除效率不同: ArrayList 在中间位置删除元素时需要移动其它元素放入空位,因此删除效率较低。 HashSet 在删除元素时只需要更新哈希表中的映射关系,因此删除效率较高。
在选择使用 ArrayList 和 HashSet 时,应该根据具体的需求来进行选择。如果需要按顺序存储,或者需要允许有重复元素,那么就应该使用 ArrayList。如果需要快速查找元素且不允许有重复元素,那么就应该使用 HashSet。
--- END ---
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。