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

深入解析ArrayList源码:从短链项目实战到底层原理

深入解析ArrayList源码:从短链项目实战到底层原理

前言

在Java开发中,ArrayList是我们最常用的集合类之一。本文将结合一个实际的短链项目,深入分析ArrayList的源码实现,帮助大家理解其底层原理和设计思想。

1. 项目背景:短链系统中的ArrayList应用

在我们的短链项目中,ArrayList被广泛使用。让我们先看几个典型的应用场景:

1.1 统计数据收集

// 短链接访问统计数据收集
List<ShortLinkStatsAccessDailyRespDTO> daily = new ArrayList<>();
List<ShortLinkStatsLocaleCNRespDTO> localeCnStats = new ArrayList<>();
List<Integer> hourStats = new ArrayList<>();

1.2 批量数据处理

// 批量创建短链接
List<ShortLinkBatchCreateRespDTO> result = new ArrayList<>();
for (int i = 0; i < requestParam.getOriginUrls().size(); i++) {// 处理每个URL并添加到结果列表result.add(createShortLink(createReqDTO));
}

1.3 查询结果存储

// 存储分组查询结果
List<GroupDO> groupDOList = baseMapper.selectList(queryWrapper);
if (CollUtil.isNotEmpty(groupDOList) && groupDOList.size() == groupMaxNum) {throw new ClientException("已超出最大分组数");
}

这些场景都体现了ArrayList的核心特点:动态扩容随机访问顺序存储

2. ArrayList核心源码解析

2.1 类定义和继承关系

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {// 默认初始容量private static final int DEFAULT_CAPACITY = 10;// 空数组实例private static final Object[] EMPTY_ELEMENTDATA = {};// 默认大小的空数组实例private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 存储ArrayList元素的数组缓冲区transient Object[] elementData;// ArrayList的大小(包含的元素数量)private int size;
}

关键点解析:

  • RandomAccess:标记接口,表示支持快速随机访问
  • transient Object[] elementData:实际存储数据的数组,transient表示不参与序列化
  • size:当前元素个数,注意区别于数组容量

2.2 构造方法深度解析

2.2.1 无参构造方法
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

在短链项目中的应用:

// 创建空的统计数据列表,后续动态添加
List<ShortLinkStatsAccessDailyRespDTO> daily = new ArrayList<>();

设计思想:

  • 延迟初始化:不立即分配内存,节省资源
  • 默认容量:首次添加元素时才分配默认容量10
2.2.2 指定容量构造方法
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);}
}

在短链项目中的优化应用:

// 如果知道大概的数据量,可以预设容量避免扩容
List<ShortLinkBatchCreateRespDTO> result = new ArrayList<>(requestParam.getOriginUrls().size());

2.3 核心方法源码解析

2.3.1 add方法 - 动态扩容的核心
public boolean add(E e) {modCount++;  // 修改次数+1,用于fail-fast机制add(e, elementData, size);return true;
}private void add(E e, Object[] elementData, int s) {if (s == elementData.length)elementData = grow();  // 扩容elementData[s] = e;size = s + 1;
}

扩容机制详解:

private Object[] grow() {return grow(size + 1);
}private Object[] grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, /* minimum growth */oldCapacity >> 1           /* preferred growth */);return elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容策略分析:

  1. 扩容时机:当前大小等于数组长度时
  2. 扩容大小:新容量 = 旧容量 + 旧容量/2(即1.5倍)
  3. 数据迁移:使用Arrays.copyOf复制到新数组

在短链项目中的性能影响:

// 不好的做法:频繁扩容
List<String> urls = new ArrayList<>();  // 默认容量10
for (int i = 0; i < 1000; i++) {urls.add("url" + i);  // 会触发多次扩容
}// 好的做法:预设容量
List<String> urls = new ArrayList<>(1000);  // 避免扩容
for (int i = 0; i < 1000; i++) {urls.add("url" + i);
}
2.3.2 get方法 - 随机访问的实现
public E get(int index) {Objects.checkIndex(index, size);  // 边界检查return elementData(index);
}E elementData(int index) {return (E) elementData[index];  // 直接数组访问
}

性能分析:

  • 时间复杂度:O(1)
  • 实现原理:直接通过数组下标访问

在短链项目中的应用:

// 高效的随机访问
List<ShortLinkStatsAccessDailyRespDTO> dailyStats = getDailyStats();
for (int i = 0; i < dailyStats.size(); i++) {ShortLinkStatsAccessDailyRespDTO stat = dailyStats.get(i);  // O(1)时间复杂度// 处理统计数据
}
2.3.3 remove方法 - 元素删除的实现
public E remove(int index) {Objects.checkIndex(index, size);final Object[] es = elementData;@SuppressWarnings("unchecked") E oldValue = (E) es[index];fastRemove(es, index);return oldValue;
}private void fastRemove(Object[] es, int i) {modCount++;final int newSize;if ((newSize = size - 1) > i)System.arraycopy(es, i + 1, es, i, newSize - i);  // 数组元素前移es[size = newSize] = null;  // 清除引用,帮助GC
}

删除机制分析:

  1. 元素移动:删除位置后的所有元素前移一位
  2. 时间复杂度:O(n),最坏情况需要移动n-1个元素
  3. 内存管理:将最后一个位置置null,帮助垃圾回收

3. ArrayList vs LinkedList:在短链项目中的选择

3.1 性能对比

操作ArrayListLinkedList短链项目场景
随机访问O(1)O(n)统计数据查询 ✓
头部插入O(n)O(1)很少使用
尾部插入O(1)*O(1)批量数据收集 ✓
中间插入O(n)O(1)**很少使用
删除O(n)O(1)**偶尔使用

摊销时间复杂度,可能触发扩容
需要先定位到节点

3.2 内存使用对比

// ArrayList内存布局(更紧凑)
[data1][data2][data3][null][null]...// LinkedList内存布局(额外指针开销)
[prev|data1|next] -> [prev|data2|next] -> [prev|data3|next]

在短链项目中的选择:

// 适合ArrayList的场景:统计数据展示
List<ShortLinkStatsAccessDailyRespDTO> dailyStats = new ArrayList<>();
// 频繁的随机访问,内存使用效率高// 不适合LinkedList的场景:
// 1. 需要根据索引快速访问统计数据
// 2. 数据量大时,LinkedList的指针开销明显

4. ArrayList的线程安全问题

4.1 并发修改异常

// 在短链项目中可能遇到的问题
List<String> urls = new ArrayList<>();
urls.add("url1");
urls.add("url2");// 遍历时修改会抛出ConcurrentModificationException
for (String url : urls) {if (url.contains("invalid")) {urls.remove(url);  // 危险操作!}
}

解决方案:

// 方案1:使用Iterator的remove方法
Iterator<String> iterator = urls.iterator();
while (iterator.hasNext()) {String url = iterator.next();if (url.contains("invalid")) {iterator.remove();  // 安全删除}
}// 方案2:使用removeIf方法(推荐)
urls.removeIf(url -> url.contains("invalid"));// 方案3:在短链项目中,使用线程安全的集合
List<String> safeUrls = Collections.synchronizedList(new ArrayList<>());

4.2 多线程环境下的选择

// 在短链项目的统计模块中
public class ShortLinkStatsService {// 线程不安全的做法private List<StatsRecord> records = new ArrayList<>();// 线程安全的做法private List<StatsRecord> safeRecords = new CopyOnWriteArrayList<>();// 或者使用同步private List<StatsRecord> syncRecords = Collections.synchronizedList(new ArrayList<>());
}

5. ArrayList性能优化实践

5.1 容量预设优化

// 在短链项目中的优化实践
public List<ShortLinkBatchCreateRespDTO> batchCreateShortLinks(ShortLinkBatchCreateReqDTO requestParam) {List<String> originUrls = requestParam.getOriginUrls();// 预设容量,避免扩容开销List<ShortLinkBatchCreateRespDTO> result = new ArrayList<>(originUrls.size());for (int i = 0; i < originUrls.size(); i++) {// 批量处理逻辑result.add(createSingleShortLink(originUrls.get(i)));}return result;
}

5.2 内存优化

// 及时释放不需要的引用
public void processLargeDataSet() {List<LargeObject> largeList = new ArrayList<>(10000);// 处理数据...// 处理完成后,及时清理largeList.clear();  // 清除所有元素largeList = null;   // 释放引用
}

5.3 批量操作优化

// 使用addAll进行批量添加
List<String> allUrls = new ArrayList<>();
List<String> batchUrls = getBatchUrls();// 好的做法:批量添加
allUrls.addAll(batchUrls);// 不好的做法:逐个添加
for (String url : batchUrls) {allUrls.add(url);  // 可能触发多次扩容
}

6. 实际项目中的最佳实践

6.1 合理选择初始容量

// 根据业务场景选择合适的初始容量
public class ShortLinkStatsCollector {public List<HourlyStats> collectHourlyStats() {// 一天24小时,预设容量24List<HourlyStats> hourlyStats = new ArrayList<>(24);for (int hour = 0; hour < 24; hour++) {hourlyStats.add(getStatsForHour(hour));}return hourlyStats;}public List<DailyStats> collectMonthlyStats() {// 一个月最多31天,预设容量31List<DailyStats> monthlyStats = new ArrayList<>(31);// ...return monthlyStats;}
}

6.2 避免不必要的装箱拆箱

// 对于基本类型,考虑使用专门的集合
// 不好的做法:
List<Integer> ids = new ArrayList<>();  // 会有装箱开销// 更好的做法:使用第三方库如Trove或Eclipse Collections
// TIntArrayList ids = new TIntArrayList();  // 避免装箱

6.3 合理使用Stream API

// 在短链项目中处理统计数据
public List<String> getActiveShortLinks(List<ShortLinkDO> allLinks) {return allLinks.stream().filter(link -> link.getEnableStatus() == 0)  // 过滤启用的链接.filter(link -> link.getDelFlag() == 0)       // 过滤未删除的链接.map(ShortLinkDO::getFullShortUrl)            // 提取URL.collect(Collectors.toList());                // 收集到新的ArrayList
}

7. 总结

通过对短链项目中ArrayList使用场景的分析,我们深入了解了ArrayList的:

  1. 底层实现:基于动态数组,支持自动扩容
  2. 性能特点:随机访问O(1),插入删除O(n)
  3. 扩容机制:1.5倍扩容,使用Arrays.copyOf迁移数据
  4. 线程安全:非线程安全,需要额外同步措施
  5. 优化策略:预设容量、批量操作、及时释放引用

在实际项目中,选择合适的集合类型需要根据具体的使用场景:

  • 频繁随机访问:选择ArrayList
  • 频繁插入删除:考虑LinkedList
  • 线程安全需求:使用CopyOnWriteArrayList或同步包装
  • 大量数据处理:注意内存使用和GC影响

希望这篇文章能帮助大家更好地理解和使用ArrayList,在实际项目中做出更好的技术选择!


参考资料:

  • OpenJDK ArrayList源码
  • 《Java并发编程实战》
  • 《Effective Java》第三版
http://www.xdnf.cn/news/1037593.html

相关文章:

  • windterm no match for method encryption client
  • 盟接之桥EDI软件安全机制及工作原理详解
  • uni-app项目实战笔记11--定义scss颜色变量方便页面引用
  • 论文略读: CITYANCHOR: CITY-SCALE 3D VISUAL GROUNDING WITH MULTI-MODALITY LLMS
  • 容器里有10升油,现在只有两个分别能装3升和7升油的瓶子,需要将10 升油等分成2 个5 升油。程序输出分油次数最少的详细操作过程。
  • 【leetcode】78. 子集
  • 2.2 状态空间表达式的解
  • 初探Qt信号与槽机制
  • 21 - GAM模块
  • 破壁虚实的情感科技革命:元晟定义AI陪伴机器人个性化新纪元
  • SpringBoot 自动化部署实战:从环境搭建到 CI/CD 全流程
  • vulnyx Diff3r3ntS3c writeup
  • CLONE:用于长距离任务的闭环全身人形机器人遥操作
  • C++之模板进阶
  • 多线程下 到底是事务内部开启锁 还是先加锁再开启事务?
  • 《人工智能时代与人类价值》读书简要笔记
  • [CVPR 2025] DeformCL:基于可变形中心线的3D血管提取新范式
  • Docker全平台安装指南:从零到一构建容器化环境(满级版)
  • GDI+ 中与GDI32取图形区域函数对比CreateEllipticRgn/CreatePolygonRgn
  • g++ a.cpp -o a ‘pkg-config --cflags --libs opencv4‘/usr/bin/ld: 找不到 没有那个文件或目录
  • [智能客服project] AI提示词配置 | 主协调器 | 闲鱼协议工具
  • PX4无人机|MID360使用FAST_LIO,实现自主定位及定点——PX4无人机配置流程(五)
  • Vue Methods 实现原理详解
  • 【数据集成与ETL 04】dbt实战指南:现代化数据转换与SQL代码管理最佳实践
  • 一个前端正则校验引发的问题
  • 马上行计划管理后端架构
  • 深度分析Javascript中的Promise
  • 动态多目标进化算法:基于迁移学习的动态多目标遗传算法Tr-NSGA-II求解CEC2015,提供完整MATLAB代码
  • python基础与数据类型
  • C# 枚 举(枚举)