Java ArrayList

2025年4月1日 | 阅读 21 分钟
Java ArrayList class hierarchy

Java 中的 ArrayList 是 Java 集合框架中的一个动态数组实现。它是一个大小可变的数组,当向其中添加更多元素时,它会自动增长。

ArrayList 来自 java.util 包,因其易用性和灵活性而广受欢迎。它们之所以灵活,是因为您无需在创建 ArrayList 时确定其大小,这与 Java 中的标准数组类似。因此,它比传统数组更灵活。它类似于 C++ 中的 Vector。

Java 中的 ArrayList 也可以包含重复元素。它实现了 List 接口,因此我们可以使用 List 接口的所有方法。ArrayList 在内部维护插入顺序。

它继承了 AbstractList 类并实现了 List 接口

关于 Java ArrayList 类的重要注意事项是:

  • 维护插入顺序: Java ArrayList 保证了元素的添加顺序。在遍历 ArrayList 时,元素按添加它们的相同顺序被访问。
  • 非同步: 与某些其他 Java 集合类(例如 Vector)不同,ArrayList 不是同步的。这意味着 ArrayList 不是线程安全的;因此,如果多个线程并发访问 ArrayList,可能会出现并发修改问题。
  • 支持随机访问: ArrayList 允许通过元素的索引位置实现快速的随机访问操作。这是因为内部实现使用的是数组结构,可以确保通过索引常量时间访问元素。
  • 与 LinkedList 相比,操作较慢: ArrayList 中的插入和删除等操作可能比 LinkedList 慢。因此,当在列表的除末尾以外的任何位置插入或删除项时,ArrayList 必须执行元素的移位。然而,在 LinkedList 中添加或删除元素非常容易,并且不需要移位。
  • 需要包装类来表示原始类型: ArrayList 不直接支持 `int`、`float` 和 `char` 等原始数据类型。相反,它需要 `Integer`、`Float`、`Character` 等包装类来容纳这些原始类型。例如
  • 动态大小调整: ArrayList 在添加或删除元素时会扩展或收缩以满足所需的新大小。由于自适应大小调整,可以在不进行物理大小调整的情况下管理在线集合。
  • 容量: ArrayList 有一个初始容量,这是它在重新分配之前可以容纳的元素数量。如果元素数量无法容纳在此容量中,则列表会自动增大其大小以容纳其余元素。这可能涉及将所有元素复制到新的、更大的数组中。
  • 可迭代: ArrayList 实现 `Iterable` 接口。因此,我们可以轻松地使用增强 for 循环或迭代器来迭代元素。
  • 动态初始化: Java ArrayList 在不指定其大小的情况下进行初始化。与必须声明固定大小的传统数组不同,ArrayList 根据添加或删除的元素数量动态调整其大小。这种动态大小调整消除了预分配内存或担心超出数组边界的需要,从而在管理集合方面提供了更大的灵活性。

ArrayList 类的继承结构

如上图所示,Java ArrayList 类继承了 AbstractList 类,该类实现了 List 接口。List 接口在其继承结构中继承了 Collection 和 Iterable 接口。

ArrayList 类声明

让我们看看 java.util.ArrayList 类的声明。

public class ArrayList<E>: 引入了 ArrayList 类作为通用类,其中类型参数 E 是 ArrayList 中存储的元素的类型。类型参数 E 使 ArrayList 能够存储任何引用类型的对象。

extends AbstractList<E>: 表示 ArrayList 类是 AbstractList 类的子类。AbstractList 实现 List 接口的骨架方面,提供了其几乎所有方法。

implements List<E>: 表示 ArrayList 类实现了 List 接口。List 接口描述了列表的操作,并继承了 Collection 接口。

implements RandomAccess: 表示 ArrayList 类实现了 RandomAccess 标记接口。此接口是为提供对其元素的快速(常量时间)随机访问的类设置的标志。

implements Cloneable: 表明 ArrayList 类实现了 Cloneable 标记接口。此接口表示 ArrayList 实例可以使用 clone() 方法进行克隆。

implements Serializable: 这表明 ArrayList 类实现了 Serializable 标记接口。此接口使得 ArrayList 类的实例可以被序列化(转换为字节流)以进行存储或传输。

ArrayList 类的构造函数

构造函数描述
ArrayList()用于构建一个空的 ArrayList。
ArrayList(Collection<? extends E> c)用于构建一个已初始化有集合 c 的元素的 ArrayList。
ArrayList(int capacity)用于构建一个具有指定初始容量的 ArrayList。

ArrayList 类的方法

方法描述
void add(int index, E element)用于将指定元素插入到列表中的指定位置。
boolean add(E e)用于将指定元素添加到列表的末尾。
boolean addAll(Collection<? extends E> c)用于将指定集合中的所有元素按指定集合的迭代器返回的顺序添加到此列表的末尾。
boolean addAll(int index, Collection<? extends E> c)用于将指定集合中的所有元素添加到列表的指定位置。
void clear()用于从列表中删除所有元素。
void ensureCapacity(int requiredCapacity)用于增加 ArrayList 实例的容量。
E get(int index)用于从列表的特定位置获取元素。
boolean isEmpty()如果列表为空,则返回 true,否则返回 false。
Iterator()返回一个按正确顺序遍历 ArrayList 中元素的迭代器。
允许顺序访问 ArrayList 中的元素。
listIterator()返回一个按正确顺序遍历 ArrayList 中元素的列表迭代器。
允许双向访问 ArrayList 中的元素,包括在迭代过程中添加、删除和修改元素。
int lastIndexOf(Object o)用于返回列表中指定元素的最后一次出现的索引,如果列表不包含该元素,则返回 -1。
Object[] toArray()用于返回一个包含列表中所有元素的数组,顺序正确。
<T> T[] toArray(T[] a)用于返回一个包含列表中所有元素的数组,顺序正确。
Object clone()用于返回 ArrayList 的浅拷贝。
boolean contains(Object o)如果列表包含指定元素,则返回 true。
int indexOf(Object o)用于返回列表中指定元素的第一次出现的索引,如果列表不包含该元素,则返回 -1。
E remove(int index)用于删除列表中指定位置的元素。
boolean remove(Object o)用于删除指定元素的第一次出现。
boolean removeAll(Collection<?> c)用于从列表中删除所有元素。
boolean removeIf(Predicate<? super E> filter)用于从列表中删除满足给定谓词的所有元素。
protected void removeRange(int fromIndex, int toIndex)用于删除给定范围内所有元素。
void replaceAll(UnaryOperator<E> operator)用于用指定元素替换列表中的所有元素。
void retainAll(Collection<?> c)用于保留列表中存在于指定集合中的所有元素。
E set(int index, E element)用于替换列表中指定位置的指定元素。
void sort(Comparator<? super E> c)用于根据指定的比较器对列表中的元素进行排序。
Spliterator<E> spliterator()用于在列表中的元素上创建 spliterator。
List<E> subList(int fromIndex, int toIndex)用于获取给定范围内所有元素。
int size()用于返回列表中元素的数量。
void trimToSize()用于将此 ArrayList 实例的容量修剪为列表的当前大小。

Java 非泛型与泛型集合

在 JDK 1.5 之前,Java 集合框架是非泛型的,这意味着类型安全性无法保证,并且在从集合中检索元素时需要手动进行类型转换。

在这个非泛型 ArrayList 中,您可以添加任何类型的元素,编译器也不会强制执行类型安全。例如,您可以先添加一个 String,然后添加一个 Integer,而无需编译时检查。

随着 Java 5 (JDK 1.5) 中泛型的引入,集合框架变成了泛型的。泛型允许您在编译时指定集合可以容纳的元素类型,从而提供类型安全性并消除显式类型转换的需要。例如,创建泛型 ArrayList 的示例:让我们看看创建 Java 集合的新泛型示例。

在这个泛型 ArrayList 中,类型参数指定 ArrayList 只能包含 String 类型的元素。如果您尝试添加任何其他类型的元素,编译器将生成编译时错误,从而确保类型安全。例如,尝试将 Integer 添加到此 ArrayList 将导致编译时错误。

Java ArrayList 示例

示例

编译并运行

输出

[Mango, Apple, Banana, Grapes]

使用 Iterator 遍历 ArrayList

在 Java 中,使用 Iterator 遍历 ArrayList 是一个常见的操作,它允许您按顺序遍历列表的元素。迭代器提供了一种安全高效的方式来访问集合中的元素,特别是当您想在迭代过程中执行删除元素等操作时。让我们来看一个使用 Iterator 接口遍历 ArrayList 元素的示例。

示例

编译并运行

输出

Mango
Apple
Banana
Grapes

使用 For-each 循环遍历 ArrayList

使用 for-each 循环(也称为增强 for 循环)遍历 ArrayList 提供了一种简洁易读的方式来按顺序访问列表中的元素。这种方法简化了代码,特别是在您只需要遍历元素而不需要索引或执行复杂操作时非常有用。让我们看一个使用 for-each 循环遍历 ArrayList 元素的示例。

示例

编译并运行

输出

Mango
Apple
Banana
Grapes

获取和设置 ArrayList

您可以使用 get() 和 set() 方法检索和修改 ArrayList。

使用 get() 方法

  • get() 方法用于在 ArrayList 中检索特定索引处的元素。
  • 它接受一个整数参数,该参数指定要检索的元素的索引。
  • 索引从 0 开始,表示第一个元素,一直到 size() - 1,表示 ArrayList 中的最后一个元素。

使用 set() 方法

  • set() 方法用于用新元素替换 ArrayList 中特定索引处的元素。
  • 它接受两个参数:要替换的元素的索引,以及要放在该索引处的新元素。
  • 指定索引处的先前元素将被新元素覆盖。

示例

编译并运行

输出

Returning element: Apple
Mango
Dates
Banana
Grapes

如何对 ArrayList 进行排序?

java.util 包提供了一个实用类 **Collections**,其中包含静态方法 sort()。使用 **Collections.sort()** 方法,我们可以轻松地对 ArrayList 进行排序。

示例

编译并运行

输出

Apple
Banana
Grapes
Mango
Sorting numbers...
1
11
21
51

遍历 Java 中集合元素的方法

遍历集合元素有多种方法

  1. 通过 Iterator 接口。
  2. 通过 for-each 循环。
  3. 通过 ListIterator 接口。
  4. 通过 for 循环。
  5. 通过 forEach() 方法。
  6. 通过 forEachRemaining() 方法。

通过其余方法遍历集合

让我们看一个通过其他方式遍历 ArrayList 元素的示例

示例

编译并运行

输出

Traversing list through List Iterator:
Ajay
Ravi
Vijay
Ravi
Traversing list through for loop:
Ravi
Vijay
Ravi
Ajay
Traversing list through forEach() method:
Ravi
Vijay
Ravi
Ajay
Traversing list through forEachRemaining() method:
Ravi
Vijay
Ravi
Ajay

Java ArrayList 中的用户自定义类对象

ArrayList 可以存储用户自定义类的对象,从而在管理自定义数据类型集合方面提供了灵活性和可扩展性。此功能允许开发人员创建根据其特定应用程序需求量身定制的集合。

在 ArrayList 中使用用户自定义类对象时,ArrayList 中的每个元素都代表用户自定义类的一个实例。例如,考虑一个 Student 类代表具有姓名、年龄和学号等属性的个人。我们可以创建一个 ArrayList 来存储 Sudent 类的实例

让我们看一个将 Student 类对象存储在 ArrayList 中的示例。

示例

输出

       101 Sonoo 23
       102 Ravi 21
       103 Hanumat 25

Java ArrayList 序列化和反序列化示例

Java 中的序列化和反序列化是将对象转换为字节流以进行存储或传输,然后分别从这些字节流中重构对象的进程。此机制允许将对象保存到文件、通过网络发送或存储在数据库中。

让我们看一个序列化 ArrayList 对象然后对其进行反序列化的示例。

示例

输出

[Ravi, Vijay, Ajay]

Java ArrayList 添加元素的示例

在这里,我们看到添加元素的各种方法。

示例

输出

Initial list of elements: []
After invoking add(E e) method: [Ravi, Vijay, Ajay]
After invoking add(int index, E element) method: [Ravi, Gaurav, Vijay, Ajay]
After invoking addAll(Collection<? extends E> c) method: 
[Ravi, Gaurav, Vijay, Ajay, Sonoo, Hanumat]
After invoking addAll(int index, Collection<? extends E> c) method: 
[Ravi, John, Rahul, Gaurav, Vijay, Ajay, Sonoo, Hanumat]

Java ArrayList 删除元素的示例

在这里,我们看到删除元素的各种方法。

示例

输出

An initial list of elements: [Ravi, Vijay, Ajay, Anuj, Gaurav]
After invoking remove(object) method: [Ravi, Ajay, Anuj, Gaurav]
After invoking remove(index) method: [Ajay, Anuj, Gaurav]
Updated list : [Ajay, Anuj, Gaurav, Ravi, Hanumat]
After invoking removeAll() method: [Ajay, Anuj, Gaurav]
After invoking removeIf() method: [Anuj, Gaurav]
After invoking clear() method: []

Java ArrayList retainAll() 方法示例

Java ArrayList 中的 retainAll() 方法用于仅保留 ArrayList 中也存在于另一个集合(通常是另一个 ArrayList)中的元素。换句话说,它会删除当前 ArrayList 中不存在于指定集合中的所有元素。

示例

编译并运行

输出

iterating the elements after retaining the elements of al2
Ravi

Java ArrayList isEmpty() 方法示例

Java ArrayList 中的 isEmpty() 方法用于检查 ArrayList 是否为空,即,如果 ArrayList 不包含任何元素,则返回 true,否则返回 false。

示例

编译并运行

输出

Is ArrayList Empty: true
After Insertion
Is ArrayList Empty: false

Java ArrayList 示例:Book

让我们看一个 ArrayList 示例,其中我们将书籍添加到列表中并打印所有书籍。

示例

编译并运行

输出

101 Let us C Yashwant Kanetkar BPB 8
102 Data Communications and Networking Forouzan Mc Graw Hill 4
103 Operating System Galvin Wiley 6

ArrayList 的大小和容量

ArrayList 的大小和容量是初学者容易混淆的两个术语。ArrayList 通过添加或删除元素来处理可变大小的元素,使其在集合方面具有灵活性。ArrayList 的长度是指要存储在其中的元素数量,您可以通过调用 size() 方法来获取。添加或删除元素时,此大小会自动调整。

Array List 的容量是指在需要重新调整大小时它可以容纳的最大元素数量。最初,ArrayList 的容量由用于创建它的构造函数定义,或者在没有定义构造函数的情况下由默认容量定义。当 ArrayList 中的对象总数超过当前容量时,ArrayList 会自动重新分配并增加其容量以容纳更多对象,从而实现高效的存储和即时响应。可以使用 capacity() 方法检索 ArrayList 的大小。ArrayList 的大小和容量的逻辑在 ArrayList 和 Vector 类中是相同的。

让我们在这一节中通过一些例子来理解它。考虑以下代码片段。

示例

编译并运行

输出

The size of the array is: 0

解释: 输出是有意义的,因为我们还没有对 ArrayList 做任何事情。现在观察以下程序。

示例

编译并运行

输出

The size of the array is: 0

解释: 我们看到大小仍然是 0,原因是因为数字 10 代表容量而不是大小。事实上,大小代表数组中存在的元素总数。由于我们没有添加任何元素,因此两个程序中的 ArrayList 的大小都为零。

想象你有一个叫做 ArrayList 的盒子。这个盒子有一个叫做容量的特殊属性,它告诉你里面能装多少东西。假设这个盒子的容量是 10。

现在,你开始往这个盒子里放东西,比如玩具。每次放一个玩具进去,你都会检查盒子里的玩具数量(这叫做大小)是否等于容量。如果是,这意味着盒子满了,你需要一个更大的盒子来放更多的玩具。

所以,在你往盒子里放 10 个玩具之前,容量一直保持在 10。但是一旦你试图往里放第 11 个玩具,你就会发现盒子满了,你需要一个更大的盒子。在我们的例子中,由于盒子只能装 10 个玩具,你需要一个新的容量更大的盒子。

现在,请记住,有两种情况

在第一种情况下,你从一个可以装 10 个玩具的默认盒子开始。你没有指定容量;它只是默认的。

在第二种情况下,你明确地说:“我想要一个容量为 10 个玩具的盒子。”所以,一开始,你就得到了一个可以装 10 个玩具的盒子。

但在这两种情况下,过程都是相同的:你不断地添加玩具,如果盒子满了,你就会换一个更大的。

注意:没有标准的方法可以知道 ArrayList 的容量是如何增加的。事实上,容量增加的方式因 JDK 版本而异。因此,需要检查 JDK 中的容量增加代码是如何实现的。ArrayList 类中没有预定义的方法可以返回 ArrayList 的容量。因此,为了更好地理解,请使用 Vector 类的 capacity() 方法。ArrayList 和 Vector 类的大小和容量的逻辑是相同的。

ArrayList 的优点

动态大小调整: ArrayList 在添加或删除元素时会自动调整大小;因此,无需手动调整大小。ArrayList 能够自动调整大小,使其能够处理各种数量的元素,而无需手动调整大小。

随机访问: ArrayList 通过索引确保常数时间访问元素,允许执行随机访问。这一优势使得 ArrayList 成为需要基于其顺序进行随机访问元素的最佳选择。

快速迭代: ArrayList 允许使用增强循环或迭代器高效地遍历其所有元素。这种能力很有优势,因为它使得可以无缝地按顺序处理列表中的每个元素。无论是使用循环还是迭代器,您都可以轻松地访问和操作 ArrayList 中的每个项目,而无需复杂的索引或遍历逻辑。这种简化的方法提高了代码的可读性并简化了操作,使 ArrayList 成为各种编程任务的通用且方便的数据结构。

多功能性: ArrayList 可以维护任何类型的对象,包括用户创建的类。这种多功能性使开发人员能够根据客户的独特需求设计解决方案,整合不同的数据类型和格式。

灵活性: ArrayList 提供了大量与修改、插入或放置元素相关的方法,从而确保了在操作记录方面的灵活性。它支持添加、删除、访问、编辑等功能,使数据管理简单顺畅。

泛型支持: 随着 Java 5 中泛型的出现,ArrayList 变得类型安全,您可以定义它包含的元素类型。这改进了编译时类型检查并保证了类型安全,从而防止了运行时错误。

与 Java 集合框架集成: ArrayList 是 Java 集合框架的一部分,该框架定义了用于组织对象集合的统一架构。模块集成提供了与框架内其他排序模块和算法的兼容性。

ArrayList 的缺点

动态大小调整开销: ArrayList 的动态大小调整功能虽然有利于容纳添加或删除的元素,但会带来性能开销。随着内部数组动态地扩展或收缩,频繁的大小调整操作(尤其是在大型 ArrayList 中)会产生明显的开销。每次大小调整都涉及分配新内存、复制现有元素以及释放旧数组,这会消耗计算资源和时间。因此,过度的调整大小会影响程序效率,导致执行时间变慢。因此,虽然 ArrayList 的灵活性是有利的,但开发人员必须仔细管理大小调整操作以减轻性能损失,确保处理大型或频繁修改的 ArrayList 的应用程序获得最佳性能。

内存浪费: ArrayList 以块的形式分配内存,并且可能会分配比所需更多的内存来满足未来的增长。这可能会导致内存浪费,尤其是在 ArrayList 容量很大但只包含几个元素的情况下。

低效的插入和删除: ArrayList 的插入和删除操作效率低下,尤其是在列表的中间或开头。这种低效率是由于在添加新元素或删除元素时移动 ArrayList 中的元素所致。

不适合原始类型: ArrayList 只能存储对象,不能直接存储原始数据类型。在存储原始类型时,需要 Integer、Double 等包装类,这可能会消耗更多内存并导致额外的装箱/拆箱开销。

非线程安全: ArrayList 是非同步的,并且在设计上不是线程安全的。多个线程可以同时对 ArrayList 进行并发修改,这可能导致不可预测或损坏的数据。必须使用外部同步块来处理同步,或者使用 Vector 或 CopyOnWriteArrayList 作为线程安全的替代方案。

大型集合性能受限: 然而,对于极大的集合,ArrayList 在插入、删除或搜索操作方面可能不如 LinkedList 和 HashSet 等其他数据结构有效。替代数据结构可能更适用于特定情况。


Java ArrayList 选择题

1. 以下哪个接口是 Java ArrayList 类直接实现的?

  1. Set
  2. Queue
  3. 列表
  4. Map
 

答案:C

解释: ArrayList 类直接实现了 List 接口。此接口允许有序集合,并提供操作列表大小和元素的方法。Set、Queue 和 Map 接口不是 ArrayList 直接实现的。


2. 当从 ArrayList 中删除元素时,以下哪项最能描述对剩余元素的影响?

  1. 所有元素都将重新哈希
  2. 对剩余元素没有影响
  3. 删除元素后的元素将向左移动
  4. ArrayList 将变为不可变
 

答案:C

解释: 当从 ArrayList 中删除元素时,所有后续元素都会向左移动以填补被删除元素造成的空隙。这可能导致删除操作的时间复杂度为 O(n),其中 n 是被删除元素后的元素数量。


3. Java 中 ArrayList 的初始默认容量是多少?

  1. 5
  2. 10
  3. 16
  4. 20
 

答案:B

解释: 当不指定容量创建 ArrayList 时,默认初始容量为 10。随着向列表中添加元素,此容量可以动态增加。


4. 使用哪种方法可以确保 ArrayList 实例至少包含指定数量的元素?

  1. trimToSize(int capacity)
  2. ensureCapacity(int minCapacity)
  3. increaseCapacity(int capacity)
  4. setCapacity(int capacity)
 

答案:B

解释: ensureCapacity(int minCapacity) 方法用于增加 ArrayList 实例的容量,以确保它可以容纳至少由 minCapacity 指定的元素数量。这有助于在预期添加大量元素时最大限度地减少大小调整操作的数量。


5. Java ArrayList 在其容量需要增加时如何处理这种情况?

  1. 它会抛出 OutOfMemoryError
  2. 它使用链表来容纳新元素
  3. 它创建一个具有增加容量的新数组并将元素复制过去
  4. 它将元素附加到辅助数组
 

答案:C

解释: 当 ArrayList 需要更多容量时,它会创建一个具有更大容量的新数组(通常是当前容量的 1.5 倍或更多),然后将现有元素复制到这个新数组中。这种大小调整机制允许 ArrayList 有效地处理动态增长。


下一主题Java LinkedList