基于JDK1.8的ArrayList源码分析

222 阅读12分钟

概述

ArrayList 本质上是一个动态调整长度的数组,可以保存Item元素的容量可以动态增长,允许每个元素都为null,但它线程不安全。从源码定义来看,它继承了抽象类 AbstractList,并同时实现了 List、 RandomAccess、 Cloneable、 java.io.Serializable这四个接口。

其中:

RandomAccess接口:被ArrayList实现之后,表明ArrayList为我们提供了随机访问功能,也就是通过下标获取元素对象的功能。之所以是标记接口,是该类本来就具有某项能力,使用接口对其进行标签化,便于其他的类对其进行识别(instanceof)。而ArrayList数组实现,本身就有通过下标随机访问任意元素的功能。另外,我们需要细注意ArrayList的随机下标访问和LinkedList实现的顺序下标访问功能的不同。对于LinkedList而言,最好不要使用循环遍历,而是用迭代器遍历的原因。简单说就是,实现了RandomAccess接口就下标遍历,反之迭代器遍历。

Cloneable接口:被ArrayList实现之后,表明ArrayList为我们提供了从原ArrayListcopy一个新的ArrayList对象的能力。

Serializable接口:被ArrayList实现之后,表明ArrayList具备了序列化的能力。

而List接口:则为我们提供了add()、remove()、isEmpty()、contains()、toArray()、containsAll()、addAll()、replaceAll()、sort()、clear()、get()、set()、(last)IndexOf()、subList()等基本操作。

下面我们来看一下ArrayList的几个公共变量的定义:

无参构造初始化

接着我们先看看通过无参构造创建ArrayList对象的实现:

从源码实现我们就可以看可以看到,无参构造器仅仅是将空的(Empty)数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给ArrayList的elementData实例对象。

int类型入参的有参构造初始化

从上面的代码实现,我们可以看到,当入参initialCapacit大于0时,就将ArrayList的elementData实例对象初始化为长度为initialCapacit的空(Empty)数组;当入参initialCapacit等于0时,就将空的(Empty)数组EMPTY_ELEMENTDATA赋值给ArrayList的elementData实例对象;当入参initialCapacit小于0时,直接抛出异常;

Collection类型的有参构造

在这里,首先把Collection类型的入参c转换为Object[]数组a,然后再判断判断数组a的长度是否为0,如果是0,则将空的(Empty)数组EMPTY_ELEMENTDATA赋值给ArrayList的elementData实例对象。否则:如果入参c的getClass()对象是ArrayList.class对象,那么就直接将前面的Object[]数组a赋值给ArrayList的elementData实例对象;如果不是,则执行elementData = Arrays.copyOf(a, size, Object[].class),将Object[]数组a通过Arrays.copyOf()方法转换后,赋值给ArrayList的elementData实例对象。

上面我们了解了ArrayList的对象初始化的几种场景,下面我们开始了解ArrayList的一些基础功能。

ArrayList.add(E e)

其源码实现如下: 在add方法内部,首先调用了ensureCapacityInternal(size+1),该方法的核心作用就是对ArrayList的elementData实例对象进行扩容,以保证当前ArrayLis-elementData实例对象长度空间充足;另外一个辅助作用就是对modCount执行加1(+1)操作,用于判断elementData实例对象在迭代时是否进行了结构性修改。

然后是执行“elementData[size++] = e”操作,就是对elementData数组对象的下标“size++”位置处赋值为e;

ArrayList.get(int index)

其源码实现如下: get()方法功能很简单: 先是调用rangeCheck(int index)检查index下标是否符合要求:

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

rangeCheck(int index)的作用就是检查下标index是否在ArrayList的elementData实例对象的长度范围内,如果不在,就抛出数组越界异常“IndexOutOfBoundsException”,但这里有个缺陷,就是只检查了“index >= size”的情况,没有检查“index < 0”的情况。

然后再执行“elementData(index)”方法,“elementData(index)”方法很简单,就是读取ArrayList的elementData实例对象的index下标位置的元素值并返回。

ArrayList.add(int index, E element)

老样子,我们先来看看源码实现: add(int index, E element)方法的实现,相对复杂一点,但理解上海市没什么难度。 首先是调用“rangeCheckForAdd(int index)”方法来鉴定index下标的合法性,功能同rangeCheck(int index)一样,但这里弥补了rangeCheck(int index)方法中的缺陷,判断了“index < 0”的情况。

然后是调用“ensureCapacityInternal(size + 1)”方法判断ArrayList的elementData实例对象的扩容情况。

接着是执行“System.arraycopy(elementData, index, elementData, index + 1, size - index)”方法,该方法在这里的作用就是将elementData数组中从下标index开始的后面的所有元素Item向后移动一位,空出index下标位置。

最后是执行“elementData[index] = element”,就是将elementData数组的index下标位置赋值为element对象。然后执行size++操作,实现ArrayList长度加1的逻辑。

ArrayList.remove(int index)

remove(int index)方法起作用就是移除位于index下标位置的元素Item。源码实现如下: 第一步,还是调用rangeCheck(index)方法来鉴定index下标的合法性,这个不再多说;

第二步,执行“modCount++”,表示ArrayList又执行了一次结构性变化的操作;

第三步,调用elementData(index)方法,从数组elementData中获取到位于下标index位置的元素值,并赋值:E oldValue = elementData(index);

第四步,计算出从index位置的下一个位置开始,往后直至size-1的地方,一共要移动的Item元素的数量:int numMoved = size - index - 1;

第五步,如果numMoved>0,就执行“System.arraycopy(elementData, index+1, elementData, index, numMoved)”,其作用是从(index+1)位置开始,直到size-1位置为止,这之间的所有元素向前移动一位。

第六步,将数组elementData原本最后一位的元素置空,并对数组长度的size减1:elementData[--size] = null;

ArrayList.remove(Object o)

相比于remove(int index)方法,remove(Object o)方法的代码量虽然多一丢丢,但其逻辑确实更加简单:

总结起来就是两个for循环: 一、当要移除的对象o为空时,在数组elementData中,通过for循环,查找元素为空对应的下标位置index,然后调用“fastRemove(index)”方法来快速移除index位置的元素。相比“remove(int index)”方法,“fastRemove(index)”方法少了两个功能:

    一是不调用rangeCheck(index)方法来鉴定index下标的合法性;
    二是不调用elementData(index)方法,从数组elementData中获取到位于下标index位置的元素值,并赋值:E oldValue = elementData(index);

其他功能都一样。 二、当要移除的对象o不为空时,在数组elementData中,通过for循环,调用equals()方法将每一个Item元素与对象o作比较,记录下相同元素的下标index,然后调用“fastRemove(index)”方法来快速移除index位置的元素。 在这里需要注意的是:在for循环中,当查找到与要移除的对象o相同的元素和位置时,并没有立即马上调用return或者break关键字结束for循环,而是继续for循环,直至数组elementData的最后一个元素。 这是因为List及其子类是允许其保存的每一个元素有序的,且可以重复的,也可以为null的。

ArrayList.clear()

该方法就是通过for循环清空数组elementData的所有Item元素。源码实现如下: 第一步,执行“modCount++”,表示ArrayList又执行了一次结构性变化的操作; 第二步,for循环数组elementData,并将每一个Item置为 null; 第三步,将数组elementData的长度size置为0;

ArrayList.addAll(Collection<? extends E> c)

该方法功能是将一个新的Collection集合添加到已经存在的ArrayList的实例对象elementData中去。源码实现如下: 上述代码中,

第一步,是将Collection集合c转换为数组Object[] a;

第二步,是获取Collection集合c/数组Object[] a的长度:int numNew = a.length;

第三步,是调用ensureCapacityInternal(size + numNew)方法,对ArrayList的实例对象elementData数组进行动态扩容;

第四步,是调用System.arraycopy(a, 0, elementData, size, numNew)方法将数组Object[] a添加到elementData数组;

第五步,是更新elementData数组的长度;

最后是判断:numNew != 0,并将此作为执行addAll()是否成功的结果返回;这里其实操作是错误的,numNew != 0并不代表着将Collection集合c添加到elementData数组是成功的。应该是将源码中的最后两行代码改为这样来判断:

ArrayList.addAll(int index, Collection<? extends E> c)

该方法的作用是将Collection集合c插入到ArrayList的实例对象elementData数组中的指定位置index开始的区间内。而elementData数组终,从index位置开始直到(size - 1)位置的所有元素Item都后移c.size个位置。源码实现如: addAll(int index, Collection<? extends E> c)方法相比于addAll(Collection<? extends E> c)方法,多了以下几个操作: 一是,方法一开始就调用rangeCheckForAdd(index)鉴定index的合法性,是否存在下标越界的问题; 二是,计算出elementData数据中从index位置开始往后,要后移的Item的数量:int numMoved = size - index; 三是,调用“System.arraycopy(elementData, index, elementData, index + numNew, numMoved)”方法执行后移操作,将elementData数组中的元素,从index位置开始往后的所有Item元素,后移numMoved个位置; 最后执行原addAll(Collection<? extends E> c)方法种第三步及以后得操作。

ArrayList.removeRange(int fromIndex, int toIndex)

该方法是移除指定区间内的Item元素,源码实现如下:

modCount++ 操作的作用,前面已经介绍过很多遍了,这里不再说;

int numMoved = size - toIndex操作时计算出要移动的toIndex位置后的Item数量;

System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved)是执行移动操作,是将toIndex位置之后的numMoved歌Item元素前移到以fromIndex开始的位置;

int newSize = size - (toIndex-fromIndex)是计算出新的elementData数组的长度;

for循环是将elementData数组中,剩余的下标位置的空间置为null;

size = newSize则是更新长度;

ArrayList.contains(Object o)

该方法用于判断ArrayList数组对象elementData中是否包含Object o,如果包含则返回true,不包含返回false;该方法实现其实是以“indexOf(Object o)”为基础的,而indexOf(Object o)方法又跟remove()方法很相似,源码如下:

对比着remove()方法就很容易理解了,这里不多说;

ArrayList.set(int index, E element;源码实现如下:)

该方法作用是在指定位置index插入指定元素E element;源码实现如下: 第一步,调用rangeCheck(index)检验Index下标的合法性,是否有数组越界的可能; 第二步,调用elementData(index)方法,获取elementData数组中指定位置index处的元素Item:E oldValue = elementData(index); 第三步,将elementData数组中指定位置index处的元素Item替换为element:elementData[index] = element; 第四步,返回elementData数组中index下标位置原本旧的Item元素oldValue;

总结

基于前面对ArrayList的几个基础方法的分析,我们对ArrayList可以总结如下:

底层数组实现,使用默认构造方法初始化出来的容量是10;
长度是可以动态扩容的,扩容的长度是在原长度基础上加二分之一;
线程不安全,因为所有的方法均不是同步方法也没有加锁,因此多线程下慎用;
实现了RandomAccess接口,底层又是数组,get(int index)读取元素性能很好;
执行顺序添加add(E e)、更新set(int index, E element)以及移除remove(int size-1)也就是移除最后一个时,很方便,性能也比较高,因为不需要执行System.arraycopy()操作,复制数组;
删除中间位置的Item以及在中间位置插入Item时,需要System.arraycopy()操作,复制数组,性能很差(此时如果不考虑通过get(int index)下标读取数据时,可以考虑使用LinkindList)

这里顺便提一下几种常用的,但是又不是线程安全的几种数据结果如何实现线程安全:

1、对于ArrayList,有两种实现方式:

// 方式一
List<String> arraySafeList = Collections.synchronizedList(new ArrayList<>());
arraySafeList.add("创建一个线程安全的ArrayList");
// 方式二
Vector<String> safeVector = new Vector<>();
safeVector.add("Vector本身就是一个线程安全的集合,其功能与ArrayList几乎完全相同");

Vector本身就是一个线程安全的集合,其功能与ArrayList几乎完全相同

2、对于HashMap,也有两种实现方式:

线程安全的两种HashMap的实现方式:

一是,通过具有分段锁机制的ConcurrentHashMap来创建;
二是,通过Collections.synchronizedMap()来实现;

具体如下:

Map<String, String> linkedHashSafeMap = new ConcurrentHashMap<>();
Map<String, String> hashSafeMap = Collections.synchronizedMap(new HashMap<>());

3、对于LinkedHashMap:

Map<String, String> linkedSafeHashMap = Collections.synchronizedMap(new LinkedHashMap<>());

4、对于LinkedList:

List<String> linkedSafeList = Collections.synchronizedList(new LinkedList<>());
linkedSafeList.add("创建一个线程安全的LinkedList");

归纳起来,我们可以通过 Collections.synchronizedxxx(new xxx()) 系列方法创建一系列的线程安全的数据结果来实现我们的开发需求: image.png

为什么ArrayList的elementData是用transient修饰的?

transient修饰的属性意味着不会被序列化,也就是说在序列化ArrayList的时候,不序列化elementData。

为什么要这么做呢?

elementData不总是满的,每次都序列化,会浪费时间和空间
重写了writeObject 保证序列化的时候虽然不序列化全部 但是有的元素都序列化

所以说不是不序列化 而是不全部序列化,做局部序列化。

既然ArrayList是线程不安全的,那么类似ArrayList功能,但线程有安全的实现吗? 答案是有,而且还实现的非常好,那就是:Vector。

那ArrayList和Vector的区别是啥? 简单来说,就只有两点: 一是,ArrayList是线程不安全的,Vector是线程安全的。前者所有的方法均不是同步方法也没有加锁,而后者的关键方法都添加了同步锁; 二是,动态扩容时候ArrayList扩0.5倍,Vector扩1倍;当然,这在一定程度上导致了Vector的内存浪费;

那么ArrayList有没有办法实现线程安全? 答案是有,两种方案:一是自定义工具类,对重要核心的方法实现同步锁;二是Collections工具类有一个synchronizedList方法,通过它来实现。 但是,但是,把list变为线程安全的骚操作,意义不大,因为可以直接使用Vector来实现,不需要重复造轮子。

下面我们通过几个Vector的方法的源码实现,来理解Vector是线程安全的: 从以上源码中,可以看到,Vector几乎每一个方法都有添加synchronized同步关键字。