当前位置: 首页 > news >正文

深入浅出 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):

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)精确访问元素。

举个例子:

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));}}
}

输出结果:

0:苹果
1:香蕉
2:橙子

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

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

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

示例代码:

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); // 存储nulllist.add(null); // 重复nullSystem.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 万条数据为例)

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");}
}

输出结果(仅供参考):

ArrayList随机访问时间:0ms
LinkedList随机访问时间:35ms

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

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

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

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

示例:多线程下的 ConcurrentModificationException

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();}
}

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

Exception in thread "Thread-1" java.util.ConcurrentModificationExceptionat 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):

public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

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

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

示例:无参构造的使用

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

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

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

源码如下:

public ArrayList(int initialCapacity) {if (initialCapacity > 0) {// 初始容量>0:创建指定容量的Object数组this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {// 初始容量=0:使用空数组EMPTY_ELEMENTDATAthis.elementData = EMPTY_ELEMENTDATA;} else {// 初始容量<0:抛出非法参数异常throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);}
}

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

// 提前知道要存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 中,可以用这个构造方法,源码如下:

public ArrayList(Collection<? extends E> c) {// 将传入的集合转为数组,赋值给elementDataelementData = 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_ELEMENTDATAthis.elementData = EMPTY_ELEMENTDATA;}
}

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

// 创建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 的底层数组类型正确。

http://www.dtcms.com/a/350893.html

相关文章:

  • vue-Router中通过路由地址path中的数据转换为props传参,不建议添加多个可选参数
  • More Effective C++ 条款07:不要重载、和,操作符
  • linux的conda配置与应用阶段的简单指令备注
  • Typora + PicList + Gitee 图床完整配置教程
  • 《P1656 炸铁路》
  • C++ 编译链接杂谈——前向声明
  • JavaScript 类中静态变量与私有变量的区别及用法
  • eniac:世界上第一台通用电子计算机的传奇
  • 开发避坑指南(36):Java字符串Base64编码实战指南
  • 深度学习-----《PyTorch深度学习核心应用解析:从环境搭建到模型优化的完整实践指南》
  • 初步了解多线程
  • 交换机是如何同时完成帧统计与 BER/FEC 分析的
  • 【应急响应工具教程】SPECTR3:通过便携式 iSCSI 实现远程证据的只读获取与分析
  • [pilot智驾系统] 模型守护进程(modeld)
  • rbio1:以生物学世界模型为软验证器训练科学推理大语言模型
  • 面试八股文之——JAVA基础
  • 深度学习梯度下降与交叉熵损失
  • 重塑企业沟通与增长:云蝠智能大模型如何成为您的智能语音中枢
  • 大模型(一)什么是 MCP?如何使用 Charry Studio 集成 MCP?
  • SQL查询-设置局部变量(PostgreSQL、MySQL)
  • 嵌入式学习 day58 驱动字符设备驱动
  • 玳瑁的嵌入式日记D25-0825(进程)
  • Java全栈开发实战:从Spring Boot到Vue3的项目实践
  • Android Glide 缓存机制深度解析与优化:从原理到极致实践
  • 集成电路学习:什么是ONNX开放神经网络交换
  • 深度学习③【卷积神经网络(CNN)详解:从卷积核到特征提取的视觉革命(概念篇)】
  • 详解 Transformer 激活值的内存占用公式
  • SOME/IP-SD报文中 Entry Format(条目格式)-理解笔记5
  • 算法题记录01:
  • 0826xd