前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ArrayList、LinkedList 你真的了解吗?

ArrayList、LinkedList 你真的了解吗?

作者头像
程序猿Damon
发布2020-05-21 12:13:34
4070
发布2020-05-21 12:13:34
举报
文章被收录于专栏:程序猿 Damon 带你进阶全栈

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 的性能。

代码语言:javascript
复制
publicclass ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

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

至于 ArrayList、LinkedList 的各种操作方法这里不再说了,大家可以看 浅谈&nbsp;Java&nbsp;集合&nbsp;|&nbsp;底层源码解析 这一篇。

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

  1. ArrayList、LinkedList 新增测试
代码语言:javascript
复制
头部: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 的操作。

代码语言:javascript
复制
public boolean add(E e) {
    linkLast(e);
    returntrue;
}

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 删除测试
代码语言:javascript
复制
头部:ArrayList.Time 大于 LinkedList.Time
中间:ArrayList.Time 小于 LinkedList.Time
末尾:ArrayList.Time 小于 LinkedList.Time

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

  1. ArrayList、LinkedList 遍历测试
代码语言:javascript
复制
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 删除操作的过程中,有下面两种写法:

代码语言:javascript
复制
public static void removeA(ArrayList<String> l) {
  for (String s : l){
      if (s.equals("aaa")) {
        l.remove(s);
      }
  }
}
代码语言:javascript
复制
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 方法:

代码语言:javascript
复制
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                returntrue;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                returntrue;
            }
    }
    returnfalse;
}
代码语言:javascript
复制
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 对象加锁。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 交个朋友之猿天地 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、 前言
  • 2、集合 List
    • 2.1 大家心中的 List
      • 2.2 性能比较
      • 3、常错点
        • 思考
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档