专栏名称: 石杉的架构笔记
专注原创、用心雕琢!十余年BAT一线大厂架构经验倾囊相授
目录
相关文章推荐
51好读  ›  专栏  ›  石杉的架构笔记

项目里用到 Java 集合时,应该避免的一些坑!

石杉的架构笔记  · 公众号  ·  · 2020-01-03 10:50

正文

公众号后台回复“ 学习 ”,免费获取精品学习资料

扫描下方海报 试听


本文来源:crossoverJie

前言

前不久帮同事一起 review 一个 job 执行缓慢的问题时发现不少朋友在撸码实现功能时还是有需要细节不够注意,于是便有了这篇文章。

ArrayList 踩坑

  1. List<String> temp = new ArrayList() ;


  2. //获取一批数据

  3. List<String> all = getData();

  4. for(String str : all) {

  5. temp.add(str);

  6. }

首先大家看看这段代码有什么问题嘛?

其实在大部分情况下这都是没啥问题,无非就是循环的往 ArrayList 中写入数据而已。

但在特殊情况下,比如这里的 getData () 返回数据非常巨大时后续 temp . add ( str ) 就会有问题了。

比如我们在 review 代码时发现这里返回的数据有时会高达 2000W,这时 ArrayList 写入的问题就凸显出来了。

填坑指南

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

这里以插入到尾部为例 add(E e)。

ArrayList temp = new ArrayList<>(2) ;temp.add("1");temp.add("2");temp.add("3");

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

之所以是 3 ,是因为新的长度=原有长度 * 1.5

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

但其实并不是在初始化的时候就创建了 DEFAULT_CAPACITY = 10 的数组。

而是在往里边 add 第一个数据的时候会扩容到 10.

既然知道了默认的长度为 10 ,那说明后续一旦写入到第九个元素的时候就会扩容为 10 * 1.5 = 15 。这一步为数组复制,也就是要重新开辟一块新的内存空间存放这 15 个数组。

一旦我们频繁且数量巨大的进行写入时就会导致许多的数组复制,这个效率是极低的。

但如果我们提前预知了可能会写入多少条数据时就可以提前避免这个问题。

比如我们往里边写入 1000W 条数据,在初始化的时候就给定数组长度与用默认 10 的长度之间性能是差距巨大的。

我用 JMH 基准测试验证如下:

  1. @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)

  2. @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)

  3. public class CollectionsTest {


  4. private static final int TEN_MILLION = 10000000;


  5. @Benchmark

  6. @BenchmarkMode(Mode.AverageTime)

  7. @OutputTimeUnit(TimeUnit.MICROSECONDS)

  8. public void arrayList() {


  9. List<String> array = new ArrayList<>();


  10. for (int i = 0; i < TEN_MILLION; i++) {

  11. array.add( "123");

  12. }


  13. }


  14. @Benchmark

  15. @BenchmarkMode(Mode.AverageTime)

  16. @OutputTimeUnit(TimeUnit.MICROSECONDS)

  17. public void arrayListSize() {

  18. List<String> array = new ArrayList<>(TEN_MILLION);


  19. for (int i = 0; i < TEN_MILLION; i++) {

  20. array.add("123");

  21. }


  22. }



  23. public static void main(String[] args) throws RunnerException {

  24. Options opt = new OptionsBuilder()

  25. .include(CollectionsTest.class.getSimpleName())

  26. .forks(1)

  27. .build();



  28. new Runner(opt).run();

  29. }

  30. }

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

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


再一个是一定要慎用 add ( int index , E element ) 向指定位置写入数据。

通过源码我们可以看出,每一次写入都会将 index 后的数据往后移动一遍,其实本质也是要复制数组;

但区别于往常规的往数组尾部写入数据,它每次都会进行数组复制,效率极低。

LinkedList

提到 ArrayList 就不得不聊下 LinkedList 这个孪生兄弟;虽说都是 List 的容器,但本质实现却完全不同。

LinkedList 是由链表组成,每个节点又有头尾两个节点分别引用了前后两个节点;因此它也是一个双向链表。

所以理论上来说它的写入非常高效,将不会有 ArrayList 中效率极低的数组复制,每次只需要移动指针即可。

这里偷懒就不画图了,大家自行脑补下。

对比测试

坊间一直流传:

LinkedList 的写入效率高于 ArrayList,所以在写大于读的时候非常适用于 LinkedList 。







请到「今天看啥」查看全文