优秀的编程知识分享平台

网站首页 > 技术文章 正文

Java 开发必看!ArrayList 初始化为啥要指定容量?

nanyue 2025-10-23 08:39:20 技术文章 1 ℃

作为一名 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 基础打得更牢!

最近发表
标签列表