已知集合元素总数时应预分配数组还是动态调用add()创建?
明确知道集合最终元素数时的选择结论
你猜的没错,提前利用已知总数做容量预分配的方案确实更优,不管是直接用定长原生数组,还是给ArrayList指定初始容量,都比无参构造后逐次调用add()的效率高。
先给你确认:ArrayList.add()扩容时确实会做很多额外操作
默认无参构造出来的ArrayList,内部存引用的数组初始容量只有10(JDK8之后的实现逻辑,旧版本差异不影响核心结论),每次调用add()发现剩余空间不够时,会走一整套完全可以避免的扩容流程:
- 先算新的数组长度:默认按现有长度的1.5倍扩容
- 在堆上申请一块新长度的连续数组内存
- 把旧数组里的所有元素引用全量复制到新数组里
- 旧数组直接变成待回收的垃圾对象,等GC清理
- 最后才把新元素放到新数组的对应位置,更新集合的size计数
就拿你要存几百个对象的场景算,从初始10容量开始,存到800个元素要触发11次扩容,每次都要做一次数组拷贝。虽然拷贝引用的速度不算慢,但这些操作全是没有任何价值的冗余开销。
你提到的内存碎片化差异是真实存在的
两种方案的内存表现确实有区别,但没有到会严重影响程序运行的程度:
- 直接创建
MyObject[n]定长数组,是一次性申请一整块刚好够用的连续内存存所有对象引用,整个填充过程不会产生多余的临时对象,内存布局最规整,完全不会给GC添额外的活 - 用无参ArrayList逐次
add()的话,扩容过程中会生成好几个用完就丢的临时数组,这些临时对象会在堆里产生零散的内存碎片,虽然现代JVM的GC基本都能处理这些碎片,但属于完全可以规避的无意义消耗 - 这里要补个很多人容易漏的点:ArrayList不是必须走动态扩容的,你调用构造方法的时候直接传元素总数
new ArrayList<MyObject>(n),它内部会直接创建一个长度为n的底层数组,后续add()只要不超过这个长度,根本不会触发扩容,内存表现和你自己新建原生数组几乎没差别,只是多了一层极薄的封装,开销可以忽略。
针对你这个场景的选择建议
你提到MyObject本身内存大、实例化慢,这部分的开销才是整个流程的绝对大头,集合层面的性能占比其实很低,不用过度焦虑:
- 如果你后续不需要对这个集合做动态增删,也不需要用到List接口的各类方法,直接建
MyObject[n]定长数组按索引赋值就行,这是理论上效率最高的方案 - 如果你后续需要用到List的迭代、增删、配合Collections工具类等能力,别用无参构造的ArrayList,直接在构造时传入你已知的元素总数,之后正常调用
add()就行,实际运行性能和原生数组没有可感知的差距 - 别信什么“几百个元素那点性能差异无所谓”的政治正确,写代码的时候顺手就能避免的无意义开销,没必要留着。
内容的提问来源于stack exchange,提问作者AlexHomeBrew
相关产品推荐
相关产品推荐

