夺命追问带你深入了解ArrayList与LinkedList

2024-01-11 11:36

本文主要是介绍夺命追问带你深入了解ArrayList与LinkedList,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、ArrayList

问题1:说一下JDK1.7与1.8 ArrayList有什么区别?

问2:说一下ArrayList的扩容机制?

问3:下面这段代码会将数组扩容到多少?

问4:说说迭代器Iterator的两种规则:fail-fast和fail-safe

问5:简单说说fail-fast的源码

二、LinkedList

问1:ArrayList与LinkedList的比较?

问2:ArrayList与LinkedList 查询、增删性能比较?

问3:什么是局部性原理?


一、ArrayList

问题1:说一下JDK1.7与1.8 ArrayList有什么区别?

答:

(1)JDK7:JDK7的ArrayList类似于单例模式的饿汉式,new ArrayList();时就创建长度为10的数组,当你添加到第11个元素时进行数组的扩容,扩容成当前的1.5倍,也就是长度15的数组。

(2)JDK8:JDK8的ArrayList类似于单例模式的懒汉式,new ArrayList();仅仅是将elementData初始化为{},也就是一个长度为0的数组。当第一次add()的时候,底层才创建了长度为10的数组。添加到第11个元素时数组进行扩容,扩容成当前的1.5倍,也就是长度15的数组。

问2:说一下ArrayList的扩容机制?

答:一开始new ArrayList(); 底层创建容量为0的数组,当我们add()第一个元素时,触发第一次扩容,将容量0变为10,当数组容量存满10个,add()第11个元素时触发第二次扩容,扩容为原理的1.5倍,也就是15。当我们add()到第16个元素,触发第三次扩容,但注意,扩容1.5倍并不准确,拿第三次扩容举例,15 * 1.5 = 22.5,结果发现带小数了。其实底层运用的是移位,15 >> 1(15右移一位,相当于除2)等于7,结果7再加上原始的容量15,就是新的容量,结果是22。

问3:下面这段代码会将数组扩容到多少?

ArrayList<Integer> list = new ArrayList<>();
list.addAll(List.of(1,2,3,4,5,6,7,8,9,10,11));

答:正常我们分析一开始初始化容量是0,一口气放11个元素,那肯定扩容成15了。但结果却是11,为什么呢?因为addAll运行的规律是,当你原始的容量不够时,它会在下一次扩容容量的大小(10,第一次是0,下一次就是10)和我们addAll的元素个数二者之间取一个较大值作为下一次容量。那就是10和11比,那结果就是11。

这就是addAll方法特殊的运行规律。

问4:说说迭代器Iterator的两种规则:fail-fast和fail-safe

答:我们都知道,ArrayList使用迭代器或foreach遍历时,其它线程不可改变其元素,如果改变则会抛出异常,而List其实有两种规则来指定此问题。

fail-fast:一旦发现遍历的同时其它人来修改,则立刻抛异常。

fail-safe:发现遍历的同时其它人来修改,应当能有应对策略,例如牺牲一致性让整个遍历运行完成。例如遍历有5个元素的集合,分别是abcde,假如遍历到c的时候,我们添加一个f,但发现并不会报错,只是结果还是旧的abcde,但是当你整个for循环遍历完成,再打印一次list,则会发现有f元素了。

ArrayList是fail-fast的典型代表,遍历的同时不能修改,尽快失败抛出异常。

CopyOnWriteArrayList是fail-safe的典型代表,遍历的同时可以修改,原理是读写分离。底层就是弄两个数组,一个是用来读的,另一个新数组是用来写新元素的。

问5:简单说说fail-fast的源码

答:foreach遍历集合,其实是走的Iterator,首先判断hasNext(),如果没有了则终止循环,否则next()获取元素时,都要check一下集合元素个数是否变化了,如果变化了,则抛出异常。

modCount是集合添加元素、删除元素的次数,expectedModCount是预期的修改次数。增删操作会使得modCount+1,不等于expetedModCount,所以抛出异常。

没有使用list.iterator时调用的是ArrayList自己的remove方法,并不会同步这两个值,导致抛出异常。调用了ArrayList.iterator之后,此后再remove,remove方法中有让这两个值相等的操作。

迭代器的remove方法会修改expectedModCount,从而使modCount与之相等。

也就是说,如果你要在迭代中移除元素,那你最好用迭代器,别用foreach,因为foreach用list.remove();会报错,但迭代器中的iterator.remove();不会报错。

注意:

expectedModCount:预期修改次数,集合假如一开始add() 5个元素,那expectedModCount就是5。

modCount:在循环的时候如果增删,这个变量就会+1。

二、LinkedList

问1:ArrayList与LinkedList的比较?

答:

(1)ArrayList

① 底层是数组,需要连续内存。

② 随机访问快(指根据下标访问)。

③ 尾部插入、删除性能可以,其它部分插入、删除会移动数据,因此性能会低。

④ 可以利用cpu缓存,局部性原理。

(2)LinkedList

① 底层是双向链表,无需连续内存。

② 随机访问慢(要沿着链表遍历)。

③ 头尾部插入删除性能高。

④ 占用内存多。

解释链表为何查询慢:因为每个节点只知道上一个元素和下一个元素是什么,我如果想找到4,那我从1开始找,1下一个元素是2,2下一个元素是3,3下一个元素是4这么找,沿着链表遍历。

而删除为什么快,双向链表可以想象成小朋友手拉手站成一排,如果其中一个小朋友退出游戏,那么直接让旁边两个小朋友拉手即可,跟其他的小朋友没关系。删除第3个元素,只跟第2个和第4个元素有关系,只需要把第2个元素的地址值给第4个,再将第4个元素的地址值给第2个即可。

问2:ArrayList与LinkedList 查询、增删性能比较?

答:放弃ArrayList增删慢,查询快。LinkedList增删快,查询慢这种说法。

首先我们要明确一个概念,什么是随机访问?什么是查询?

随机访问:是随便根据索引去挑一个。

查询:根据元素的内容去元素里找。

按查询来说,ArrayList和LinkedList 根据元素内容去找的话,时间复杂度都差不多,都是O(n)。换句话说,这哥俩都不太适合用来做查询,要真想做查询,就用HashMap或者TreeMap这种高效的数据结构,它们的哈希表和红黑树都更适合用来做查询。

按随机访问来说,ArrayList肯定比LinkedList要快,因为带索引查询,而双向链表只能沿着链表一点点找。

增删的话,ArrayList尾部增删都很快,因为不用移动其它元素,直接增加或删除即可,如果其它位置增删,比如头部增删,那其它的元素都需要移动位置来配合你。而LinkedList只是头尾插入删除性能比较高,但链表的中间去增删元素,它的性能实在不敢恭维。测试代码结果是LinkedList比ArrayList耗费时间多出6倍。

为什么LinkedList中间插入元素会那么慢?

原因是你需要先定位到中间,那必然通过LinkedList的next一个个的找到中间位置元素,这个查找的过程,移动指针的过程是非常慢的。当然定位到了以后做增删操作是非常快的,无非是链表的指针重新改变一下。

问3:什么是局部性原理?

答:局部性原理就是我们查询一个元素时,可以将此数据相邻的几个元素都加入到缓存中,因为它会做出一种假设,如果你查询的是1,那么你有可能马上会用到2、3、4、5,那我就一起都放入缓存中。因为ArrayList底层是数组,所以它可以实现这种局部性原理的方式来优化。但LinkedList不行,因为双向链表的缘故,有可能你查询的1元素,它的指针指向下一个元素是2,但有可能1和2元素并不相邻,中间可能差的有点远。所以缓存中并没有把你接下来的元素2加载进缓存中。

这篇关于夺命追问带你深入了解ArrayList与LinkedList的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/594229

相关文章

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

深入手撕链表

链表 分类概念单链表增尾插头插插入 删尾删头删删除 查完整实现带头不带头 双向链表初始化增尾插头插插入 删查完整代码 数组 分类 #mermaid-svg-qKD178fTiiaYeKjl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-

深入理解RxJava:响应式编程的现代方式

在当今的软件开发世界中,异步编程和事件驱动的架构变得越来越重要。RxJava,作为响应式编程(Reactive Programming)的一个流行库,为Java和Android开发者提供了一种强大的方式来处理异步任务和事件流。本文将深入探讨RxJava的核心概念、优势以及如何在实际项目中应用它。 文章目录 💯 什么是RxJava?💯 响应式编程的优势💯 RxJava的核心概念

深入理解数据库的 4NF:多值依赖与消除数据异常

在数据库设计中, "范式" 是一个常常被提到的重要概念。许多初学者在学习数据库设计时,经常听到第一范式(1NF)、第二范式(2NF)、第三范式(3NF)以及 BCNF(Boyce-Codd范式)。这些范式都旨在通过消除数据冗余和异常来优化数据库结构。然而,当我们谈到 4NF(第四范式)时,事情变得更加复杂。本文将带你深入了解 多值依赖 和 4NF,帮助你在数据库设计中消除更高级别的异常。 什么是

速了解MySQL 数据库不同存储引擎

快速了解MySQL 数据库不同存储引擎 MySQL 提供了多种存储引擎,每种存储引擎都有其特定的特性和适用场景。了解这些存储引擎的特性,有助于在设计数据库时做出合理的选择。以下是 MySQL 中几种常用存储引擎的详细介绍。 1. InnoDB 特点: 事务支持:InnoDB 是一个支持 ACID(原子性、一致性、隔离性、持久性)事务的存储引擎。行级锁:使用行级锁来提高并发性,减少锁竞争

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理 秒杀系统是应对高并发、高压力下的典型业务场景,涉及到并发控制、库存管理、事务管理等多个关键技术点。本文将深入剖析秒杀商品业务中常见的几个核心问题,包括 AOP 事务管理、同步锁机制、乐观锁、CAS 操作,以及用户限购策略。通过这些技术的结合,确保秒杀系统在高并发场景下的稳定性和一致性。 1. AOP 代理对象与事务管理 在秒杀商品