深入浅出 ArrayList:从基础用法到底层原理的全面解析(上)

在 Java 开发中,集合框架是日常编码的 "基础设施",而 ArrayList 作为其中最常用的 List 实现类,几乎出现在每一个 Java 项目中。无论是存储数据、遍历集合,还是处理动态数据场景,ArrayList 都以其便捷性和高效性成为开发者的首选。但很多人对 ArrayList 的理解仅停留在 "能用来存数据" 的层面,对其底层结构、扩容机制、线程安全等核心问题一知半解。本文将从基础到深入,带你全面掌握 ArrayList 的核心知识,帮你在实际开发中避坑提效。

一、ArrayList 是什么?------ 从定义到底层结构

1.1 ArrayList 的官方定位

ArrayList 是 Java 集合框架中java.util包下的一个动态数组实现类,它实现了 List 接口,继承自 AbstractList 抽象类,同时还实现了 Cloneable(支持克隆)、Serializable(支持序列化)、RandomAccess(支持随机访问)三个标记接口。

官方文档对 ArrayList 的描述是:"可动态调整大小的数组实现,允许存储所有类型的元素(包括 null),并提供了基于索引的快速访问能力"。简单来说,ArrayList 就是 "能自动扩容的数组"------ 解决了普通数组 "初始化后容量固定,无法动态添加元素" 的痛点。

1.2 ArrayList 与普通数组的区别

很多人会疑惑:"既然有普通数组,为什么还要用 ArrayList?" 其实两者的核心差异体现在 "灵活性" 和 "功能完整性" 上,具体对比如下:

特性 普通数组(Array) ArrayList
容量特性 初始化时必须指定容量,且不可变 容量动态扩展,无需手动指定初始值
元素操作 仅支持通过索引访问,无内置方法 提供 add/remove/get/contains 等丰富方法
存储类型 支持基本类型(int [])和引用类型 仅支持引用类型(存储基本类型需用包装类,如 Integer)
长度获取 通过 "数组名.length"(属性) 通过 "list.size ()"(方法)
空元素支持 引用类型数组可存 null,基本类型不行 支持存储任意数量的 null 元素

举个简单例子:如果需要存储一个 "不确定长度的用户列表",用普通数组会面临 "容量不够时需要手动创建新数组、复制元素" 的麻烦,而 ArrayList 会自动处理扩容,开发者只需专注于 "存数据" 即可。

1.3 ArrayList 的底层结构

ArrayList 的底层是通过一个Object 类型的数组(elementData) 来存储元素的,核心源码如下(基于 JDK 1.8):

java 复制代码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 序列化版本号
    private static final long serialVersionUID = 8683452581122892189L;

    // 默认初始容量(JDK 1.8中,无参构造初始化时不立即分配,第一次add时才扩容到10)
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组(无参构造初始化时使用)
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 默认空数组(区别于EMPTY_ELEMENTDATA,用于无参构造,标记"未初始化"状态)
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 底层存储元素的数组(transient修饰:序列化时需手动处理,避免序列化空元素)
    transient Object[] elementData;

    // 集合中实际存储的元素个数(注意:不是数组容量)
    private int size;

    // 省略其他方法...
}

从源码可以看出:

  • elementData是 ArrayList 的 "核心容器",所有元素都存在这个数组里;
  • size记录的是 "当前集合中元素的实际数量",而elementData.length才是 "数组的容量"(即能容纳的最大元素数);
  • transient修饰elementData:因为 ArrayList 的容量可能大于实际元素个数,序列化时只需要保存有值的元素,避免浪费空间,所以 ArrayList 重写了writeObjectreadObject方法手动处理序列化。

二、ArrayList 核心特性 ------ 必须掌握的 4 个关键点

在使用 ArrayList 前,必须先明确它的核心特性,这直接决定了它的适用场景和避坑方向。

2.1 有序性:元素存储顺序与插入顺序一致

ArrayList 是有序集合,这里的 "有序" 指的是 "元素的存储顺序与插入顺序完全一致",并且支持通过索引(0-based)精确访问元素。

举个例子:

java 复制代码
public class ArrayListOrderDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("苹果");
        list.add("香蕉");
        list.add("橙子");
        
        // 遍历集合:输出顺序与插入顺序一致
        for (int i = 0; i < list.size(); i++) {
            System.out.println(i + ":" + list.get(i));
        }
    }
}

输出结果:

java 复制代码
0:苹果
1:香蕉
2:橙子

这一点与 HashSet(无序)、HashMap(key 无序,JDK 1.8 后 LinkedHashMap 有序)形成鲜明对比,适合需要 "按插入顺序存储和访问" 的场景(如用户操作日志、订单列表)。

2.2 可重复性:允许存储重复元素

ArrayList 允许存储多个相同的元素(包括 null),这一点与 Set 接口(不允许重复元素)完全不同。

示例代码:

java 复制代码
public class ArrayListDuplicateDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10);
        list.add(20);
        list.add(10); // 重复元素
        list.add(null); // 存储null
        list.add(null); // 重复null
        
        System.out.println(list); // 输出:[10, 20, 10, null, null]
    }
}

需要注意的是:当调用contains(Object o)判断元素是否存在时,ArrayList 会通过equals()方法比较元素(null 元素会直接判断是否为 null),因此如果存储自定义对象,需要重写equals()方法才能正确判断重复。

2.3 随机访问:查询效率极高

ArrayList 实现了RandomAccess接口(标记接口,无实际方法),表示它支持 "随机访问"------ 即通过索引(get(int index))直接定位元素,时间复杂度为O(1)

这是因为底层是数组,数组的内存空间是 "连续的",通过 "数组首地址 + 索引 × 元素大小" 的计算方式,能直接找到元素的内存位置,无需像 LinkedList(链表结构)那样从头遍历。

示例:查询 ArrayList 和 LinkedList 的效率对比(以 100 万条数据为例)

java 复制代码
public class ArrayListAccessSpeedDemo {
    public static void main(String[] args) {
        // 初始化ArrayList和LinkedList,各存100万条数据
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < 1000000; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }

        // 测试ArrayList随机访问(访问第50万条数据)
        long start1 = System.currentTimeMillis();
        arrayList.get(500000);
        long end1 = System.currentTimeMillis();
        System.out.println("ArrayList随机访问时间:" + (end1 - start1) + "ms");

        // 测试LinkedList随机访问
        long start2 = System.currentTimeMillis();
        linkedList.get(500000);
        long end2 = System.currentTimeMillis();
        System.out.println("LinkedList随机访问时间:" + (end2 - start2) + "ms");
    }
}

输出结果(仅供参考):

java 复制代码
ArrayList随机访问时间:0ms
LinkedList随机访问时间:35ms

可以看到,ArrayList 的随机访问效率远超 LinkedList,这也是它成为 "查询密集型场景首选" 的核心原因。

2.4 非线程安全:多线程环境下需谨慎

ArrayList不是线程安全的(线程不安全的集合还有 HashMap、HashSet 等),在多线程同时对 ArrayList 进行 "添加 / 删除" 操作时,可能会出现两种问题:

  1. ConcurrentModificationException(并发修改异常):迭代器遍历过程中,其他线程修改了集合结构;
  2. 数据不一致:如元素丢失、数组越界等。

示例:多线程下的 ConcurrentModificationException

java 复制代码
public class ArrayListThreadSafeDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        // 线程1:添加元素
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                list.add("元素" + i);
            }
        }).start();

        // 线程2:遍历集合
        new Thread(() -> {
            Iterator<String> iterator = list.iterator();
            while (iterator.hasNext()) {
                System.out.println(iterator.next());
            }
        }).start();
    }
}

运行后大概率会抛出异常:

java 复制代码
Exception in thread "Thread-1" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at com.example.ArrayListThreadSafeDemo.lambda$main$1(ArrayListThreadSafeDemo.java:19)

关于线程安全的解决方案,后文会详细讲解,这里先记住:单线程环境用 ArrayList,多线程环境需额外处理线程安全

三、ArrayList 构造方法详解 ------3 种初始化方式

ArrayList 提供了 3 个常用的构造方法,不同的初始化方式对应不同的使用场景,理解它们的差异能帮助你更合理地初始化 ArrayList,避免不必要的性能消耗。

3.1 无参构造方法:ArrayList ()

无参构造是日常开发中最常用的方式,源码如下(JDK 1.8):

java 复制代码
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

这里有个关键细节:JDK 1.8 中,无参构造并不会立即创建容量为 10 的数组,而是将 elementData 赋值为 "空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA" ,只有当第一次调用add()方法时,才会触发扩容,将数组容量初始化为 10。

这种 "延迟初始化" 的设计是为了节省内存 ------ 如果创建了 ArrayList 但暂时不存元素,就不会占用 10 个 Object 的内存空间(尤其在创建大量空 ArrayList 时,优化效果明显)。

示例:无参构造的使用

java 复制代码
// 无参初始化:此时elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组)
List<String> list = new ArrayList<>();
// 第一次add:触发扩容,elementData容量变为10
list.add("第一次添加");

3.2 指定初始容量构造方法:ArrayList (int initialCapacity)

如果提前知道集合大概会存储多少元素,可以用这个构造方法指定初始容量,避免后续频繁扩容(扩容会消耗性能)。

源码如下:

java 复制代码
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 {
        // 初始容量<0:抛出非法参数异常
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

适用场景:已知元素数量的场景(如存储 100 个用户信息),示例:

java 复制代码
// 提前知道要存100个用户,指定初始容量100,避免扩容
List<User> userList = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
    userList.add(new User("用户" + i));
}

注意:如果指定的初始容量小于 0,会抛出IllegalArgumentException,开发中需避免这种错误。

3.3 传入集合构造方法:ArrayList (Collection<? extends E> c)

如果需要将其他集合(如 HashSet、LinkedList)中的元素 "复制" 到 ArrayList 中,可以用这个构造方法,源码如下:

java 复制代码
public ArrayList(Collection<? extends E> c) {
    // 将传入的集合转为数组,赋值给elementData
    elementData = c.toArray();
    // 如果数组长度>0:
    if ((size = elementData.length) != 0) {
        // 判断c.toArray()返回的是否是Object[]类型(避免某些集合的toArray()返回子类数组)
        if (elementData.getClass() != Object[].class) {
            // 复制为Object[]数组(确保底层是Object[],避免类型转换问题)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
        }
    } else {
        // 数组长度=0:使用空数组EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

示例:将 HashSet 的元素复制到 ArrayList 中

java 复制代码
// 创建HashSet(无序、无重复)
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");

// 将HashSet转为ArrayList(转为有序集合)
List<String> list = new ArrayList<>(set);
System.out.println(list); // 输出可能是[a, b, c]或其他顺序(取决于HashSet的哈希分布)

注意:c.toArray()可能返回非 Object [] 类型的数组(如某些自定义集合),因此源码中会通过Arrays.copyOf转为 Object [],确保 ArrayList 的底层数组类型正确。

相关推荐
十八旬6 分钟前
苍穹外卖项目实战(日记十)-记录实战教程及问题的解决方法-(day3-2)新增菜品功能完整版
java·开发语言·spring boot·mysql·idea·苍穹外卖
这周也會开心39 分钟前
Java-多态
java·开发语言
Forward♞41 分钟前
Qt——网络通信(UDP/TCP/HTTP)
开发语言·c++·qt
XH华42 分钟前
C语言第十三章自定义类型:联合和枚举
c语言·开发语言
2401_858286111 小时前
OS26.【Linux】进程程序替换(下)
linux·运维·服务器·开发语言·算法·exec·进程
草莓熊Lotso1 小时前
【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day13
c语言·开发语言·刷题·强化训练
一尘之中2 小时前
在Python 2.7中安装SQLAlchemy的完整指南
开发语言·python·ai写作
黄贵根2 小时前
使用JDK11标准 实现 图数据结构的增删查改遍历 可视化程序
java·开发语言·数据结构
电商数据girl2 小时前
Python 爬虫获得淘宝商品详情 数据【淘宝商品API】
大数据·开发语言·人工智能·爬虫·python·json·php
盒马盒马3 小时前
Rust:变量、常量与数据类型
开发语言·rust