对于ArrayList,我们平时使用最多的方法应该是add和remove。本文主要从这两个基本方法入手,通过源码了解ArrayList的底层原理。add默认添加元素。这应该是最常用的方法。它的用法如下。接下来我们看一下add方法的底层源码。ensureCapacityInternal的作用是保证在不断向ArrayList中插入数据时,数组不会越界,实现自动扩容。这里minCapacaity的值其实就是调用当前add操作后数组中元素的个数。这里注意,当前的添加操作已经完成。比如在调用add之前,ArrayList中有3个元素,那么此时minCapacity的值为4。另外可以看到函数calculateCapacity的返回值作为ensureExplicitCapacity的输入。calculateCapacity的作用,说白了就是如果当前数组为空,则直接返回数组的默认长度(10)和minCapacity的最大值,否则直接返回minCapacity。接下来是ensureExplicitCapacity,源码如下:modCount表示ArrayList改变了多少次,这里的改变不仅仅是增加,还有删除。通过上面的理解,我们知道如果当前数组的元素个数小于数组的长度,那么minCapacity的值一定小于elementData.length。反之,如果数组的元素个数已经等于数组的长度,那么0>0一定为false,就会调用grow函数对数组进行扩容。核心逻辑很简单,新数组长度=旧数组长度+旧数组长度/2。这里的右移相当于直接除以2。因此,通过这里的源码,我们验证每次ArrayList的扩容都是1.5倍。但是这里会有一个疑问,因为上面说的minCapacity的值和数组的长度在扩容的时候应该是相等的,所以新的数组长度-minCapacity应该总是大于0的,为什么会小于0呢?答案是addAll。可以看出add和addAll底层会调用ensureCapacityInternal。add将单个元素添加到数组,而addAll将整个数组添加到数组。假设我们传入一个很大的值给addAll。例如ArrayList默认数组长度为10,展开一次后长度为15。假设我们传入的数组元素有10个,那么即使一次扩容也不足以放下15<20以来的所有元素。这就是为什么newCapacity(扩容后的数组长度)size(3个元素)肯定是不成立的。验证完成后,仍会调用上面讨论的ensureCapacityInternal方法,根据需要扩展底层数组。然后调用System.arraycopy方法。这个方法比较关键,也比较难理解,所以我就用一句简单的话概括一下它的作用——把下标做后的所有元素向后移动。System.arraycopy接收4个参数,分别是:原数组中的起始下标、目标数组中的起始下标、目标数组中的起始下标。单看参数,不举例很难理解。这是一个简单的例子。假设现在数组中有元素[123],然后我此时调用方法add(1,4),表示我要将元素4插入到数组下标为1的位置,那么此时index的值为1,size的值为3。System.arraycopy的方法会变成System.arraycopy(elementData,1,elementData,2,2),也就是复制从开始的两个元素elementData的下标1(即2和3)到下标2开始的elementData。可能还是有点绕。也就是说,执行完之后,elementData变成了[1223],然后给elementData赋值,把下标为1的元素变成本次需要添加的元素。用人类的话来说,它变成了[1423]。所以综上所述,没有黑魔法,主要要理解的是两个关键函数,即Arrays.copy和System.arraycopy。我们需要记住这两个封装函数的作用。Arrays.copy扩展数组。System.arraycopy将数组中下标之后的所有元素向后移动一位。所以从这里可以看出,看源码的好处主要有两个方面。首先,相信大家在做题的时候一定遇到过需要将数组的元素移回原位的情况,但是您可能并不知道可以使用System.我不明白;第二,我知道System.arraycopy,但是感觉这些函数完全没有应用场景。remove为了理解数据是怎么来的,我们先来看看数据是如何被remove的。首先我们来看最常用的两种:RemovebysubscriptRemovebyelementRemovebysubscript首先,removebysubscript这里我们先检查传入的索引是否合法,但是这里index和add调用的rangeCheck有点不同的。add中的rangeCheckForAdd会判断索引是否为负数,而remove中的rangeCheck只会判断索引是否>=数组元素个数。其实从函数名就可以看出rangeCheckForAdd是专门用于add方法的。如果此时传入的索引真的是负数怎么办?实际上,会抛出ArrayIndexOutOfBoundsException,因为remove方法是用Range注解的。一旦完成,modCount的值还是更新了,这也验证了我们上面提到的modCount代表的变化也包含了删除。接下来计算一个numMoved,代表需要移动的元素个数。添加一个元素,对应的下标元素需要向后移动一位,remove也是一样,删除某个位置的元素后,其后面对应的所有元素都需要向前移动一位,像这样:知道了之后许多元素需要移动,我们的System.arraycopy可以再次出现。元素移动完成后,数组末尾会有一个空元素,可以直接设置为null,交给GC回收,最后返回移除的值。按值删除例如,按值删除看起来像这样。废话不多说,直接看核心源码,第二行完全看不懂。删除null到底是什么鬼?你需要循环删除它吗?其实ArrayList允许我们传入null值,再举个例子。看完这个例子,你应该能明白为什么要进行o==null的判断了。如果传入null,ArrayList会遍历底层数组,移除第一个匹配到值为null的元素。如果该值不为空,则同样如此。如果数组中有多个相同的值,ArrayList也会遍历它们,去掉第一个匹配到的值。从源码可以看出,无论value是否为null,都会调用真正的方法fastRemove来删除元素,和remove几乎是一样的。它们之间唯一的区别是通过下标删除将返回删除的元素;按值移除只会返回移除是否成功。所以,在阅读了ArrayList的部分源码后,我们可以知道ArrayList的底层数据结构是一个数组。ArrayList虽然对用户来说是一个动态数组,但底层实际上是一个定长数组,只有在需要的时候,底层数组才会每次扩展1.5倍。但是从源码也可以看出,扩容和删除都是有代价的,尤其是在极端情况下,会需要移动大量的元素。所以我们得出结论,如果ArrayList有频繁的随机插入和频繁的删除操作,其性能会受到很大的影响。综上所述,ArrayList适用于读多写少的场景。另外很重要的一点,这里要提一下,ArrayList不是线程安全的。在多线程的情况下,会出现数据不一致或者抛出ConcurrentModificationException。这个我们以后再说。让我们谈谈如何从数据结构中访问和删除数据。其实理解起来也跟它有关系,也是顺理成章的。很多东西。例如,indexOf方法用于返回数组中指定元素的下标。理解了remove中的遍历匹配,甚至可以直觉直接思考,indexOf不就是一个for循环匹配吗?lastIndexOf不就是做一个反向的for循环匹配吗?所以这里把源码贴出来除了让文章长一点没有任何意义。如果对此感兴趣,可以找源码看看。