杰瑞科技汇

ArrayList如何实现动态扩容?

什么是 ArrayList?

ArrayList 是 Java 集合框架中一个非常核心的类,它的中文意思是“动态数组”或“数组列表”。

ArrayList如何实现动态扩容?-图1
(图片来源网络,侵删)

核心特点:

  • 动态大小:与 Java 中固定长度的普通数组(int[])不同,ArrayList 的长度是可变的,你可以在运行时随时添加或删除元素,它的大小会自动调整。
  • 基于数组实现:尽管它叫“动态数组”,但其底层实现仍然是数组,当数组空间不足时,它会创建一个更大的新数组,并将旧数组中的元素复制到新数组中。
  • 有序ArrayList 中的元素是按照你添加的顺序(插入顺序)进行存储的。
  • 允许重复元素:你可以向 ArrayList 中添加多个相同的元素。
  • 允许 null 元素ArrayList 可以存储 null 值。

你可以把它想象成一个可以自动伸缩的盒子,你可以随意往里面放东西,不用提前考虑盒子的大小。


为什么使用 ArrayList 而不是普通数组?

特性 普通数组 (int[]) ArrayList
大小 固定,创建时必须指定长度,之后无法改变。 动态可变,可以随时添加或删除元素。
类型 可以是基本类型(如 int[]),也可以是对象(如 String[])。 只能存储对象,不能直接存储基本类型,但可以使用其包装类(如 Integer)。
性能 访问(get)速度快,因为基于索引,增删慢,因为需要移动元素。 访问(get)速度快,因为底层是数组,增删在末尾时快,在中间/开头时慢(需要移动元素)。
功能 功能有限,只有 length 属性和一些基本方法。 功能丰富,提供了大量操作列表的方法(如 add(), remove(), size() 等)。
代码 String[] arr = new String[10]; ArrayList<String> list = new ArrayList<>();

当你需要一个可以动态改变大小的集合,并且需要频繁地通过索引访问元素时,ArrayList 是最佳选择。


如何使用 ArrayList (常用方法)

在使用 ArrayList 之前,你需要导入它所在的包:

import java.util.ArrayList;

1 创建和初始化

// 1. 创建一个空的 ArrayList,默认初始容量是 10
ArrayList<String> names = new ArrayList<>();
// 2. 创建一个指定初始容量的 ArrayList (推荐,如果预知大小,可以减少扩容带来的性能损耗)
ArrayList<Integer> numbers = new ArrayList<>(20);
// 3. 使用 Arrays.asList 初始化 (注意:返回的是固定大小的列表,不能增删)
ArrayList<Double> prices = new ArrayList<>(Arrays.asList(10.5, 20.0, 15.8));

2 常用操作方法

假设我们有以下 ArrayList

ArrayList<String> fruits = new ArrayList<>();
方法 描述 示例
add(E element) 向列表末尾添加一个元素。 fruits.add("Apple");
add(int index, E element) 在指定位置插入一个元素。 fruits.add(0, "Banana"); // 在开头插入
get(int index) 获取指定索引位置的元素。 String firstFruit = fruits.get(0); // "Banana"
set(int index, E element) 修改指定索引位置的元素。 fruits.set(1, "Orange"); // 将第二个元素改为 "Orange"
remove(int index) 移除指定索引位置的元素,并返回该元素。 fruits.remove(0); // 移除 "Banana"
remove(Object obj) 移除列表中第一个出现的指定元素。 fruits.remove("Orange");
size() 返回列表中的元素个数(类似数组的 length)。 int count = fruits.size();
clear() 清空列表中的所有元素。 fruits.clear();
isEmpty() 判断列表是否为空,如果为空返回 true boolean empty = fruits.isEmpty();
contains(Object obj) 判断列表中是否包含某个元素,包含返回 true boolean hasApple = fruits.contains("Apple");
indexOf(Object obj) 返回元素第一次出现的索引,如果不存在返回 -1。 int index = fruits.indexOf("Apple");

3 遍历 ArrayList

有三种主要的方式来遍历 ArrayList

for 循环 (通过索引)

ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
for (int i = 0; i < fruits.size(); i++) {
    System.out.println(fruits.get(i));
}

for-each 循环 (推荐)

这是最简洁、最常用的方式,适用于大多数场景。

for (String fruit : fruits) {
    System.out.println(fruit);
}

使用迭代器 (Iterator)

迭代器是一种更安全、更通用的方式,特别是在遍历过程中需要删除元素时。

Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
    String fruit = iterator.next();
    if ("Banana".equals(fruit)) {
        iterator.remove(); // 安全地删除当前元素
    }
}

注意:在 for-each 循环中使用 list.remove() 是不安全的,可能会导致 ConcurrentModificationException,而使用迭代器的 remove() 方法是安全的。


ArrayList 的底层原理与性能分析

理解 ArrayList 的底层原理有助于更好地使用它。

1 核心属性

ArrayList 内部主要有两个核心属性:

  1. Object[] elementData: 一个对象数组,用于真正存储数据。
  2. int size: 记录当前列表中元素的个数。

2 扩容机制

这是 ArrayList 最关键的部分。

  1. 当你调用 add() 方法添加一个元素时,它会检查 size 是否已经等于 elementData.length(即数组是否已满)。
  2. 如果未满:直接将新元素添加到 elementData[size] 的位置,size++
  3. 如果已满:触发扩容
    • 计算新的容量:新容量通常是旧容量的 1.5 倍newCapacity = oldCapacity + (oldCapacity >> 1))。
    • 创建一个这个新容量大小的新数组。
    • 将旧数组中的所有元素复制到新数组中。
    • elementData 引用指向这个新数组。
    • 将新元素添加进去,size++

扩容是一个比较耗时的操作,因为它涉及到数组的创建和复制。

3 性能分析

操作 时间复杂度 说明
add(E e) 平均 O(1),最坏 O(n) 平均情况是 O(1),因为添加到末尾很快,最坏情况发生在扩容时,需要复制所有元素,所以是 O(n)。
add(int index, E e) O(n) 因为需要在指定位置插入元素,该位置之后的所有元素都需要向后移动一位。
get(int index) O(1) 直接通过索引访问数组,速度极快,这是 ArrayList 最大的优势之一。
remove(int index) O(n) 需要将被删除元素之后的所有元素向前移动一位来填补空缺。
remove(Object o) O(n) 需要先遍历列表找到该元素的索引,然后再执行删除操作(移动元素)。
contains(Object o) O(n) 需要遍历整个列表进行比较。

ArrayList 与 LinkedList 的区别

这是面试中非常经典的问题。

特性 ArrayList LinkedList
数据结构 动态数组 双向链表
内存布局 连续的内存空间 不连续,每个节点(Node)存储数据、前驱指针和后继指针
随机访问 (get) 快 O(1) 慢 O(n),需要从头或尾开始遍历
头部增删 (add(0, e), remove(0)) 慢 O(n),需要移动所有元素 快 O(1),只需修改头节点的指针
尾部增删 (add(e), remove(size-1)) 快 O(1) (不考虑扩容) 快 O(1)
内存消耗 较低,只需要一个数组对象 较高,每个元素都是一个对象,额外存储了指针信息
适用场景 频繁查询,较少增删的场景。 频繁增删,较少查询的场景。

如何选择?

  • 如果你的应用场景主要是读取数据,或者你知道大概的数据量,选择 ArrayList
  • 如果你的应用场景主要是在列表中间或头部频繁插入和删除数据,选择 LinkedList

泛型

你一定注意到了 ArrayList<String> 这种写法,这就是泛型

作用:

  1. 类型安全:编译器会检查你放入 ArrayList 的元素类型是否正确。ArrayList<String> 只能存放 String 类型的对象,放入 Integer 会在编译时报错。
  2. 避免强制类型转换:从 ArrayList 中取出元素时,不需要再进行强制类型转换,代码更简洁、更安全。

不使用泛力的旧式写法 (不推荐):

// 可以存放任何 Object 类型的对象
ArrayList list = new ArrayList();
list.add("Hello");
list.add(123); // 编译器不会报错,但逻辑上很混乱
String str = (String) list.get(0); // 取出时必须强制转换
Integer num = (Integer) list.get(1);

ArrayList 是 Java 开发者必须掌握的工具,记住它的核心思想:一个基于数组、大小可变的动态列表

  • 优点:查询快,内存占用相对较低。
  • 缺点:在中间或头部增删慢,扩容时有性能开销。
  • 核心方法add(), get(), remove(), size(), contains()
  • 最佳实践:尽量使用 for-each 循环遍历,如果预知大小,在初始化时指定容量,以减少扩容次数。
分享:
扫描分享到社交APP
上一篇
下一篇