前言前不久和同事一起review一个job执行慢的问题时,发现很多朋友在编码实现功能的时候还是需要注意细节,所以这篇文章来了出去。ArrayList踩坑,首先我们看看这段代码有什么问题?其实在大多数情况下,是没有问题的。无非是循环往ArrayList中写入数据。但是在特殊情况下,比如这里getData()返回的数据非常大的时候,后面的temp.add(str)就会出现问题。比如我们在查看代码的时候,发现这里返回的数据有时候可以高达2000W。这时候ArrayList里面写的问题就凸显出来了。大家都知道ArrayList是用数组实现的,数据的长度是有限制的;需要适时扩充阵法。这里以最后插入为例add(Ee)。ArrayListtemp=newArrayList<>(2);temp.add("1");temp.add("2");temp.add("3");当我们初始化一个长度为2的ArrayList,并向其中写入三段数据时,ArrayList就不得不扩容,即将之前的数据复制到一个数组长度为3的新数组中。原因为什么是3是因为新的长度=原来的长度*1.5从源码可以知道ArrayList的默认长度是10。image但实际上DEFAULT_CAPACITY=10的数组在创建的时候并没有创建初始化。相反,当第一个数据被添加到它时,它将扩展到10。现在我们知道默认长度是10,也就是说一旦写入第9个元素,就会扩容为10*1.5=15。这一步就是数组复制,也就是开辟一个新的内存空间来存放这15个数组。一旦频繁大量写入,就会导致数组副本很多,效率极低。但是如果我们提前预测可能会写入多少条数据,就可以提前避免这个问题。比如我们往里面写入1000W条数据,初始化时给定的数组长度和默认的10长度存在巨大的性能差距。我使用JMHbenchmark测试验证如下:根据结果可以看出,预设长度的效率会比默认高很多(这里的Score指的是函数执行的时间).所以这里强烈建议大家:当ArrayList写入大量数据时,一定要初始化指定长度。另一种是谨慎使用add(intindex,Eelement)将数据写入指定位置。从源码中可以看出,每次写入都会将索引后的数据向后移动。其实本质就是复制数组;但它不同于常规方式向数组末尾写入数据,每次都会复制数组。效率极低。LinkedList提到ArrayList,就不得不说到LinkedList的孪生兄弟;虽然都是List容器,但是本质实现是完全不同的。LinkedList由链表组成,每个节点有两个节点,开头和结尾分别指代前后两个节点;因此它也是一个双向链表。所以理论上它的写法是非常高效的,不会出现ArrayList中极其低效的数组拷贝,每次只需要移动指针即可。这里偷懒就不画图了,大家自己脑补吧。市场上一直流传着对比测试:LinkedList的写效率比ArrayList高,所以写大于读的时候很适合LinkedList。这里测试一下,看结论是否一致;同样是向LinkedList写入1000W数据。从结果可以看出,ArrayList初始化数组长度的效率明显高于LinkedList。但是这里的前提是提前预设ArrayList的数组长度,避免数组扩容,这样ArrayList的写入效率非常高,而LinkedList不需要拷贝内存,但是需要创建对象,改变指针等操作。查询就不用说了,ArrayList可以支持下标随机访问,效率很高。由于LinkedList底层不是数组,所以不支持通过下标访问。而是需要根据查询索引的位置判断是从头遍历还是从尾遍历。但是不管是什么,都需要移动指针一点一点遍历,尤其是当index靠近中间位置的时候,会很慢。综上所述,高性能应用都是从小细节构建起来的。就像这里说的ArrayList这个坑,日常使用没什么大问题。一旦数据量增加,所有的小问题都会变成大问题。所以总结一下:在使用ArrayList的时候,如果可以提前预知数据的大小,那么一定要在数据比较大的时候指定它的长度。尽量避免使用add(index,e)api,会导致数组被拷贝,效率降低。补充一点,另外一个常用的Map容器??HashMap也推荐初始化长度,避免膨胀。