【Java】ArrayList和LinkedList使用不当,性能差距会如此之大!

文章目录

  • 前言
  • 源码分析
    • ArrayList
      • 基本属性
      • 初始化
      • 新增元素
      • 删除元素
      • 遍历元素
    • LinkedList
      • 实现类
      • 基本属性
      • 节点查询
      • 新增元素
      • 删除元素
      • 遍历元素
  • 分析测试

前言

在面试的时候,经常会被问到几个问题:

  • ArrayList和LinkedList的区别,相信大部分朋友都能回答上:

    • ArrayList是基于数组实现,LinkedList是基于链表实现
    • 当随机访问List时,ArrayList比LinkedList的效率更高,等等
  • 当被问到ArrayList和LinkedList的使用场景是什么时,大部分朋友的答案可能是:

    • ArrayList和LinkedList在新增、删除元素时,LinkedList的效率要高于 ArrayList,而在遍历的时候,ArrayList的效率要高于LinkedList

那这个回答是否准确呢?今天我们就来研究研究!
从源码角度解析ArrayList.subList的几个坑
我们先来简单介绍下ArrayList和LinkedList的原理实现!

源码分析

ArrayList

实现类

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

ArrayList实现了List接口,继承了AbstractList抽象类,底层是数组实现的,并且实现了自增扩容数组大小。

ArrayList还实现了Cloneable接口和Serializable接口,所以他可以实现克隆和序列化。

ArrayList还实现了RandomAccess接口,这个接口是一个标志接口,他标志着“只要实现该接口的List类,都能实现快速随机访问”。

基本属性

ArrayList属性主要由数组长度size、对象数组elementData、初始化容量default_capacity等组成, 其中初始化容量默认大小为10。

//默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
//对象数组
transient Object[] elementData; 
//数组长度
private int size;

从ArrayList属性来看,elementData被关键字transient修饰了,transient关键字修饰该字段则表示该属性不会被序列化。

但ArrayList其实是实现了序列化接口,这是为什么呢?

由于ArrayList的数组是基于动态扩增的,所以并不是所有被分配的内存空间都存储了数据。
如果采用外部序列化法实现数组的序列化,会序列化整个数组,ArrayList为了避免这些没有存储数据的内存空间被序列化,内部提供了两个私有方法writeObject以及readObject来自我完成序列化与反序列化,从而在序列化与反序列化数组时节省了空间和时间。
因此使用transient修饰数组,是防止对象数组被其他外部方法序列化。
ArrayList自定义序列化方法如下:
在这里插入图片描述

初始化

有三种初始化办法:无参数直接初始化、指定大小初始化、指定初始数据初始化,源码如下:
在这里插入图片描述

当ArrayList新增元素时,如果所存储的元素已经超过其已有大小,它会计算元素大小后再进行动态扩容,数组的扩容会导致整个数组进行一次内存复制。

因此,我们在初始化ArrayList时,可以通过第一个构造函数合理指定数组初始大小,这样有助于减少数组的扩容次数,从而提高系统性能。

注意点:

ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。

新增元素

ArrayList新增元素的方法有两种,一种是直接将元素加到数组的末尾,另外一种是添加元素到任意位置。

public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;}public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1);  // Increments modCount!!System.arraycopy(elementData, index, elementData, index + 1,size - index);elementData[index] = element;size++;}

两个方法的相同之处是在添加元素之前,都会先确认容量大小,如果容量够大,就不用进行扩容;如果容量不够大,就会按照原来数组的1.5倍大小进行扩容,在扩容之后需要将数组复制到新分配的内存地址。
下面是具体的源码:
在这里插入图片描述

这两个方法也有不同之处,添加元素到任意位置,会导致在该位置后的所有元素都需要重新排列,而将元素添加到数组的末尾,在没有发生扩容的前提下,是不会有元素复制排序过程的。

所以ArrayList在大量新增元素的场景下效率不一定就很慢的

如果我们在初始化时就比较清楚存储数据的大小,就可以在ArrayList初始化时指定数组容量大小,并且在添加元素时,只在数组末尾添加元素,那么ArrayList在大量新增元素的场景下,性能并不会变差,反而比其他List集合的性能要好。

删除元素

ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多。
ArrayList在每一次有效的删除元素操作之后,都要进行数组的重组,并且删除的元素位置越靠前,数组重组的开销就越大。
我们选取根据值删除方式来进行源码说明:
在这里插入图片描述

遍历元素

由于ArrayList是基于数组实现的,所以在获取元素的时候是非常快捷的。

public E get(int index) {rangeCheck(index);return elementData(index);}E elementData(int index) {return (E) elementData[index];}

LinkedList

LinkedList是基于双向链表数据结构实现的。
这个双向链表结构,链表中的每个节点都可以向前或者向后追溯,有几个概念如下:

  • 链表每个节点我们叫做 Node,Node 有 prev 属性,代表前一个节点的位置,next 属性,代表后一个节点的位置;
  • first 是双向链表的头节点,它的前一个节点是 null。
  • last 是双向链表的尾节点,它的后一个节点是 null;
    当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null;
  • 因为是个双向链表,只要机器内存足够强大,是没有大小限制的。

Node结构中包含了3个部分:元素内容item、前指针prev以及后指针next,代码如下。

private static class Node<E> {E item;// 节点值Node<E> next; // 指向的下一个节点Node<E> prev; // 指向的前一个节点// 初始化参数顺序分别是:前一个节点、本身节点值、后一个节点Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}

LinkedList就是由Node结构对象连接而成的一个双向链表。

实现类

LinkedList类实现了List接口、Deque接口,同时继承了AbstractSequentialList抽象类,LinkedList既实现了List类型又有Queue类型的特点;LinkedList也实现了Cloneable和Serializable接口,同ArrayList一样,可以实现克隆和序列化。
由于LinkedList存储数据的内存地址是不连续的,而是通过指针来定位不连续地址,因此,LinkedList不支持随机快速访问,LinkedList也就不能实现RandomAccess接口。

public class LinkedListextends AbstractSequentialListimplements List, Deque, Cloneable, java.io.Serializable

基本属性

transient int size = 0;
transient Node first;
transient Node last;

我们可以看到这三个属性都被transient修饰了,原因很简单,我们在序列化的时候不会只对头尾进行序列化,所以LinkedList也是自行实现readObject和writeObject进行序列化与反序列化。
下面是LinkedList自定义序列化的方法。
在这里插入图片描述

节点查询

链表查询某一个节点是比较慢的,需要挨个循环查找才行,我们看看 LinkedList 的源码是如何寻找节点的:
在这里插入图片描述

LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。
如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能。

新增元素

LinkedList添加元素的实现很简洁,但添加的方式却有很多种。
默认的add (Ee)方法是将添加的元素加到队尾,首先是将last元素置换到临时变量中,生成一个新的Node节点对象,然后将last引用指向新节点对象,之前的last对象的前指针指向新节点对象。
在这里插入图片描述

LinkedList也有添加元素到任意位置的方法,如果我们是将元素添加到任意两个元素的中间位置,添加元素操作只会改变前后元素的前后指针,指针将会指向添加的新元素,所以相比ArrayList的添加操作来说,LinkedList的性能优势明显。
在这里插入图片描述

删除元素

在LinkedList删除元素的操作中,我们首先要通过循环找到要删除的元素,如果要删除的位置处于List的前半段,就从前往后找;若其位置处于后半段,就从后往前找。
这样做的话,无论要删除较为靠前或较为靠后的元素都是非常高效的,但如果List拥有大量元素,移除的元素又在List的中间段,那效率相对来说会很低。

遍历元素

LinkedList的获取元素操作实现跟LinkedList的删除元素操作基本类似,通过分前后半段来循环查找到对应的元素,但是通过这种方式来查询元素是非常低效的,特别是在for循环遍历的情况下,每一次循环都会去遍历半个List。
所以在LinkedList循环遍历时,我们可以使用iterator方式迭代循环,直接拿到我们的元素,而不需要通过循环查找List。
分析测试
新增元素操作性能测试
测试用例源代码:

ArrayList:paste.ubuntu.com/p/gktBvjgMG…
LinkedList:paste.ubuntu.com/p/3jQrY2XMP…

分析测试

在这里插入图片描述
测试结果
在这里插入图片描述

操作花费时间从集合头部位置添加元素(ArrayList)550从集合头部位置添加元素(LinkedList)34从集合中间位置位置添加元素(ArrayList)32从集合中间位置位置添加元素(LinkedList)58746从集合尾部位置添加元素(ArrayList)29从集合尾部位置添加元素(LinkedList)31
通过这组测试,我们可以知道LinkedList添加元素的效率未必要高于ArrayList。

从集合头部位置添加元素

由于ArrayList是数组实现的,在添加元素到数组头部的时候,需要对头部以后的数据进行复制重排,所以效率很低;
LinkedList是基于链表实现,在添加元素的时候,首先会通过循环查找到添加元素的位置,如果要添加的位置处于List的前半段,就从前往后找;若其位置处于后半段,就从后往前找,因此LinkedList添加元素到头部是非常高效的。

从集合中间位置位置添加元素

ArrayList在添加元素到数组中间时,同样有部分数据需要复制重排,效率也不是很高;
LinkedList将元素添加到中间位置,是添加元素最低效率的,因为靠近中间位置,在添加元素之前的循环查找是遍历元素最多的操作。

从集合尾部位置添加元素

而在添加元素到尾部的操作中,在没有扩容的情况下,ArrayList的效率要高于LinkedList。
这是因为ArrayList在添加元素到尾部的时候,不需要复制重排数据,效率非常高。
LinkedList虽然也不用循环查找元素,但LinkedList中多了new对象以及变换指针指向对象的过程,所以效率要低于ArrayList。

注意:这是排除动态扩容数组容量的情况下进行的测试,如果有动态扩容的情况,ArrayList的效率也会降低。

删除元素操作性能测试
ArrayList和LinkedList删除元素操作测试的结果和添加元素操作测试的结果很接近!
结论: 如果需要在List的头部进行大量的插入、删除操作,那么直接选择LinkedList。否则,ArrayList即可。
遍历元素操作性能测试
测试用例源代码:

在这里插入图片描述

测试结果:

在这里插入图片描述

操作花费时间for循环(ArrayList)3for循环(LinkedList)17557迭代器循环(ArrayList)4迭代器循环(LinkedList)4
我们可以看到,LinkedList的for循环性能是最差的,而ArrayList的for循环性能是最好的。
这是因为LinkedList基于链表实现的,在使用for循环的时候,每一次for循环都会去遍历半个List,所以严重影响了遍历的效率;ArrayList则是基于数组实现的,并且实现了RandomAccess接口标志,意味着ArrayList可以实现快速随机访问,所以for循环效率非常高。
LinkedList的迭代循环遍历和ArrayList的迭代循环遍历性能相当,也不会太差,所以在遍历LinkedList时,我们要切忌使用for循环遍历。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/196884.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Unity】单例模式及游戏声音管理类应用

【Unity】单例模式及游戏声音管理类应用 描述 在日常游戏项目开发中&#xff0c;单例模式是一种常用的设计模式&#xff0c;它允许在应用程序的生命周期中只创建一个对象实例&#xff0c;并提供对该实例的全局访问点。通过使用单例模式&#xff0c;可以提高代码的可维护性和可…

cesium雷达效果(脉冲圆)

cesium雷达效果(脉冲圆) 下面富有源码 实现思路 使用ellipse方法加载圆型,修改ellipse中‘material’方法重写glsl来实现当前效果 示例代码 index.html <!DOCTYPE html> <html lang="en"><head>

Redis(集合Set和有序集合SortedSet)

SET集合中的元素是不允许重复的&#xff0c;SET中的命令都是以S开头的。 使用SADD 在集合中添加元素&#xff0c;使用SMEMBERS查看元素。 当添加重复元素时&#xff0c;会返回0代表添加失败&#xff0c;查询还是就Redis一个元素。 使用SISMEMBER查询元素是否在集合中&#xff…

井盖位移监测,智能井盖智慧监测方式

在推动城市向智能化和高效化方向发展的过程中&#xff0c;科学技术发挥着至关重要的作用。智能井盖传感器作为科学技术进步的产物&#xff0c;正逐渐在城市管理过程之中崭露头角。这些看似不起眼的设备&#xff0c;虽然隐藏在井盖下方不被人们看到&#xff0c;但实实在在为人民…

Jquery 通过class名称属性,匹配元素

UI自动化过程中&#xff0c;常常需要判断某个元素是否满足条件&#xff0c;再走不通的脚本逻辑&#xff1b;、本文介绍如何通过jquery判断菜单是否展开&#xff0c;来决定是否执行菜单展开脚本&#xff1b;Jquery通过class名称属性&#xff0c;匹配元素 我们先分析&#xff0c;…

HTTP 到 HTTPS 再到 HSTS 的转变

近些年&#xff0c;随着域名劫持、信息泄漏等网络安全事件的频繁发生&#xff0c;网站安全也变得越来越重要&#xff0c;也促成了网络传输协议从 HTTP 到 HTTPS 再到 HSTS 的转变。 HTTP HTTP&#xff08;超文本传输协议&#xff09; 是一种用于分布式、协作式和超媒体信息系…

RabbitMQ 消息丢失解决 (高级发布确认、消息回退与重发、备份交换机)

目录 一、发布确认SpringBoot版本 确认机制图例&#xff1a; 代码实战&#xff1a; 代码架构图&#xff1a; 1.1交换机的发布确认 添加配置类 消息消费者 消息生产者发布消息后的回调接口 测试&#xff1a; 1.2回退消息并重发&#xff08;队列的发布确认&#xff09; …

编写程序,要求输入x的值,输出y的值。分别用(1)不嵌套的if语句(2)嵌套的if语句(3)if-else语句(4)switch语句。

编写程序&#xff0c;要求输入x的值&#xff0c;输出y的值。分别用&#xff08;1&#xff09;不嵌套的if语句&#xff08;2&#xff09;嵌套的if语句&#xff08;3&#xff09;if-else语句&#xff08;4&#xff09;switch语句。 选择结构是编程语言中常用的一种控制结构&…

适用于 Windows 的 10 个最佳视频转换器:快速转换高清视频

您是否遇到过由于格式不兼容而无法在您的设备上播放视频或电影的情况&#xff1f;您想随意播放从您的相机、GoPro 导入的视频&#xff0c;还是以最合适的格式将它们上传到媒体网站&#xff1f;您的房间里是否有一堆 DVD 光盘&#xff0c;想将它们转换为数字格式以便于播放&…

清华学霸告诉你:如何自学人工智能?

清华大学作为中国顶尖的学府之一&#xff0c;培养了许多优秀的人才&#xff0c;其中不乏在人工智能领域有所成就的学霸。通过一位清华学霸的经验分享&#xff0c;揭示如何自学人工智能&#xff0c;帮助你在这场科技浪潮中勇往直前。 一、夯实基础知识 数学基础&#xff1a;学习…

2023年首届天府杯数学建模国际大赛问题A思路详解与参考代码:大地测量数据中异常现象的特征和识别

地球变形观测是固体潮汐曲线分析和地震前体研究的重要手段&#xff0c;也是地球观测技术的重要组成部分。基于各种精密科学仪器的变形观测点主要集中在洞穴、地下井等易的自然灾害&#xff08;雷暴、强降雨、降雪等&#xff09;&#xff0c;人工维护、人工爆破等外部条件&#…

详解如何使用Jenkins一键打包部署SpringBoot项目

目录 1、Jenkins简介 2、Jenkins的安装及配置 2.1、Docker环境下的安装​编辑 2.2、Jenkins的配置 3、打包部署SpringBoot应用 3.1、在Jenkins中创建执行任务 3.2、测试结果 1、Jenkins简介 任何简单操作的背后&#xff0c;都有一套相当复杂的机制。本文将以SpringBoot应…

春秋云境靶场CVE-2022-32991漏洞复现(sql手工注入)

文章目录 前言一、CVE-2022-32991靶场简述二、找注入点三、CVE-2022-32991漏洞复现1、判断注入点2、爆显位个数3、爆显位位置4 、爆数据库名5、爆数据库表名6、爆数据库列名7、爆数据库数据 总结 前言 此文章只用于学习和反思巩固sql注入知识&#xff0c;禁止用于做非法攻击。…

电子商务、搜索引擎

电子商务 域名 网络服务 网络樱肖 搜索引擎优化

掌握深度学习利器——TensorFlow 2.x实战应用与进阶

掌握深度学习利器——TensorFlow 2.x实战应用与进阶 摘要&#xff1a;随着人工智能技术的飞速发展&#xff0c;深度学习已成为当下最热门的领域之一。作为深度学习领域的重要工具&#xff0c;TensorFlow 2.x 备受关注。本文将通过介绍TensorFlow 2.x的基本概念和特性&#xff…

duplicate复制数据库单个数据文件复制失败报错rman-03009 ora-03113

duplicate复制数据库单个数据文件复制失败报错rman-03009 ora-03113 搭建dg过程中&#xff0c;发现有一个数据文件在复制过程中没有复制过来&#xff0c;在备库数据文件目录找不到这个数据文件 处理方法&#xff1a; 第一步&#xff1a;主库备份86#数据文件 C:\Users\Admi…

低代码编辑平台后台实现

背景 之前做过一个前端低代码编辑平台&#xff0c;可以实现简单的移动端页面组件拖拽编辑&#xff1a; https://github.com/li-car-fei/react-visual-design 最近基于C的oatpp框架实现了一下后台。使用oatpp框架做web后台开发时&#xff0c;发现按照官方的示例使用的话&#…

AI Navigation导航系统_unity基础开发教程

AI Navigation导航系统 安装插件烘焙导航系统障碍物创建人物的AI导航动态障碍物 在unity编辑器中&#xff0c;有一个灰常好用的插件&#xff1a;Navigation。有了它1&#xff0c;你就可以实现人物自动走到你鼠标点击的位置&#xff0c;而且还会自动避开障碍物&#xff0c;下面就…

CronExpression

CronTrigger配置格式: 格式: [秒] [分] [小时] [日] [月] [周] [年]序号 说明 是否必填 允许填写的值 允许的通配符 1 秒 是 0-59 , - * / 2 分 是 0-59 , - * / 3 小时 是 0-23 , - * / 4 日 是 1-31 , - * ? / L W 5 月 是 1-12 or JA…

服务容错之限流之 Tomcat 限流 Tomcat 线程池的拒绝策略

在文章开头&#xff0c;先和大家抛出两个问题&#xff1a; 每次提到服务限流为什么都不考虑基于 Tomcat 来做呢&#xff1f;大家有遇到过 Tomcat 线程池触发了拒绝策略吗&#xff1f; JUC 线程池 在谈 Tomcat 的线程池前&#xff0c;先看一下 JUC 中线程池的执行流程&#x…