JDK源码剖析-集合篇-ArrayList

1.ArrayList基本原理以及优缺点

1.1ArrayList基本原理

一句话讲,在JDK中,ArrayList底层基于一个Object[]数组来维护数据。

1.2ArrayList优缺点

缺点:

  1. 容量受限时,需要进行数组扩容,进行元素拷贝会影响性能

  2. 频繁删除和往中间插入元素时,产生元素挪动,也会进行元素拷贝,影响性能

  3. 不是线程安全的 

优点:

  1. 随机访问某个元素,性能很好

建议:

维护顺序插入的数据,遍历或者索引访问很方便,预估数据大小,避免频繁扩容。不适合频繁的删除和中间插入等操作。

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)方法的源码原理就已经研究透彻了。

最终的源码原理图如下:

 

posted @   民宿  阅读(121)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· 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
点击右上角即可分享
微信分享提示