常见的集合容器应当避免的坑

前言

前不久帮同事一块儿 review 一个 job 执行缓慢的问题时发现很多朋友在撸码实现功能时仍是有须要细节不够注意,因而便有了这篇文章。java

ArrayList 踩坑

List<String> temp = new ArrayList() ;

//获取一批数据
List<String> all = getData();
for(String str : all) {
	temp.add(str);
}
复制代码

首先你们看看这段代码有什么问题嘛?git

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

但在特殊状况下,好比这里的 getData() 返回数据很是巨大时后续 temp.add(str) 就会有问题了。api

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

填坑指南

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

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

ArrayList<String> temp = new ArrayList<>(2) ;
temp.add("1");
temp.add("2");
temp.add("3");
复制代码

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

之因此是 3 ,是由于新的长度=原有长度 * 1.5ui

经过源码咱们能够得知 ArrayList 的默认长度为 10.spa

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

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

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

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

但若是咱们提早预知了可能会写入多少条数据时就能够提早避免这个问题。

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

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

@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();
    }
}
复制代码

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

因此这里强烈建议你们:在有大量数据写入 ArrayList 时,必定要初始化指定长度。


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

经过源码咱们能够看出,每一次写入都会将 index 后的数据日后移动一遍,其实本质也是要复制数组;

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

LinkedList

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

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

因此理论上来讲它的写入很是高效,将不会有 ArrayList 中效率极低的数组复制,每次只须要移动指针便可。

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

对比测试

坊间一直流传:

LinkedList 的写入效率高于 ArrayList,因此在写大于读的时候很是适用于 LinkedList 。

@Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public void linkedList() {
        List<String> array = new LinkedList<>();

        for (int i = 0; i < TEN_MILLION; i++) {
            array.add("123");
        }

    }
复制代码

这里测试看下结论是否符合;一样的也是对 LinkedList 写入 1000W 次数据,经过结果来看初始化数组长度的 ArrayList 效率明显是要高于 LinkedList

但这里的前提是要提早预设 ArrayList 的数组长度,避免数组扩容,这样 ArrayList 的写入效率是很是高的,而 LinkedList 的虽然不须要复制内存,但却须要建立对象,变换指针等操做。

而查询就不用多说了,ArrayList 能够支持下标随机访问,效率很是高。

LinkedList 因为底层不是数组,不支持经过下标访问,而是须要根据查询 index 所在的位置来判断是从头仍是从尾进行遍历。

但不论是哪一种都得须要移动指针来一个个遍历,特别是 index 靠近中间位置时将会很是慢。

总结

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

因此再总结下:

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

本文全部测试代码:

github.com/crossoverJi…

你的点赞与分享是对我最大的支持

相关文章
相关标签/搜索