ArrayList与LinkedList的性能分析

简介: List下主流子类的性能比较,让你大跌眼镜。

1、 前言




经常在面试时,被问到集合的概念,集合 List、Map、Set 等底层设计以及其使用场景与注意细节。但大部分人的回答都是千篇一律,跟网上的答案一模一样,这是致命滴。其实,大家都错了,尤其是网上,更是误导大家,详细原因,且听我来分析。






2、集合 List




2.1 大家心中的 List

在广大的网友心中,List 是一个缓存数据的容器,是 JDK 为开发者提供的一种集合类型。面试时,被问到最常见的就是 ArrayList 和 LinkedList 的区别。


相信大部分网友都能回答上:ArrayList 是基于数组实现,LinkedList 是基于链表实现。而在使用场景时,我发现大部分网友的答案都是:在新增、删除操作时,LinkedList 的效率要高于 ArrayList,而在查询、遍历操作的时候,ArrayList 的效率要高于 LinkedList。这个答案是否准确呢?今天就带大家验证一哈。


2.2 性能比较

首先,大家都知道 ArrayList、LinkedList 都继承了 AbstractList 抽象类,而 AbstractList 实现了 List 接口。ArrayList 使用数组实现,而 LinkedList 使用了双向链表实现。接下来,我们就详细地分析下 ArrayList 和 LinkedList 的性能。



public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

在源码中,我们知道 ArrayList 除了实现克隆和序列化,还实现了 RandomAccess 接口。大家可能会对这个接口比较陌生,通过代码我们可以发现,这个接口其实是一个空接口,没有实现逻辑,那么 ArrayList 为什么要实现它呢?原来 RandomAccess 接口是一个标志接口,它标志着只要实现该接口,就能实现快速随机访问。


至于 ArrayList、LinkedList 的各种操作方法这里不再说了,大家可以看 [](https://mp.weixin.qq.com/s?__biz=MzA5NTE1MjY0NQ==&mid=2648832131&idx=1&sn=27dd4c28a480600cd74f92285a83e781&chksm=8856e7e9bf216effd0c4f78eaab940b2e13ab452e24661df74f4861dadc6cd9161b7f629913b&token=13659584&lang=zh_CN#rd) 这一篇。


接下来,我们看看一些测试数据,以测试 50000 次为例:


  1. ArrayList、LinkedList 新增测试
头部:ArrayList.Time 大于 LinkedList.Time
中间:ArrayList.Time 小于 LinkedList.Time
末尾:ArrayList.Time 小于 LinkedList.Time

通过这测试,我们可以看到 LinkedList 新增元素的未必要快于 ArrayList。

由于 ArrayList 是数组实现的,而数组是一块连续的内存空间,在新增元素到数组头部的时候,需要对头部以后的数据进行重排,所以效率很低。而 LinkedList 是基于链表实现,在新增元素的时候,首先会通过循环查找到新增元素的位置,如果要新增的位置处于前半段,就从前往后找;若其位置处于后半段,就从后往前找。故 LinkedList 新增元素到头部是非常高效的。

在中间位置插入时,ArrayList 同样有部分数据需要重排,效率也不是很高,而 LinkedList 将元素新增到中间,耗时最久的,因为靠近中间位置,在新增元素之前的循环查找是遍历元素最多的操作。

而在尾部操作时,发现在没有扩容的前提下,ArrayList 的效率要高于 LinkedList。这是因为 ArrayList 在新增元素到尾部的时候,不需要复制、重排,效率非常高。而 LinkedList 虽然也不用循环查找元素,但 LinkedList 中多了 new 对象以及变换指针指向对象的逻辑,所以要耗时多于 ArrayList 的操作。

 public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}


  1. ArrayList、LinkedList 删除测试
头部:ArrayList.Time 大于 LinkedList.Time
中间:ArrayList.Time 小于 LinkedList.Time
末尾:ArrayList.Time 小于 LinkedList.Time


大家会发现 ArrayList 和 LinkedList 删除操作的测试结果和新增的结果很接近,这是一样的道理,我就不赘述了。


  1. ArrayList、LinkedList 遍历测试
for循环:ArrayList.Time 小于 LinkedList.Time
迭代器:ArrayList.Time 几乎等于 LinkedList.Time


我们可以看到,LinkedList 的 for 循环遍历比不上 ArrayList 的 for 循环。这是因为 LinkedList 基于链表实现的,在使用 for 循环的时候,每一次 for 循环都会去遍历大半个 List,所以严重影响了遍历的效率。而 ArrayList 是基于数组实现的,并且实现了 RandomAccess 接口标志,意味着 ArrayList 可以实现快速随机访问,所以 for 循环非常快。LinkedList 的迭代遍历和 ArrayList 的迭代性能差不多,也不会太差,所以在遍历 LinkedList 时,我们要使用迭代循环遍历。






3、常错点




思考

在一次 ArrayList 删除操作的过程中,有下面两种写法:


public static void removeA(ArrayList<String> l) {
  for (String s : l){
      if (s.equals("aaa")) {
        l.remove(s);
      }
  }
}



public static void removeB(ArrayList<String> l) {
    Iterator<String> it = l.iterator();
    while (it.hasNext()) {
        String str = it.next();
        if (str.equals("aaa")) {
            it.remove();
        }
    }

}


第一种写法错误,第二种是正确的,原因是上面的两种写法都有用到 list 内部迭代器Iterator,即遍历时,ArrayList 内部创建了一个内部迭代器 iterator,在使用 next 方法来取下一个元素时,会使用 ArrayList 里保存的一个用来记录 list 修改次数的变量 modCount,与 iterator 保存了一个叫 expectedModCount 的表示期望的修改次数进行比较,如果不相等则会抛出一个叫 ConcurrentModificationException 的异常。且在 for 循环中调用 list 中的 remove 方法,会走到一个 fastRemove 方法,该方法不是 iterator 中的方法,而是 ArrayList 中的方法,在该方法只做了 modCount++,而没有同步到 expectedModCount。所以不一致就抛出了 ConcurrentModificationException 异常了。


下面是 ArrayList 自己的remove 方法:

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}


如果有看过阿里 Java 编程规范就知道,在集合中进行 remove 操作时,不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

相关文章
|
负载均衡 Java 物联网
SpringCloud简介和用处
SpringCloud简介和用处
469 0
|
搜索推荐 数据挖掘 数据管理
短链接系统精选:打造高效网络分享体验
在互联网时代,短链接系统扮演着重要角色,将长网址转化为简洁、易记的字符串。本文介绍了四款知名服务:行业标准的Bitly,提供详细统计和定制功能;简洁的TinyURL,操作简便;品牌化的Rebrandly,支持自定义域名以增强营销效果;以及DZ_tech/ShortURL,提供轻量级的私有部署方案。选择合适的短链接服务能优化用户体验,助力数据分析和营销。
|
10月前
|
机器学习/深度学习 存储 测试技术
RT-DETR改进策略【模型轻量化】| EMO:ICCV 2023,结构简洁的轻量化自注意力模型
RT-DETR改进策略【模型轻量化】| EMO:ICCV 2023,结构简洁的轻量化自注意力模型
394 0
RT-DETR改进策略【模型轻量化】| EMO:ICCV 2023,结构简洁的轻量化自注意力模型
|
数据挖掘 BI 定位技术
南大通用GBase 8s 高级分组查询 —— GROUP BY ROLLUP介绍
本文详细介绍了GBase 8s数据库中GROUP BY ROLLUP的高级分组查询功能,涵盖基本概念、语法结构、应用示例及使用场景。ROLLUP支持多维度数据汇总,适用于销售分析、财务报表和用户统计等领域,提升数据汇总的灵活性与便捷性。
|
设计模式 SQL JSON
谷粒商城笔记+踩坑(8)——仓库管理
采购单维护-采购需求、 采购单维护-采购单、 仓库维护、商品库存:
谷粒商城笔记+踩坑(8)——仓库管理
|
存储 缓存 NoSQL
京东面试:亿级黑名单 如何设计?亿级查重 呢?(答案含:布隆过滤器、布谷鸟过滤器)
尼恩,40岁的老架构师,近期在读者交流群中分享了几个大厂面试题及其解决方案。这些问题包括亿级数据查重、黑名单存储、电话号码判断、安全网址判断等。尼恩给出了三种解决方案:使用BitMap位图、BloomFilter布隆过滤器和CuckooFilter布谷鸟过滤器。这些方法不仅高效,还能显著提升面试表现。尼恩还建议大家系统化学习,刷题《尼恩Java面试宝典PDF》,并提供简历修改和面试辅导,帮助大家实现“offer自由”。更多技术资料和PDF可在公众号【技术自由圈】获取。
|
供应链 物联网 新制造
云上智能制造:重塑工业未来,驱动智能升级的新篇章
云上智能制造平台作为智能制造领域的重要创新成果,正以其独特的优势和广泛的应用场景引领着制造业的智能化升级。未来,随着技术的不断进步和应用的不断拓展,云上智能制造平台将在推动产业升级、提升生产效率、优化资源配置等方面发挥更加重要的作用。我们有理由相信,在云上智能制造平台的助力下,制造业将迎来更加辉煌的未来。
959 0
|
安全 定位技术
漏洞扫描工具 -- Skipfish
漏洞扫描工具 -- Skipfish
777 0
漏洞扫描工具 -- Skipfish
|
存储 NoSQL MongoDB
MongoDB分片教程
MongoDB分片教程
480 0