JDK源码剖析-集合篇-ArrayList
1.ArrayList基本原理以及优缺点
1.1ArrayList基本原理
一句话讲,在JDK中,ArrayList底层基于一个Object[]数组来维护数据。
1.2ArrayList优缺点
缺点:
-
容量受限时,需要进行数组扩容,进行元素拷贝会影响性能
-
频繁删除和往中间插入元素时,产生元素挪动,也会进行元素拷贝,影响性能
-
不是线程安全的
优点:
-
随机访问某个元素,性能很好
建议:
维护顺序插入的数据,遍历或者索引访问很方便,预估数据大小,避免频繁扩容。不适合频繁的删除和中间插入等操作。
2.ArrayList源码解析
2.1如何看源码
看核心源码和精读源码的方式不太一样。看核心源码,也就是核心部分的源码,一般也就是10-30%。你应该都是从一些入口开始看起,但是如果你要精读源码,除了在阅读核心源码的基础上,你还需要将源码拆解开来,研究每一个组件的各个作用,之后再从入口开始,一行一行都读懂。应该先看核心源码再精读源码,从整体到细节,再主干到分支。
看源码无论是主动还是被动,都是先有问题,再有技术。为了解决什么问题,从而衍生出了解决问题的技术。
阅读源码的技巧,说白了就是一些思想,常见的有:
-
自顶而下
-
从整体到细节
-
在主干到分支
-
连猜带蒙
-
结合功能场景逐个分析
方法就更多了,主要有:
静态
-
看注释
-
加注释
-
画图
动态
-
Debug
-
观察日志或增加日志
阅读源码,需要很多技术基础,的确没错。但是也分是什么样的源码,如果只是简单的源码,需要的基础知识会很少,比如JDK的源码。但是如果比较难的源码,Zookeeper、HDFS、Kafka、Dubbo这些源码,就需要掌握Netty,NIO,网络,Java集合和并发等基础才能看得更好。当然如果没有这些基础也不是不能看的,就是理解可能不会深,对源码最后可能只是初步了解。所以还是那句话,不要一概而论。
2.2ArrayList源码整体
当你看ArrayList的源码,可以先看看它有哪些核心成员变量,刚开始,你肯定不知道哪些是核心的变量,所以简单过一下即可,根据名字,类型,大概看看即可,看不懂也没关系。之后看下有哪些方法,根据名字大概猜猜是做什么的。做这些主要让你了解基本的脉络,对源码有个大体映象,千万不要在这里细究源码。你主要有哪些方法和成员变量、内部类就可以了。
比如,你可以看到ArrayList源码中,有几个静态成员,从名字上看像是容量大小相关的,还有size和elementData等成员变量,有几个构造函数,
有一些ensureCapacity,calculateCapacity等私有方法,感觉也是和容量大小有关的。因为Capacity就是容量,大小的意思。还有很多你应该常用的方法add(),set(),get()等等。
2.3从ArrayList构造函数开始
之前我们提到过,看核心源码一般是从入口开始,也就常说的自顶而下。
首先你要有个代码Demo。之后从它的入口开始看起,代码清单如下:
import java.util.List; public class ArrayListDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); } }
这个很简单的代码,入口是main函数,第一行就是创建了一个ArrayList,里面的元素都是String类型,使用默认无参的构造函数,你点击构造函数,进入源码来看看:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size; public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
可以看到,有一个成员变量叫elementData的Object[]数组,这个印证了我们之前提到过的ArrayList底层基本原理是数组。而且你记不记得之前,在看ArrayList源码脉络的时候是不是已经看到过了这个变量呢?
默认无参的构造函数让elementData指向了和空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA一样的地址。
如果各位如果有印象的话,DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个变量也是你之前看到过的静态成员变量。
上面还列了一个成员变量size,你可以连蒙带猜一下,它应该是描述数组大小的变量。而且size是int类型,大家都知道int的默认值是0。
所以小结一下,你知道了new ArrayList<>()这个动作,底层是使用了Object[]数组,但是这个数组默认是空的,大小为0。
上面我们主要通过看脉络,连蒙带猜的思想,看了一个简单的不能再简单的源码。不知道你有没有感觉到这两个思想。有了初步的感受。接下来我还要引入一个非常关键的方法:画图。
上面ArrayList的无参构造函数的源码,基本上如下图所示:
2.4ArrayList第一次调用add方法
首先你要修改下你的Demo,修改如下:
import java.util.ArrayList; import java.util.List; public class ArrayListDemo { public static void main(String[] args) { List<String> hostList = new ArrayList<>(); //默认大小是0 hostList.add("host1"); } }
上面代码,假设你通过add方法向hostList添加了1个host主机地址。ArrayList第一次调用add方法发生了什么?
你可以点击进去,看到如下代码清单:
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
看之前,我又要多说两句,源码的注释和方法名,有时候能帮助你了解这个方法大致的作用。这是很关键的一点。比如注释的意思是add方法是追加一个具体的元素到list的末尾;方法名ensureCapacityInternal,是确保内部容量的意思。看过之后,你应该可以猜到这方法大致是确认容量相关的,可能是判断容量能否添加元素用的。
还要注意的是,你看一个方法的时候,不要着急直接从第一个行就开始看,也是先根据注释和方法名有一个大概的认识后,你需要看下整个方法的结构之后再开始。比如调用了哪些其他方法,有几个for循环或if结构。就像这个add方法,他主要有2步,第一步是调用了一个方法,应该是确保内部容量是否够添加元素,第二步是一个数组基本赋值操作并且size++一下。如下图所示:
2.5ArrayList的数组大小为0也能可以添加元素?
接着当你知道了整个方法的脉络,你再来看下细节,先看第一步:ensureCapacityInternal()。
这个方法入参是size + 1,size之前说你应该看到过,就是一个成员变量int size。它的默认值是0,所以size+1后,这个方法的实参就是1,那么形参minCapacity的值就是1。(实参就是传入方法实际的值,形参就是方法括号中使用的参数名)。可以看到ensureCapacityInternal的代码清单如下:
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
接着你可以看到这里又调用了calculateCapacity方法和ensureExplicitCapacity方法。我们先进入calculateCapacity看下,你可以记下它的实参是minCapacity=1,elementData还记得么?是ArrayList核心的那个成员变量Object[]数组。calculateCapacity代码如下:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private static final int DEFAULT_CAPACITY = 10; private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
这里你可以看熟悉的两个成员变量:elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
在ArrayList无参构造函数的时候就看到过。第一次调用add方法的时候,它们俩的引用地址和值都是应该一样的,因为在构造函数中有过这么一句话this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
所以这里进入第一个if条件,然后使用DEFAULT_CAPACITY和minCapacity作比较,可以看到DEFAULT_CAPACITY这个变量的值是10,也就是说这里Math.max操作后会返回10。
到这里你可以在完善下之前画的图,就会变成如下所示:
也就是说calculateCapacity返回后,回到ensureCapacityInternal这个方法,会传入ensureExplicitCapacity的实参是10,因为calculateCapacity(elementData, minCapacity)= 10。
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
接着你又会进入ensureExplicitCapacity方法,看到如下代码:
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
此时形参minCapacity也就是刚才传入的10,你可以从名字上看这个方法,ensureExplicitCapacity意思是确保精确的容量的意思。还是先看下方法的脉络,第一行有一个modCount++,之后你可以看到一行代码注释,overflow-consciouscode意思是说具有溢出意思的代码,之后有一个if条件,满足会进入grow方法。
到这里你可以连蒙带猜下,这里的grow方法,是不是就是说容量不够,会进行增长,也就是扩容呢?
因为我们知道,无参构造函数创建的ArrayList大小默认是一个为0的Object[]数组elementdata。而且elementdata.length肯定是0,难道也能添加元素么?肯定是不可以的。
接着我们逐行看下,第一行的modCount好像不太知道是什么,你可以点击下它,看到它是一个成员变量,而且有一大堆注释,好像也没看懂什么意思。所以这里要告诉大家另一个看源码的思想了:抓大放小。比如这里看上去modeCount和添加操作没啥关系,就先跳过!(其实这个modCount是并发操作ArrayList时,fail-fast机制的实现)
目前执行代码如下图所示:
2.6ArrayList 第一次添加元素如何扩容
你接着往下看,很明显,minCapacity=10,elementData这个空数组length肯定是0。所以10-0>0,会满足if条件,进入grow方法,它的实参是10。它的代码清单如下:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
grow方法形参minCapacity的值是10,你进入这个grow方法后,从脉络上来看,有两个if分支,有两个局部变量oldCapacity和newCapacity,还有一步elementData通过Arrays.copyOf方法的赋值操作。
而且oldCapacity应该是0,因为我们知道elementData数组是空的。接着你会看到一句:intnewCapacity = oldCapacity +(oldCapacity >> 1);
这句话是什么意思呢?其实就是oldCapacity右位移1位。如果你还记得计算机基础中的话,右移一位,有点类似于除以2,底层是二进制的运算而已。这里可以举个例子:
比如oldCapacity=10,如果先左移1,就是乘以2,在右移 1就是除以2,如下所示:
左移和右移1位举例: |
二进制 1010 十进制:10 原始数 oldCapacity 二进制 10100 十进制:20 左移一位 oldCapacity = oldCapacity << 1; 二进制 1010 十进制:10 右移一位 oldCapacity = oldCapacity >> 1; |
那么int newCapacity = oldCapacity +(oldCapacity >> 1); 其实这句话的意思就是在原有大小基础上增加一半大小。
但是由于oldCapacity=0,所以增加一半大小,newCapacity=0+0>>1=0+0=0。
而此时minCapacity=10。这样就会进入第一个if条件,因为newCapacity - minCapacity < 0,即0-10<0,然后进行了一步赋值操作,newCapacity就会变成和minCapacity一样,值是10。
这里你可以总结一下,也就是说如果创建ArrayList时,不指定大小, ArrayList第一次添加元素,会指定默认的容量大小为10。
你可以继续完善你的图,grow方法逻辑如下:
2.7ArrayList计算完扩容容量大小后,又干了什么?
除了上面计算扩容容量大小的代码,是grow的核心逻辑之一。
第一次添加元素时,在grow中,最后还会执行一行代码:
elementData= Arrays.copyOf(elementData, newCapacity);
这个也是grow方法中另一个核心步骤,数组的拷贝。
你可以点击Arrays.copyOf,看看它底层做了些什么。代码如下:
public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } public static <T,U> T[] copyOf(U[] original, int newLength , Class<? extends T[]> newType) { T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength)); return copy; }
还是从脉络上看下,这里Arrays.copyOf它实参是elementData(Object[]空数组)和newCapacity=10。可以看到它内部直接调用了一个重载方法。
在重载方法中,首先调用了一个三元表达式和一个System.arraycopy方法调用,接着执行了
System.arraycopy(original, 0, copy, 0,Math.min(original.length,newLength));
这句话,它看样子像是操作了original和 copy的样子。
之后你再来仔细分析下细节。
2.8ArrayList的缺点,原因原来是在这里!
根据传递的参数elementData的class是Object[].class的类型,可以看出三元表达式会执行(T[]) new Object[newLength]。
接着就会执行System.arraycopy这个方法。你可以查阅下JDK的API,它的主要是作用是数组的拷贝。
由于这个方法的API不是很好理解。这里我给大家讲下,它的方法签名如下:
System.arraycopy(Objectsrc,int srcPos,Object dest,int destPos, int length)。
这里,如果大家遇见难以理解的代码,除了画图,另外一个方法就是举例子。比如:
举个例子:
假设:有两个数组src和dest,它们都有5个元素,0,1,2,3,4。即:src[0,1,2,3,4] dest[0,1,2,3,4]。 问题:执行System.arraycopy(src, 3, dest, 1,2)后是什么意思呢? 答案:意思就是从src源数组3位置开始,移动2个元素到dest数组位置1, 从dest数组的1位置开始覆盖。 结果dest就会变为0,3,4,3,4
好了,你知道了这个API基本的使用,再来看源码中的代码:
System.arraycopy(original,0,copy,0,Math.min(original.length, newLength));
首先original就是我们传递进来的elementData。它的length是0。newLength是传递进来的newCapacity,也就是10。copy 是我们刚创建的Object[]数组,长度为10。Math.min取最小后是0。也就是变成:
System.arraycopy(original,0, copy, 0,0);
这个就很好理解了,就是从original位置0开始移动0个元素到copy数组,从copy数组0位置开始覆盖。这就等于什么都没拷贝,直接返回了我们新创建的数组T[] copy,这个数组大小为10。
最终Arrays.copyOf获得了一个长度为10的数组,但里面没有任何元素。接着这句话就执行完了。elementData = Arrays.copyOf(elementData, newCapacity);结束grow方法的也就执行完了。
而grow方法执行完就意味着ensureCapacityInternal执行完了。这个方法已经确保了内部数组的容量大小可以放入新元素。
我们回到开始,执行完第一步,内部容量确保后,接着第二步直接执行了elementData[size++]= e;通过数组的赋值操作,就完成第一次元素的添加了!
到这里可以发现,当添加的时候,如果大小不够就会进入grow方法,进入grow方法就会进行一次1.5倍的扩容。而且代码规范一般都建议我们要指定ArrayList的大小,如果不指定,可能会造成频繁的扩容和拷贝,造成性能低下。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
好了,到这里ArrayList最常用的add(E e)方法的源码原理就已经研究透彻了。
最终的源码原理图如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY