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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 常见的集合容器应当避免的坑

ArrayList 踩坑


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


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


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


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


比如我们在 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.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<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…


相关文章
|
3月前
|
存储 Java 容器
HashMap 的基本操作【集合容器知识回顾 ⑤】
本文介绍了HashMap的基本操作,包括创建对象、添加、获取、删除和替换元素、获取所有key的集合、遍历HashMap,以及如何存储自定义类型键值对,并强调了当使用自定义对象作为键时需要重写equals和hashCode方法以确保正确的行为。
HashMap 的基本操作【集合容器知识回顾 ⑤】
|
3月前
|
存储 Java 容器
HashSet 的基本操作【集合容器知识回顾 ④】
本文介绍了HashSet的基本操作,包括创建和初始化、添加和删除元素、判断元素存在性、获取集合大小、遍历、求交集差集、转换为数组和其他集合类型、比较两个HashSet,以及如何将自定义对象作为HashSet的元素时重写hashCode和equals方法,最后总结了HashSet的性能特点和使用注意事项。
HashSet 的基本操作【集合容器知识回顾 ④】
|
3月前
|
存储 安全 Java
ArrayList的基本操作【集合容器知识回顾 ②】
这篇文章详细介绍了ArrayList的基本操作,包括创建对象、添加和删除元素、获取和更新元素、遍历、判断元素存在性、集合的空值检查、批量操作、转换为数组、截取子集合、查找元素索引、克隆拷贝、清空集合以及容量管理等,同时指出了使用ArrayList时的注意事项,如线程安全性、容量管理、删除元素的性能、遍历时的修改、空值处理和性能优化。
ArrayList的基本操作【集合容器知识回顾 ②】
|
3月前
|
存储 安全 Java
集合概览【集合容器知识回顾 ①】
这篇文章是关于Java集合框架的全面介绍,包括集合的层次结构、常见集合类的特点、如何学习集合框架、泛型的应用、基本操作以及一些常用的集合操作技巧和注意事项。
集合概览【集合容器知识回顾 ①】
|
3月前
|
Java API 索引
LinkedList的基本操作【集合容器知识回顾 ③】
本文详细介绍了LinkedList的基本操作,包括初始化、添加、获取、删除、替换元素、遍历,以及LinkedList独有的队列和栈相关操作,同时指出了LinkedList在插入和删除操作方面的优势以及在随机访问元素时的性能劣势。
|
4月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
4月前
|
Java 容器
【Java集合类面试一】、 Java中有哪些容器(集合类)?
这篇文章列出了Java中的四大类集合接口:Set、List、Queue和Map,以及它们的常用实现类,如HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap和TreeMap。
【Java集合类面试一】、 Java中有哪些容器(集合类)?
|
5月前
|
存储 语音技术 Python
语音识别,函数综合案例,黑马ATM,/t/t一个对不齐,用两个/t,数据容器入门,数据容器可以分为列表(list)、元组(tuple)、字符串(str)、集合(set)、字典(dict)
语音识别,函数综合案例,黑马ATM,/t/t一个对不齐,用两个/t,数据容器入门,数据容器可以分为列表(list)、元组(tuple)、字符串(str)、集合(set)、字典(dict)
|
7月前
|
存储 Python 容器
Python 基础 笔记(八) 容器---元组、字典、集合
Python 基础 笔记(八) 容器---元组、字典、集合
55 4
|
7月前
|
存储 索引 容器
【qt】联合容器和集合容器
【qt】联合容器和集合容器
63 2