ArrayList 从源码角度剖析底层原理
本篇文章已放到 Github github.com/sh-blog 仓库中,里面对我写的所有文章都做了分类,更加方便阅读。同时也会发布一些职位信息,持续更新中,欢迎 Star
对于 ArrayList
来说,我们平常用的最多的方法应该就是 add
和 remove
了,本文就主要通过这两个基础的方法入手,通过源码来看看 ArrayList
的底层原理。
add
默认添加元素
这个应该是平常用的最多的方法了,其用法如下。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gshekd5qilj316a0e0q58.jpg)
接下来我们就来看看 add
方法的底层源码。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsefiw0576j31520csgnd.jpg)
ensureCapacityInternal
作用为:保证在不停的往 ArrayList 插入数据时,数组不会越界,并且实现自动扩容。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsefmsswasj315o06owfe.jpg)
这里的 minCapacaity
的值,实际上就是在调用完当前这次 add
操作之后,数组中元素的数量
注意,这里说当前这次
add
操作完成。举个例子,调用add
之前,ArrayList 中有3个元素,那么此时这个minCapacity
的值就为 4
此外,可以看到将函数 calculateCapacity
的返回值作为了 ensureExplicitCapacity
的输入。
calculateCapacity
做了什么,用大白话来说是,如果当前数组是空的,则直接返回 数组默认长度(10) 和 minCapacity
的最大值,否则就直接返回 minCapacity
。
接下里是 ensureExplicitCapacity
,源码如下:
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsefuk5fc6j313i0aodh3.jpg)
modCount
表示该 ArrayList 被更改过多少次,这里的更改不只是新增,删除也是一种更改。通过上面的了解我们知道,如果当前数组内的元素个数是小于数组长度的,则 minCapacity
的值一定是小于 elementData.length
的。
相反,如果数组内的元素个数已经和数组长度相等了,则 0>0
一定是 false
,此时就会调用 grow
函数来进行数组扩容。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gseg1oibebj31eg0k4jvd.jpg)
核心逻辑很简单,新的数组长度 = 旧数组长度 + 旧数组长度/2。
这里的右移,就相当于直接除以2
所以通过这里的源码我们验证,ArrayList 的扩容是每次扩容 1.5 倍。但是这里会有一个疑问,因为上文提到扩容时 minCapacity
的值和数组长度应该是相等的,所以 新数组长度 - minCapacity 应该永远大于0才对,为什么会有小于0的情况?
答案是 addAll
。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsegntyaoej31bk0bmjtr.jpg)
可以看到,add
和 addAll
底层都会调用 ensureCapacityInternal
。add
是往数组中添单个元素,而 addAll
则是往数组中添加整个数组。假设 addAll
我们传进了一个很大的值,举个例子,ArrayList 的默认数组长度为 10
,扩容一次之后长度为 15
,假设我们传入的数组元素有 10
个,那么即使扩容一次还是不足以放下所有的元素,因为 15 < 20
。
所以才会出现 newCapacity(扩容之后的数组长度) < minCapacity(执行完当前操作之后的数组内元素数量)
的情况,所以当这种情况出现之后,就会直接将 minCapacity
的值赋给 newCapacity
。
除此之外,还会有个极端的情况,假设 addAll
往里面塞入了 Integer.MAX_VALUE
个元素呢?这就是 hugeCapacity
函数要处理的逻辑了。首先如果溢出了就直接抛出 OOM 异常,其次会保证其容量不会超过 Integer.MAX_VALUE
。
最后是真正执行扩容的操作,调用了 java.util
包里的 Arrays.copyOf
方法。从上图可以看出,这个方法中传入了两个参数,分别是存放元素的数组、新的数组长度,举个例子:
int[] elementData = {1, 2, 3, 4, 5};
int[] newElementData = Arrays.copyOf(elementData, 10);
System.out.println(newElementData); // [1 2 3 4 5 0 0 0 0 0]
数组扩容完成之后,就会将本次 add
的元素写入 elementData 的末尾了,elementData[size++] = e
。
接下来我们用流程图来总结一下 add
操作的整个核心流程。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsf3qgs42fj30rl0sjq71.jpg)
指定添加元素的位置
了解完了 add
和 addAll
,我们趁热打铁,来看看可以指定元素位置的 add
,其接受两个参数,分别是:
新元素在数组中的下标 新元素本身
这里和最开始的 add
就有些不同了,之前的 add
方法会将元素放在数组的末尾,而 add(int index, E element)
则会将元素插入到数组中指定的位置,接下来从源码层面看看。
首先,由于这个方法允许用户传入数组下标,所以首先要做的事情就是检查传入的数组下标是否合法,如果不合法则会直接抛出 IndexOutOfBoundsException
异常。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsf8gz2wmsj311s09u0ty.jpg)
很简单的判断,下标 index
不能小于0,并且不能超过数组中的元素个数,举个例子:
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsf8jfnr8tj317y0hktba.jpg)
像上图这种情况,调用 add(int index, E element)
之前,数组内有3个元素,即使底层数组的长度为10
,但是数组下标如果传入5,是会抛出 IndexOutOfBoundsException
异常的。在上图这种情况,index
的值最大只能为3才不会报错,因为 index(下标为3) > size(3个元素)
肯定不为 true
。
完成了校验之后,还是会调用上面聊到过的 ensureCapacityInternal
方法,根据需要,来对底层的数组进行扩容。然后调用 System.arraycopy
方法,这个方法比较关键,也比较难理解,所以我就简单一句话把它的作用概括了——将制定下标后的元素全部往后移动一位。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsfa7tdiogj31740buwgj.jpg)
System.arraycopy
接收 4 个参数,分别是:
原数组 原数组中的起始下标 目标数组 目标数组中的起始下标
光看参数,不结合例子,其实很难理解,我这里举个简单的例子。
假设现在数组里有元素 [1 2 3]
,然后此时我调用方法 add(1, 4)
,表明我想要将元素 4 插入到数组下标为 1
的位置,那么此时 index
的值为1,size
的值为 3。
System.arraycopy
的方法就会变成 System.arraycopy(elementData, 1, elementData, 2, 2)
,也就是将 elementData
从下标 1 开始的两个元素(也就是 2 和 3),拷贝到 elementData
的从下标 2 开始的地方。
可能还是有点绕,说人话就是执行完后,elementData
就变成了 [1 2 2 3]
,然后再对 elementData
进行赋值,将下标为 1 的元素改为本次需要 add
的元素。再说句人话就变成了 [1 4 2 3]
。
所以综上来看,没有什么黑魔法,主要需要了解的就是两个关键的函数,分别是 Arrays.copy
和 System.arraycopy
。我们需要把这两个封装好的函数的作用给记住。
Arrays.copy 数组扩容 System.arraycopy 将数组中某个下标之后元素全部往后移动一位
所以从这里就可以看到,看源码的好处,主要有两个方面。第一,我相信你在刷题的时候一定也遇到过需要将数组的元素整个后移的 case,但是你可能并不知道可以使用
System.arraycopy
,就算你知道有这么个函数可能就连参数都看不懂;第二,知道了System.arraycopy
,但是觉得这些函数完全没有应用场景。
remove
了解数据怎么来,接下来我们来看一下数据是怎么被移除的。首先我们来看最常用的两种:
按照下标移除 根据元素移除
根据下标移除
首先是根据下标移除
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsgps5l8bjj31ai0jydij.jpg)
这里也会先检查传入的 index
是否合法,但是这里的 index
和 add
中调用的 rangeCheck
还有点区别。add
中的 rangeCheckForAdd
会判断 index
是否为负数,而 remove
中的 rangeCheck
则只会判断 index
是否 >=
数组中的元素个数。
其实从函数的名称就能看出,
rangeCheckForAdd
是专门给add
方法用的
那如果此时传入的 index
真的是负数怎么办?其实是会抛出 ArrayIndexOutOfBoundsException
,因为remove
方法上加了 Range
注解。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsgq1xdoekj31a004cq3k.jpg)
完成后,还是会更新 modCount
的值,这也验证了我们上面提到的 modCount
代表的更改中也包含了删除。
接下来会计算一个 numMoved
,代表需要被移动的元素数量。add
一个元素,对应的下标的元素都需要往后顺移一位,remove
同理,删除了某个位置的元素后,其后面对应的所有的元素都需要往前顺移一位,就像这样:
![](https://tva1.sinaimg.cn/large/008i3skNgy1gsgr6dq4bkj30iv0j1mxw.jpg)
知道了需要移动多少个元素之后,我们的 System.arraycopy
就又可以登场了。完成了元素的移动之后,数组的末尾必然会空出来一个元素,直接将其设置为 null
然后交给 GC 回收即可,最后把被移除的值返回。
根据值移除
举个例子,根据值移除就长下面这样这样。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gshe1gp2yhj31540c2q5c.jpg)
废话不多说,直接看核心源码
![](https://tva1.sinaimg.cn/large/008i3skNgy1gshe0u0c1tj31bo0mc0vo.jpg)
完了,第二行就给整懵了,移除一个 null
是什么鬼?还要循环去移除?
实际上,ArrayList 允许我们传 null
值进去,再举个例子。
![](https://tva1.sinaimg.cn/large/008i3skNgy1gshe4jxdq0j31c00q0djw.jpg)
看完这个例子,应该就能够明白为什么要做 o == null
的判断了。如果传入的是 null
,ArrayList 会对底层的数组进行遍历,并移除匹配到的第一个值为 null
的元素。
如果值不为 null
也是同理,如果数组中有多个一样的值,ArrayList 也会对其进行遍历,并且移除匹配到的第一个值。通过源码可以看到,无论值是否为 null
,其都会调用真正的删除元素方法 fastRemove
,干的事情就跟 remove
做的几乎一样。
他们的唯一的区别在于,按照下标移除,会返回被移除的元素;按照值移除只会返回是否移除成功。
总结
所以,看完 ArrayList
的部分源码之后,我们就可以知道,ArrayList
的底层数据结构是数组。虽然对于用户来说 ArrayList
是个动态的数组,但是实际上底层是个定长数组,只是在必要的时候,对底层的数组进行扩容,每次扩容 1.5
倍。但是从源码也看出来了,扩容、删除都是有代价的,特别是在极端的情况,会需要将大量的元素进行移位。
所以我们得出结论,ArrayList
如果有频繁的随机插入、频繁的删除操作是会对其性能造成很大影响的, 总结来说,ArrayList
适合用于读多写少的场景。
另一个很重要很重要的点,这里提一下,
ArrayList
不是线程安全的。多线程的情况下会出现数据不一致或者会抛出ConcurrentModificationException
异常,关于这块后面有机会再聊吧
了解完如何向一个数据结构中存取、移除数据,其实就已经能够顺理成章的理解跟其相关的很多事情了。
举个例子,indexOf
方法用于返回指定元素在数组中的下标,了解完 remove
中的遍历匹配,或者说你甚至可以直接靠直觉就应该想到,indexOf
不就是个 for
循环匹配吗?lastIndexOf
不就是个反向的 for
循环匹配吗?所以在这里再贴出源码除了让文章篇幅更长之外,没有任何意义。这个感兴趣的话可以找源码看一看。
本篇文章已放到我的 Github github.com/sh-blog 中,欢迎 Star。微信搜索关注【SH的全栈笔记】,回复【队列】获取MQ学习资料,包含基础概念解析和RocketMQ详细的源码解析,持续更新中。
如果你觉得这篇文章对你有帮助,还麻烦点个赞,关个注,分个享,留个言。
![](https://tva1.sinaimg.cn/large/008eGmZEgy1go0v5rrqp4j31eg0hcwhm.jpg)
- END -
ArrayList 从源码角度剖析底层原理的更多相关文章
- 深入源码分析SpringMVC底层原理(二)
原文链接:深入源码分析SpringMVC底层原理(二) 文章目录 深入分析SpringMVC请求处理过程 1. DispatcherServlet处理请求 1.1 寻找Handler 1.2 没有找到 ...
- 顺序线性表 ---- ArrayList 源码解析及实现原理分析
原创播客,如需转载请注明出处.原文地址:http://www.cnblogs.com/crawl/p/7738888.html ------------------------------------ ...
- ArrayList源码深度剖析,从最基本的扩容原理,到魔幻的迭代器和fast-fail机制,你想要的这都有!!!
ArrayList源码深度剖析 本篇文章主要跟大家分析一下ArrayList的源代码.阅读本文你首先得对ArrayList有一些基本的了解,至少使用过它.如果你对ArrayList的一些基本使用还不太 ...
- Kafka源码分析及图解原理之Producer端
一.前言 任何消息队列都是万变不离其宗都是3部分,消息生产者(Producer).消息消费者(Consumer)和服务载体(在Kafka中用Broker指代).那么本篇主要讲解Producer端,会有 ...
- 源码|ThreadLocal的实现原理
ThreadLocal也叫"线程本地变量"."线程局部变量": 其作用域覆盖线程,而不是某个具体任务: 其"自然"的生命周期与线程的生命周期 ...
- 消息队列高手课,带你从源码角度全面解析MQ的设计与实现
消息队列中间件的使用并不复杂,但如果你对消息队列不熟悉,很难构建出健壮.稳定并且高性能的企业级系统,你会面临很多实际问题: 如何选择最适合系统的消息队列产品? 如何保证消息不重复.不丢失? 如果你掌握 ...
- 【react】什么是fiber?fiber解决了什么问题?从源码角度深入了解fiber运行机制与diff执行
壹 ❀ 引 我在[react] 什么是虚拟dom?虚拟dom比操作原生dom要快吗?虚拟dom是如何转变成真实dom并渲染到页面的?一文中,介绍了虚拟dom的概念,以及react中虚拟dom的使用场景 ...
- HashMap源码深度剖析,手把手带你分析每一行代码,包会!!!
HashMap源码深度剖析,手把手带你分析每一行代码! 在前面的两篇文章哈希表的原理和200行代码带你写自己的HashMap(如果你阅读这篇文章感觉有点困难,可以先阅读这两篇文章)当中我们仔细谈到了哈 ...
- ArrayDeque(JDK双端队列)源码深度剖析
ArrayDeque(JDK双端队列)源码深度剖析 前言 在本篇文章当中主要跟大家介绍JDK给我们提供的一种用数组实现的双端队列,在之前的文章LinkedList源码剖析当中我们已经介绍了一种双端队列 ...
随机推荐
- [leetcode] 45. 跳跃游戏 II(Java)(动态规划)
45. 跳跃游戏 II 动态规划 此题可以倒着想. 看示例: [2,3,1,1,4] 我们从后往前推,对于第4个数1,跳一次 对于第3个数1,显然只能跳到第4个数上,那么从第3个数开始跳到最后需要两次 ...
- pika详解(四) channel 通道
pika详解(四) channel 通道 本文链接:https://blog.csdn.net/comprel/article/details/94662394 版权 channel通道 通道 ...
- win10家庭中文版CUDA+CUDNN+显卡GPU使用tensorflow-gpu训练模型安装过程(精华帖汇总+重新修改多次复现)
查看安装包 pip list 本帖提供操作过程,具体操作网上有好多了,不赘述.红色字体为后来复现出现的问题以及批注 题外话: (1)python 的环境尽量保持干净,尽量单一,否则容易把自己搞晕,不知 ...
- final 修饰符
修饰属性,方法,类 1.修饰属性 属性只能被赋值一次 基本类型:值不能改变 引用类型:引用不可以被修改 2.修饰方法 表示方法不可以被重写,但可以被子类访问 3.修饰类 表示类不可以被继承 //fin ...
- 台积电5nm光刻技术
台积电5nm光刻技术 在IEEE IEDM会议上,台积电发表了一篇论文,概述了其5nm工艺的初步成果.对于目前使用N7或N7P工艺的客户来说,下一步将会采用此工艺,因为这两种工艺共享了一些设计规则.新 ...
- TVM在ARM GPU上优化移动深度学习
TVM在ARM GPU上优化移动深度学习 随着深度学习的巨大成功,将深度神经网络部署到移动设备的需求正在迅速增长.与在台式机平台上所做的类似,在移动设备中使用GPU可以提高推理速度和能源效率.但是,大 ...
- Pandas之:深入理解Pandas的数据结构
目录 简介 Series 从ndarray创建 从dict创建 从标量创建 Series 和 ndarray Series和dict 矢量化操作和标签对齐 Name属性 DataFrame 从Seri ...
- redis 记一次搭建高可用redis集群过程,问题解决;Node 192.168.184.133:8001 is not configured as a cluster node
------------恢复内容开始------------ 步骤 1:每台redis服务器启动之后,需要将这几台redis关联起来, 2: 关联命令启动之后 报错: Node 192.168.184 ...
- vscode使用版本控制git commit unstaged时提示对话框的设置
使用 vscode 版本控制提交代码时,如果有 unstaged file,会有一个弹出框: 选择 always 或者 never ,这个框下次就不再弹出了. 如果你想让他再次出现,请去setting ...
- java后端知识点梳理——Spring
开篇:感谢我是祖国的花朵,java3y,三太子敖丙等优秀博主!他们的文章为我学习java提供了莫大的帮助,膜拜大神! Spring的优点有哪些呢? Spring的依赖注入将对象之间的依赖关系交给了框架 ...