查看原文
其他

初探Java源码之ArrayList

王苓宇 Java后端技术 2022-04-29
前言

 在我们的日常开发中,集合类是我们基本上每个人都会用经常用到的东西,用着用着,突然有一天我心生好奇,那么java集合类的这些源码是什么呢?那么我打算接下来一个一个的查看一些常用的类源码争取达到心中有数的水平~~本文源码均来自Java 8

总体介绍


Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。Set和List两个类继承于它。Set中不能包含重复的元素,也没有顺序来存放。而List是一个有序的集合,可以包含重复的元素。

而Map又是另一个接口,它和Collection接口没有关系。Map包含了key-value键值对,同一个Map里key是不能重复的,而不同key的value是可以相同的。

在这里借用一张别人总结的对比图进行总结:

图1:集合类对比

(上图来源:http://www.cnblogs.com/leeplogs/p/5891861.html)
具体的各个类的实现子类在这就不在具体介绍,网上已经有很多介绍的文章,就不在这里再展开介绍。今天我们来专门看看ArrayList的源码。


成员变量


首先我们来看看ArrayList的成员变量:

可以看到主要的几个成员变量如上(跟进继承的父类,父父类直到根父类都没有成员变量)。我们来一一介绍一下。首先是一个常量DEFAULT_CAPACITY,根据注释表示默认的长度为10。然后是一个EMPTY_ELEMENTDATA的常量object数组,只是空有其表没有内容。然后是一个object数组elementData。

这个就是最重要的成员了,通过注释我们可以看到这表示这个数组用来存储我们的数据。也就是说,我们代码中的add的数据都会放在这个数组里面。那么由此我们可知,ArrayList内部是由数组实现。再看最后一个变量,int类型的size。

第一眼还以为是elementData数组的长度。仔细看注释,才发现它表示的是elementData数组里面包含的数据长度。


构造函数


介绍完了成员变量,我们来看看构造方法:

我们看到主要有三个构造方法。


(1)第一个构造方法需要传入一个int类型的变量。表示我们实例化一个ArrayList的时候想让ArrayList的初始化长度为多少。然后如果该变量大于0,那么new一个长度为传入值的对象数组。如果传入为0,那么等于EMPTY_ELEMENTDATA。这个变量我们上面讲过,就是实例化一个对象数组,内容为空。如果小于0,那么抛出异常。

(2)第二个构造方法是无参构造方法,直接等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA。没有其他的好说的。

(3)第三个则我们另外一种常见的使用方法,比如处理化AList的时候想把BList的值传给AList。

那么使用如下代码:

我们看看构造函数做了什么。我们看到首先调用了c.toArray()方法将我们传入的集合元素变成一个数组来赋值给elementData数组。

然后判断elementData数组里面是否有数据元素,如果有,那么再判断elementData数组类型是否为Object,不是的话,转为Object类型。如果没有元素,那么直接赋值为EMPTY_ELEMENTDATA。

至此三个构造方法就已经分析完了,基本上没有什么难度。


常见方法


接下来我们来分析一些ArrayList的常见方法。

(1)size()

很简单,就是将elementData数组中元素个数返回。

(2)isEmpty()

也很简单,就是判断sizes是否等于0,即elementData数组中是否有元素。

(3)add()

我们先来看add(E e) 方法:

可以看到add()方法还是比较简短的,接下面我们一步一步的分析,第一行是调用了一个方法,第二行是常见的数组赋值,将下标为size处的数组元素赋值为e,然后size自加1。如果有意识的话,会想到,咦?这么做的话,不怕数组越界??那么我们去第一行代码的方法里看看:

首先第一个方法中先判断elementData是否是没有元素的数组(但并不是elemetData为null)。如果是,那么取我们传入的值(也就是size + 1)和默认的数组长度(长度为10)中的最大值。然后调用了ensureExplicitCapacity()方法。我们继续看这个方法:

首先是一个int类型的成员变量modCount自加,这个变量是ArrayList的父类AbstractList的一个成员,用来表示List的修改次数。接下来有一个判断,用传入的值减去当前elementData的长度,如果大于0,调用grow()方法(我个人理解为扩展的意思),这里其实也能猜出大概意思。如果我们所需的最小数组长度已经比当前数组长度大了,那么就需要我们扩展数组了。我们接着看grow()方法:

我们接着看代码,首先保存当前数组长度到oldCapacity,然后定义一个newCapacity,新长度为旧长度的3/2,也就是增加一半的容量。然后用新长度减去所需最小长度,如果小于0,意味着新长度比所需长度还要小,那么就直接将新长度改为所需最小长度。

然后新长度如果超过了允许的数组最大长度,调用hugeCapacity()方法进行调整。最后处理完毕后,调用Arrays.copyOf()方法赋值给elementData。至此就把elementData数组扩展完毕。然后回到add方法中直接赋值 elementData[size++] = e即可。

我们来看第二个add()方法:

上面的方法也是我们常用的,将指定的下标处元素赋值为我们设定的值。最开始我用这个方法的时候一直很担心假设我把指定位置设置了值,那原来的值会不会被覆盖呢?

我们看一下实现代码解惑一下,也很简单。首先检查index索引是否比elementData中拥有元素的数量大或者小于0。有问题则抛出异常。负责又调用ensureCapacityInternal()方法来确认数组长度是否足够。然后调用System.arraycopy()方法,我们来看看:

可以看到是个native方法,我们就不跟源码了,看看参数含义就明白了。src就是源数组,srcPos就是表明从源数组的下标多少开始复制,dest和destPos就是对应的目的数组,复制源数组的数据到从目的数组的下标开始存放,length就是打算复制多少个源数组的值。

说了半天有点绕口,看看上面的调用例子,我们用具体例子来讲解。首先源数组是elementData,假设有6个元素(即size为6,但是elementData的长度大于6),index假设为3,length为size - index为3。而dest也为elementData,destPos为index + 1 等于4。

所以整体就是从index(3)下标处即elementData[3]处开始往后拿3个值,复制到elementDatadestPos开始往后3个值。

其实解释了半天就是将我们要插入的位置开始的元素全部往后移了一个位置。然后把值插入到指定的位置。(我真聪明)所以之前的担心就多余啦。我们插入到指定位置,指定位置的旧值会往后移,并不会被覆盖。

(4)clear

clear()方法也很简单,首先modCount自加,表示我们对list进行了操作。然后for循环置空即可,最后设置size等于0。

(5)remove()

remove()也有两个方法,我们来看第一个:

根据传入参数我们也能猜到意思,就是移除指定下标的元素。

首先还是检查index是否有效。然后modCount++,表示我们对list又进行一次操作。然后将指定下标的元素取出。然后计算出我们需要移动多少个元素,指的是从删除位置往后的元素,不包括删除位置的元素。如果个数大于0,那么调用 System.arraycopy()方法将删除位置后的一个元素开始到最后的元素往前移动一个位置。

然后将size立马自减,然后将最后一个位置置为null(因为元素往前移动一位,那么最后一个元素往前移后,原来的最后一个位置值还存在没有被覆盖)。

最后返回旧的删除位置的元素值。

接下来我们来看第二个remove()方法:

很明显看参数也猜得出是直接移除掉我们某个元素。首先判断我们传入的object是否为空,如果为空,那么就for循环找到第一个数组中值为null的元素,调用fastRemove()方法,我们去看看:

从注释看出,其实这就是第一个remove()方法的简化版,取消了越界检查,并且设置返回类型为void,不再返回删除的旧值。这里就不再分析。

接着上面说,如果remove()中如果传入的对象不为null,那么就是for循环找到这个值然后移除即可。整个函数返回类型为boolean,true表示有这个对象删除成功。没有表示数组里没有这个对象,没有进行删除操作。

(6)contains()

contains()也是我们经常使用的方法,用来查询当前ArrayList是否包含某个对象。我们看调用了一个indexOf()方法然后把返回值和0进行比较(乍一看还是很奇怪的,返回布尔值不好吗),我们来看看indexOf()方法:

我们来看看代码,首先是对传入对象的判空。如果对象为空,还是一样的,for循环来查找elementData中第一个为null的元素,然后返回下标。如果传入对象不为空,那么一样for循环查找第一个匹配元素,然后返回第一个匹配元素的下标。如果都找不到,那么就返回-1。

看完这个方法,就明白了为啥不用返回值布尔类型了,原来是返回下标来和0进行判断是否包含。但是我们可以看到其实contains()方法并没有返回元素下标。

所以本人第一次看完代码觉得这是多此一举。后来突然想到indexOf()方法是一个public方法,也是我们经常使用的方法。可能就在这里java编写者进行方法重用就不必再重复写新方法来判断。

顺带着我们就把indexOf()方法介绍,方法就是返回第一个匹配传入对象的元素下标。如果数组中没有匹配元素那么返回-1。

(7)get()

这个方法可以说是最最常用的方法了,但是其实我们看到非常简单,就是进行一个下标的越界判断,然后返回elementData[index]元素。

set()

set方法也是,其实set(int index, E element)和add(int index, E element)方法很相似。只是set是将指定位置的值直接覆盖掉,而add()则是将指定位置开始的元素往后全部后移一位,旧值不会被覆盖掉。set()方法没有什么可以多分析的代码。

至此我们常见的ArrayList的方法源码分析就已经完了,其他的一些方法要么不怎么用,要么非常简单只有一两行简单代码,读者一跟进去就能明白。

总结


  • 首先ArrayList内部是由数组来实现的。而且在存放数据的数组长度不够时,会进行扩容,即增加数组长度。在Java 8中是默认扩展为原来的1.5倍。

  • 既然是数组,那么优点就是查找某个元素很快。可以通过下标查找元素,查找效率高。但是由此也看出缺点,每次删除元素,都会进行大量的数组元素移动,复制新的数组等等。增加元素的话如果长度不够,还要进行扩容。因此删除效率低。如果我们在实际开发中能够清楚知道我们的数据量,建议创建ArrayList的时候指定长度,这样无需频繁增加数据时不断进行扩容。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存