网站首页 > 技术文章 正文
作为一名 Java 开发,你是不是经常在写代码时随手 new 一个 ArrayList?比如这样:List<String> list = new ArrayList<>(); 反正编译器不报错,运行起来也没毛病,久而久之就成了习惯。但你有没有想过 ——为什么很多资深开发在初始化 ArrayList 时,总会特意指定一个容量,比如 new ArrayList<>(100)? 这看似不起眼的小操作,背后藏着怎样的性能逻辑?今天咱们就从这个趣味问题切入,把 ArrayList 的 “扩容秘密” 彻底讲明白,以后写代码再也不踩内存和性能的坑。
ArrayList 的底层到底是啥?
要弄明白 “指定容量” 的意义,首先得清楚 ArrayList 的底层结构 —— 它本质上是一个动态数组,也就是说,它的底层是用数组来存储元素的,但和普通数组 “一旦创建容量就固定” 的特性不同,ArrayList 能根据元素的添加情况自动扩容。
咱们先回忆下普通数组的特点:比如String[] arr = new String[5]; 这个数组的容量就是 5,最多只能存 5 个元素,要是想存第 6 个,直接赋值就会报
ArrayIndexOutOfBoundsException异常。而 ArrayList 之所以 “动态”,就是因为它帮我们做了 “数组满了就扩容” 的工作,让我们不用手动处理数组容量的问题。
但这里有个关键问题:ArrayList 的 “自动扩容” 不是无中生有,而是有一套固定的逻辑。它默认的初始容量是多少?扩容时会扩大到原来的几倍?这些细节直接影响着代码的性能,也是我们今天要重点聊的核心。
不指定容量时,ArrayList 在偷偷做什么?
很多开发觉得 “不指定容量省事”,但你可能没意识到,ArrayList 在背后悄悄做了不少 “额外工作”,这些工作看似不影响功能,却会在不知不觉中消耗内存、拖慢性能。咱们一步步拆解这个过程:
首先,当你用new ArrayList<>()无参构造初始化时,ArrayList 的初始容量其实是0—— 没错,一开始它底层的数组是空的,连元素都装不下。那什么时候才会分配真正的容量呢?答案是当你添加第一个元素时。
比如你执行list.add("first")时,ArrayList 会先检查当前底层数组的容量:发现容量是 0,就会触发第一次扩容,此时会创建一个容量为10的新数组,然后把 “first” 这个元素存进去。这一步看起来没问题,但如果你的 list 最终要存 100 个元素,后续会发生什么?
当 list 里的元素存满 10 个时,你再添加第 11 个元素,ArrayList 会再次触发扩容。它的扩容逻辑是 “新容量 = 旧容量 + 旧容量 / 2”,也就是扩容到原来的 1.5 倍。第一次扩容后容量是 10,第二次扩容就会变成 15(10+5);当 15 个元素存满,第三次扩容会变成 22(15+7);接着是 33、49、73、109…… 直到容量能装下 100 个元素为止。
你算一算:从 0 到 100 个元素,ArrayList 一共要经历多少次扩容?答案是 7 次。每次扩容都要做两件事:创建一个新的更大容量的数组,然后把旧数组里的所有元素复制到新数组中。这两个操作都是 “耗时耗内存” 的 —— 创建新数组会占用额外的内存空间,复制元素会消耗 CPU 资源。更关键的是,旧数组在复制完成后会变成 “无用对象”,等待 GC(垃圾回收)处理,这又会增加 GC 的负担。
举个真实的例子:我之前在维护一个数据导入功能时,发现导入 10 万条数据用了近 2 秒,排查后发现,代码里用的是无参初始化的 ArrayList,底层一共扩容了 18 次,光数组复制就占用了近 30% 的耗时。后来改成指定容量new ArrayList<>(100000),导入时间直接降到了 0.8 秒,性能提升了 60%。这就是 “指定容量” 的实际价值 —— 不是玄学,是实实在在的性能优化。
怎么指定容量才合理?这 3 个方案帮你避开坑
既然指定容量这么重要,那到底该怎么定这个容量值?总不能拍脑袋随便写个数字,比如明明只存 10 个元素,却指定容量 1000,这反而会造成内存浪费。这里给大家 3 个实用方案,覆盖大部分开发场景:
方案 1:已知确切元素数量,直接指定该数量
这是最理想的场景,比如你要把数据库查询到的 100 条用户数据存到 ArrayList 里,查询结果返回时已经知道数量是 100,那直接初始化new ArrayList<>(100)就行。这样 ArrayList 一次扩容都不用做,底层数组刚好装下所有元素,内存和性能都最优。
举个代码示例:
// 从数据库查询用户,已知返回100条数据
List<User> userList = new ArrayList<>(100);
List<User> dbUsers = userDao.queryUsers(); // 返回100条数据
userList.addAll(dbUsers);
这里要注意:如果用addAll方法添加元素,要确保指定的容量大于等于待添加元素的数量,避免扩容。
方案 2:未知确切数量,但能估算范围,按 “估算值 + 10%” 指定
很多时候我们不知道元素的精确数量,但能大致估算范围。比如做 Excel 导出时,每次导出的行数在 500-800 之间,这种情况下可以按 “估算最大值 + 10%” 来指定容量,比如new ArrayList<>(880)(800 的 10% 是 80,800+80=880)。
为什么要多留 10%?因为实际业务中可能会有临时添加的元素,比如在导出数据里加一条 “合计行”,如果刚好按估算最大值指定容量,加合计行时又会触发一次扩容。多留 10% 的缓冲,既能避免额外扩容,又不会造成太多内存浪费。
代码示例:
// Excel导出,估算数据量500-800条,按880指定容量
List<ExportData> exportList = new ArrayList<>(880);
// 业务处理:添加500-800条数据
addExportData(exportList);
// 临时添加合计行
exportList.add(getTotalRow());
方案 3:完全无法估算,用 “初始容量 10” 不如用 “ArrayList (int initialCapacity)” 指定最小预期
如果实在无法估算元素数量,比如做一个用户输入数据的列表,用户可能输入 1 条,也可能输入 1000 条,这种情况下很多人会觉得 “那不如就用默认的无参构造”。但其实更优的做法是,根据业务的 “最小预期” 指定一个小容量,比如new ArrayList<>(20)。
为什么比默认的好?因为默认无参构造第一次扩容后容量是 10,要是用户输入了 11 条数据,就会触发第二次扩容到 15;而指定初始容量 20,能容纳 20 条数据,触发扩容的次数会比默认情况少。比如用户输入 25 条数据,默认构造需要扩容 3 次(0→10→15→22),而指定 20 容量只需要扩容 1 次(20→30),性能依然更优。
反思:看似 “小事” 的代码习惯,藏着开发的底层思维
聊到这里,可能有同学会说:“现在硬件配置这么高,这点性能差异用户根本感知不到,没必要这么较真吧?” 但我想跟大家说的是 ——对细节的把控,恰恰是普通开发和资深开发的差距所在。
比如在高并发场景下,一个服务每秒要处理上万次请求,如果每个请求里的 ArrayList 都多做 1 次扩容,那成千上万次请求累积起来的性能损耗,就会从 “感知不到” 变成 “服务卡顿”。再比如移动端开发,手机的内存和 CPU 资源有限,不必要的扩容会增加 App 的内存占用,甚至可能导致 OOM(内存溢出)崩溃。
更重要的是,“初始化 ArrayList 指定容量” 这个小习惯,背后体现的是 “提前规划资源” 的开发思维 —— 在写代码时,不仅要考虑 “功能能不能实现”,还要思考 “如何让代码更高效、更稳定”。就像盖房子,不仅要保证房子能住人,还要提前规划好承重、水电布局,这样房子才能住得安全、舒心。
最后问大家一个问题:你之前在项目中有没有因为 ArrayList 没指定容量踩过坑?或者你还有其他优化 ArrayList 性能的小技巧?欢迎在评论区分享,咱们一起交流学习,把 Java 基础打得更牢!
猜你喜欢
- 2025-10-23 JAVA中ArrayList、LinkedList及CopyOnWriteArrayList实现原理
- 2025-10-23 Java 开发者必看!3 个基础技能应用坑,90% 的人都踩过
- 2025-10-23 Java ArrayList基本操作及高级用法
- 2025-10-23 list可以一边遍历一边修改元素吗?
- 2025-10-23 Python:array数组比列表list更高效
- 2024-08-12 精解四大集合框架:List核心知识总结
- 2024-08-12 如何在 Flutter 中将 Map/Array 列表转换为 JSON 字符串
- 2024-08-12 夯实基础:Java 中初始化 List 集合的 6 种方式你都知道吧?
- 2024-08-12 Java基础-15总结数组,Collection,List
- 2024-08-12 并发中的List集合(并发集合和普通集合如何区别?)
- 最近发表
-
- 聊一下 gRPC 的 C++ 异步编程_grpc 异步流模式
- [原创首发]安全日志管理中心实战(3)——开源NIDS之suricata部署
- 超详细手把手搭建在ubuntu系统的FFmpeg环境
- Nginx运维之路(Docker多段构建新版本并增加第三方模
- 92.1K小星星,一款开源免费的远程桌面,让你告别付费远程控制!
- Go 人脸识别教程_piwigo人脸识别
- 安卓手机安装Termux——搭建移动服务器
- ubuntu 安装开发环境(c/c++ 15)_ubuntu安装c++编译器
- Rust开发环境搭建指南:从安装到镜像配置的零坑实践
- Windows系统安装VirtualBox构造本地Linux开发环境
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- c语言min函数头文件 (77)
- asynccallback (87)
- localstorage.removeitem (77)
- vector线程安全吗 (73)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 无效的列索引 (74)