使用 ArrayList 应当避免的坑

news/2025/1/14 6:08:27/

大家都知道 ArrayList 是由数组实现,而数据的长度有限,需要在合适的时机对数组扩容。

当我们初始化一个长度为 2 的 ArrayList ,并往里边写入三条数据时 ArrayList 就得扩容了,也就是将之前的数据复制一份到新的数组长度为 3 的数组中。

以下是扩容的源码,之所以是 3 ,是因为新的长度=原有长度 * 1.5

private void grow(int minCapacity) {int oldCapacity = elementData.length;// 新容量为旧容量的1.5倍int newCapacity = oldCapacity + (oldCapacity >> 1);// 如果新容量发现比需要的容量还小,则以需要的容量为准if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 如果新容量已经超过最大容量了,则使用最大容量if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 以新容量拷贝出来一个新数组elementData = Arrays.copyOf(elementData, newCapacity);
}

通过源码我们可以得知 ArrayList 的默认长度为 10。

/*** 默认容量*/
private static final int DEFAULT_CAPACITY = 10;

但其实并不是在初始化的时候就创建了 DEFAULT_CAPACITY = 10 的数组。而是在往里边 add 第一个数据的时候会扩容到 10。

private static int calculateCapacity(Object[] elementData, int minCapacity) {// 如果是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就初始化为默认大小10if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;
}private void ensureExplicitCapacity(int minCapacity) {modCount++;if (minCapacity - elementData.length > 0)// 扩容grow(minCapacity);
}

既然知道了默认的长度为 10 ,那说明后续一旦写入到第九个元素的时候就会扩容为 10 * 1.5 = 15。这一步为数组复制,也就是要重新开辟一块新的内存空间存放这 15 个数组。一旦我们频繁且数量巨大的进行写入时就会导致许多的数组复制,这个效率是极低的。

但如果我们提前预知了可能会写入多少条数据时就可以提前避免这个问题。比如我们往里边写入 1000W 条数据,在初始化的时候就给定数组长度与用默认 10 的长度之间性能是差距巨大的。

用 JMH 基准测试验证一下。

JMH 是 Java Microbenchmark Harness 的缩写。中文意思大致是 “JAVA 微基准测试套件”。首先先明白什么是“基准测试”。

百度百科给的定义如下:

基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

可以简单的类比成我们电脑常用的鲁大师,或者手机常用的跑分软件安兔兔之类的性能检测软件。都是按一定的基准或者在特定条件下去测试某一对象的的性能,比如显卡、IO、CPU之类的。

为什么要使用 JMH

基准测试的特质有如下几种:

  • 可重复性:可进行重复性的测试,这样做有利于比较每次的测试结果,得到性能结果的长期变化趋势,为系统调优和上线前的容量规划做参考。

  • 可观测性:通过全方位的监控(包括测试开始到结束,执行机、服务器、数据库),及时了解和分析测试过程发生了什么。

  • 可展示性:相关人员可以直观明了的了解测试结果(web界面、仪表盘、折线图树状图等形式)。

  • 真实性:测试的结果反映了客户体验到的真实的情况(真实准确的业务场景+与生产一致的配置+合理正确的测试方法)。

  • 可执行性:相关人员可以快速的进行测试验证修改调优(可定位可分析)。

可见要做一次符合特质的基准测试,是很繁琐也很困难的。外界因素很容易影响到最终的测试结果。特别对于 JAVA的基准测试。

你运行的次数与时间不同可能获得的结果也不同,很难获得一个比较稳定的结果。

对于这种情况,有一个解决办法就是大量的重复调用,并且在真正测试前还要进行一定的预热,使结果尽可能的准确。

除了这些,对于结果我们还需要一个很好的展示,可以让我们通过这些展示结果判断性能的好坏。

而这些JMH都有!😊

如何使用 JMH

JMH是 JDK9自带的,如果你是 JDK9 之前的版本也可以通过导入 openjdk

<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.19</version>
</dependency>
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.19</version>
</dependency>

下面就用实例来演示一下:ArrayList 在初始化的时候就给定数组长度与用默认 10 的长度之间性能是差距巨大的。

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class CollectionsTest {private static final int TEN_MILLION = 10000000;@Benchmark@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MICROSECONDS)public void arrayList(){List<String> array = new ArrayList<>();for (int i = 0; i < TEN_MILLION; i++) {array.add("123");}}@Benchmark@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MICROSECONDS)public void arrayListSize(){List<String> array = new ArrayList<>(TEN_MILLION);for (int i = 0; i < TEN_MILLION; i++){array.add("123");}}public static void main(String[] args) throws RunnerException{Options opt = new OptionsBuilder().include(CollectionsTest.class.getSimpleName()).forks(1).build();new Runner(opt).run();}
}

运行结果:

# Run complete. Total time: 00:00:23Benchmark                      Mode  Cnt      Score       Error  Units
CollectionsTest.arrayList      avgt    5  50264.850 ±  6723.299  us/op
CollectionsTest.arrayListSize  avgt    5  38389.625 ± 14797.446  us/op

根据结果可以看出预设长度的效率会比用默认的效率高上很多(这里的 Score 指执行完函数所消耗的时间)。

所以这里强烈建议大家:在有大量数据写入 ArrayList 时,一定要初始化指定长度。

一定要慎用 add(int index, E element) 向指定位置写入数据,源码如下:

public void add(int index, E element) {// 检查是否越界rangeCheckForAdd(index);// 检查是否需要扩容ensureCapacityInternal(size + 1);// 将inex及其之后的元素往后挪一位,则index位置处就空出来了System.arraycopy(elementData, index, elementData, index + 1,size - index);// 将元素插入到index的位置elementData[index] = element;// 大小增1size++;
}

通过源码我们可以看出,每一次写入都会将 index 后的数据往后移动一遍,其实本质也是要复制数组;但区别于往常规的往数组尾部写入数据,它每次都会进行数组复制,效率极低。

总结

高性能应用都是从小细节一点点堆砌起来的,就如这里提到的 ArrayList 的坑一样,日常使用没啥大问题,一旦数据量起来所有的小问题都会成为大问题。

  • 使用 ArrayList 时如果能提前预测到数据量大小,比较大时一定要指定其长度。
  • 尽可能避免使用 add(index,e) api,会导致复制数组,降低效率。
  • 再额外提一点,我们常用的另一个 Map 容器 HashMap 也是推荐要初始化长度从而避免扩容。

http://www.ppmy.cn/news/640541.html

相关文章

桃核做口哨

小时候,我们都很会玩,记得我们常常把桃核从一面的中间挖个圆形小孔,把里面的桃核仁一点一点弄出来.让里面成为中空,这样就可以当哨子用了,声音非常悦耳

微信小程序:王者荣耀出装与铭文推荐助手

这是一款王者荣耀助手的一款小程序源码 该小程序主要功能就是提供各个英雄出装和铭文查询 功能虽然单调但是对于玩王者的朋友来说还是挺实用的! 目前该小程序源码已支持多种流量主模式 小程序源码下载地址&#xff1a; 微信小程序&#xff1a;王者荣耀出装与铭文推荐助手-小程…

直播过程中的掌声,口哨,背景音等音效怎么实现

大家有没有想过在直播过程中的掌声&#xff0c;口哨&#xff0c;背景音等音效是怎么实现的吗?这些功能都是可以通过混音来实现的。本篇文章介绍即构科技音视频SDK高级功能第五篇&#xff0c;ZegoLiveRoom SDK 混音功能&#xff0c;还是以iOS环境为例。 混音 1、功能简介 Ze…

王者荣耀故事站小程序源码/含vue后台

王者荣耀故事站小程序源码/含vue后台-PHP文档类资源-CSDN下载王者荣耀故事站小程序源码/含vue后台更多下载资源、学习资料请访问CSDN下载频道.https://download.csdn.net/download/swich_case/85068214

6.redis-哨兵

一.实战 1./myredis目录下放sentinel.conf文件 1) sentinel26379.conf 2) sentinel26380.conf 3) sentinel26381.conf 2.重点参数说明 bind 0.0.0.0 #服务监听位置 daemonize yes #后台运行 protected-mode no #安全保护模式 port 26379 …

微信小程序:王者荣耀改名神器

这是一款王者改名小程序 支持重复名改名 支持空白名改名 另外也支持特殊符合随机生成改名等等 该款小程序引流裂变的效果非常的好 支持流量主收益如激励视频获取改名次数等等 另外该小程序还有更多,支持推荐其它小程序形成一个轮廓 另外该小程序成本比较低,无需服务器无需…

【Redis—哨兵机制】

文章目录 概念哨兵机制如何工作的监控&#xff08;如何判断主节点真的故障了&#xff09;哪个哨兵进行主从故障转移&#xff1f;故障转移流程哨兵集群 概念 当进行主从复制时&#xff0c;如果主节点挂掉了&#xff0c;那么没有主节点来服务客户端的写操作请求了&#xff0c;也…

接口性能优化的11个小技巧(荣耀典藏版)

目录 前言 1.索引 1.1 没加索引 1.2 索引没生效 1.3 选错索引 2. sql优化 3. 远程调用 3.1 并行调用 3.2 数据异构 4. 重复调用 4.1 循环查数据库 4.2 死循环 4.3 无限递归 5. 异步处理 5.1 线程池 5.2 MQ 6. 避免大事务 7. 锁粒度 7.1 synchronized 7.2 r…