杰瑞科技汇

ArrayList如何动态扩容?

ArrayList 是 Java 集合框架中非常核心和常用的一种数据结构,理解它对于进行 Java 开发至关重要。

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

什么是 ArrayList?

ArrayList 的本质是一个动态数组,它实现了 List 接口,可以看作是长度可变的数组。

核心特点:

  • 有序性:元素的存储顺序和插入顺序一致。
  • 可重复性:可以存储重复的元素。
  • 动态扩容:可以在运行时动态地增加或减少其大小,这是它与普通数组([])最大的区别。

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

特性 普通数组 (int[]) ArrayList
长度 固定,创建时必须指定长度,之后无法改变。 动态可变,可以随时添加或删除元素。
数据类型 可以是基本类型(int, char 等)或对象。 只能存储对象,对于基本类型,需要使用其对应的包装类(如 Integer, Character)。
性能 访问(随机访问)速度极快,直接通过索引计算内存地址。 访问速度也很快(底层是数组),但比普通数组稍慢一点,增删中间元素时性能较差。
功能 功能非常有限,就是一个容器。 提供了丰富的 API,如 add(), remove(), size(), contains() 等,操作非常方便。

当你需要一个固定大小的、高性能的容器时,使用普通数组,当你需要一个大小不固定、需要频繁增删元素、且希望使用丰富方法的容器时,ArrayList 是不二之选。


如何使用 ArrayList (核心 API)

1 创建 ArrayList

import java.util.ArrayList;
import java.util.List;
// 1. 创建一个可以存储 String 类型的 ArrayList
ArrayList<String> stringList = new ArrayList<>();
// 2. 创建一个可以存储 Integer 类型的 ArrayList (推荐使用 List 接口作为引用类型)
List<Integer> integerList = new ArrayList<>();
// 3. 创建时指定初始容量 (可选)
// 如果大致知道要存储多少元素,指定初始容量可以减少扩容操作,提高性能
List<Double> doubleList = new ArrayList<>(20);

2 常用方法

import java.util.ArrayList;
import java.util.List;
public class ArrayListExample {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();
        // 1. 添加元素
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Orange");
        System.out.println("添加后: " + fruits); // [Apple, Banana, Orange]
        // 2. 在指定位置插入元素
        fruits.add(1, "Mango");
        System.out.println("在索引1插入Mango后: " + fruits); // [Apple, Mango, Banana, Orange]
        // 3. 获取元素
        String firstFruit = fruits.get(0);
        System.out.println("第一个水果是: " + firstFruit); // Apple
        // 4. 获取列表大小(元素个数)
        int size = fruits.size();
        System.out.println("列表大小: " + size); // 4
        // 5. 删除元素
        // 5.1 删除指定位置的元素
        fruits.remove(2); // 删除 "Banana"
        System.out.println("删除索引2的元素后: " + fruits); // [Apple, Mango, Orange]
        // 5.2 删除指定的元素(第一个匹配的)
        fruits.remove("Apple");
        System.out.println("删除元素'Apple'后: " + fruits); // [Mango, Orange]
        // 6. 修改元素
        fruits.set(0, "Pineapple");
        System.out.println("修改索引0的元素后: " + fruits); // [Pineapple, Orange]
        // 7. 判断元素是否存在
        boolean hasOrange = fruits.contains("Orange");
        System.out.println("列表中是否包含'Orange'? " + hasOrange); // true
        // 8. 清空列表
        fruits.clear();
        System.out.println("清空列表后: " + fruits); // []
        System.out.println("列表是否为空? " + fruits.isEmpty()); // true
    }
}

ArrayList 的底层原理 (面试重点)

理解 ArrayList 的底层实现,能帮助你更好地使用它并写出高性能的代码。

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

1 核心属性

ArrayList 内部主要由两个属性构成:

// 1. 存储元素的数组, transient 表示该属性不会被序列化
private transient Object[] elementData;
// 2. ArrayList 中当前存储的元素个数
private int size;
  • elementData:这是一个Object类型的数组,真正用来存放数据的地方。
  • size:记录当前有多少个元素,它不一定是数组的长度。

2 动态扩容机制

ArrayList 的“动态”主要体现在其自动扩容机制上。

  1. 初始容量:当你使用 new ArrayList<>() 无参构造函数时,elementData 是一个空数组(长度为0),当你第一次调用 add() 方法时,它会将 elementData 的容量扩容到 DEFAULT_CAPACITY (默认为10)

  2. 扩容时机:当 add() 一个元素时,size 已经等于 elementData.length(即数组已满),就会触发扩容。

  3. 扩容方式:创建一个新的、更大的数组,然后将旧数组中的所有元素复制到新数组中,最后让 elementData 引用这个新数组。

  4. 扩容大小:新的容量通常是旧容量的1.5倍(使用位运算 oldCapacity + (oldCapacity >> 1) 实现)。

// 伪代码演示扩容过程
public void add(E e) {
    // 1. 检查是否需要扩容
    if (size == elementData.length) {
        // 计算新容量
        int newCapacity = elementData.length + (elementData.length >> 1);
        // 创建新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    // 2. 添加元素
    elementData[size++] = e;
}

扩容的代价:扩容是一个比较耗时的操作,因为它需要创建一个新数组并复制所有元素,如果你能预估到 ArrayList 将要存储的元素数量,在创建时指定一个合适的初始容量,可以避免频繁扩容,提升性能。


遍历 ArrayList

有四种常见的遍历方式:

List<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie"));
// 1. for 循环 + 索引 (最传统,适用于需要索引的场景)
System.out.println("--- for 循环 ---");
for (int i = 0; i < names.size(); i++) {
    System.out.println(names.get(i));
}
// 2. 增强 for 循环 (最常用,代码简洁)
System.out.println("\n--- 增强 for 循环 ---");
for (String name : names) {
    System.out.println(name);
}
// 3. 使用 Iterator (迭代器),适合在遍历时进行删除操作
System.out.println("\n--- 使用 Iterator ---");
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
    String name = iterator.next();
    if ("Bob".equals(name)) {
        iterator.remove(); // 安全删除
    }
}
System.out.println("删除Bob后: " + names); // [Alice, Charlie]
// 4. 使用 Java 8+ 的 Stream API (函数式风格,功能强大)
System.out.println("\n--- 使用 Stream API ---");
names.stream().forEach(System.out::println);

线程安全问题

ArrayList非线程安全的,如果在多线程环境下,多个线程同时修改一个 ArrayList 实例,可能会导致数据不一致或 ConcurrentModificationException(并发修改异常)。

如何解决?

  1. 使用 Collections.synchronizedList():将 ArrayList 包装成线程安全的列表。

    List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
  2. 使用 CopyOnWriteArrayList:这是 java.util.concurrent 包下的一个类,它是一种“写时复制”的并发列表,适用于读多写少的场景,每次修改操作(add, set, remove)都会复制一份底层数组,保证了线程安全,但写入性能较差。


特性 描述
数据结构 基于动态数组的 List 接口实现。
核心优点 随机访问快(get() 方法效率高),增删末尾元素快。
核心缺点 在中间位置增删元素慢(需要移动大量元素),扩容有性能开销。
线程安全 非线程安全
初始容量 默认为10,第一次 add 时初始化。
扩容机制 容量不足时,扩容为原容量的 5 倍
适用场景 需要频繁随机访问元素,且增删操作主要集中在列表末尾的场景。

掌握 ArrayList 是 Java 编程的基础,理解其原理能让你在开发中做出更合理的技术选型,并写出更高效的代码。

分享:
扫描分享到社交APP
上一篇
下一篇