资讯专栏INFORMATION COLUMN

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

GraphQuery / 1780人阅读

摘要:尽可能避免使用,会导致复制数组,降低效率。再额外提一点,我们常用的另一个容器也是推荐要初始化长度从而避免扩容。

前言

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

ArrayList 踩坑
List temp = new ArrayList() ;

//获取一批数据
List all = getData();
for(String str : all) {
    temp.add(str);
}

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

其实在大部分情况下这都是没啥问题,无非就是循环的往 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 基准测试验证如下:
@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 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 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 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 也是推荐要初始化长度从而避免扩容。

本文所有测试代码:

https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/basic/CollectionsTest.java

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75161.html

相关文章

  • 技术干货 | Docker容器中需要避免十种常见误区

    摘要:第二具备轻量化特性容器的体积非常小巧。他们大多认为自己应该将应用程序部署至当前正在运行的容器当中。不要创建大型镜像体积过大的镜像会加大其发布难度。总体来讲,在向生产环境中部署容器时,必须避免使用最新标签。 当下最火爆的Docker,是一个开源的应用容器引擎。大家已经开始认同并接受容器技术,并意识到它能够解决多种现实问题并具备一系列无可比拟的优势。今天小数就和大家聊一聊容器技术的优势和误...

    Gu_Yan 评论0 收藏0
  • 精读《高性能 javascript》

    摘要:嵌套对象成员会造成重大性能影响尽量少用。一般来说你可以通过这种方法提高代码的性能将经常使用的对象成员数组项和域外变量存入局部变量中。在反复访问的地方使用局部变量存放引用小心地处理集合因为他们表现出存在性总是对底层文档重新查询。 前言 本期我来给大家推荐的书是《高性能JavaScript》,在这本书中我们能够了解 javascript 开发过程中的性能瓶颈,如何提升各方面的性能,包括代码...

    caohaoyu 评论0 收藏0
  • 使用React 应当注意几个地方

    摘要:都会造成错误,注意一定一定严格的用,所以我建议直接复制我的。因为用的话他会转义代码,写不写其实一个样。不可避免的,构建肯定是要用到的。这个时候一般用的是在外面保存然后里面调用第二个坑更隐蔽。 目标人群 献给熟悉基础的React语法的刚接触React的同学~ 如果你已经写过半年以上的React那也不用看了,毕竟我水平并不高 Whats React React 是一个不存在的网络公司Fac...

    高璐 评论0 收藏0
  • 总结开发过程踩到坑(三)

    摘要:本文章旨在总结开发过程中碰到的容易忘记或者比较重要的坑,一方面加深自己对于该部分的理解,另一方面希望能够分享给大家,知识在于分享,当然踩过的坑也不例外滑稽。 在日常工作中,时常会碰到各种各样的坑,有时真的觉得很多时候开发的经验都是踩坑踩出来的。在通往大牛的道路上,希望自己能够跨越重重阻碍,越走越远。学会时常总结,不断提升自己。 本文章旨在总结开发过程中碰到的容易忘记或者比较重要的坑,...

    April 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<