经常用ArrayList?来看看源码吧!

前言

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/59/

集合类中,最基础也是最常用的,大概就是ArrayList了吧。

ArrayList的本质,是一个可变长的数组。

那有人可能就会问,哎呀这个数组老简单了,有什么好看的啊……但事实上,在面试时,有些人还是对源码的细节说不清楚,从而留下较差的印象。

这里,我就带着大家,一点一点地梳理一下,ArrayList的底层源码吧。

概览

首先我们从全局把握一下这个类,这个类的签名如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList类继承自AbstractList类,且实现了List、RandomAccess、Cloneable和Serializable接口。其中要提一嘴的是,RandomAccess接口是个空接口,作为一个标志使用,当一个类支持随机访问的时候(数组是很典型的),就可以标记这个接口。

在这个类的JavaDoc中,描述了ArrayList的一些特征,主要如下:

  • 允许 put null 值,会自动扩容;
  • size、isEmpty、get、set、add 等方法时间复杂度都是 O(1);
  • 是非线程安全的,多线程情况下,推荐使用线程安全类:Collections#synchronizedList;
  • 增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常。

JavaDoc中还提到了fail-fast机制,这个会在下面将迭代器时提到。

初始化

ArrayList有三种初始化方法,其中一种是使用现有的集合来进行初始化,就不说了。主要看下面这两种:

private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

第一个构造方法是一个无参构造,使用默认的一个空数组作为初始的一个容器,容量为0。第二种需要指定初始化的容量,如果参数大于0,就会构造一个对应大小的Object数组,如果参数为0,就使用一个容量为0的空数组作为容器,如果参数小于0,就抛出一个异常。

扫描二维码关注公众号,回复: 11506645 查看本文章

有人可能就会问,哎这个无参构造和有参构造参数为0都是长度为0的空数组,为什么还用两个不同的数组呢?这是因为,在首个元素被添加后,数组是需要进行扩容的,两种构造方法构造出的数组,扩容到的大小是不同的,使用这个来加以区分。

这里一个很重要的点,就是在无参初始化的时候,构造出的底层数组长度为0,并不是大家所说的10,在添加第一个元素时数组长度才会扩展到10。

添加元素和扩容

ArrayList添加元素有两种方法,如下:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    rangeCheckForAdd(index);
  
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

第一种方法是直接将待添加的元素放在数组末尾,该方法首先调用ensureCapacityInternal()方法来确保数组容量可用,如果不够用就会在该方法内进行扩容,接着在elementData[size]位置放置e,并且siez自加1。

第二种方法则是在要求的位置放置某个元素,首先会调用rangeCheckForAdd()方法来检查index位置是否可用,其实也只是判断下是否超出数组范围,超出的话就会抛出IndexOutOfBoundsException异常。接着使用ensureCapacityInternal()方法检查数组容量,使用System.arraycopy方法将index位置及其之后的元素向后拷贝一个单位,再将待插入元素放置在index处即可。

这里放置新元素的时候没有进行任何的判断,所以ArrayList是允许null值的,且放置是没有加锁,使得ArrayList是线程不安全的。

显然,扩容部分的核心实现就在ensureCapacityInternal()方法中,我们来看看这个方法。

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

  // overflow-conscious code        
  if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

ensureCapacityInternal方法调用了ensureExplicitCapacity方法,该方法首先将modCount自增一,接着判断是否需要扩容,即判断需要的容量是否大于现有的容量,如果大于则调用grow()方法将底层数组扩容到minCapacity,否则无动作。

注意下ensureExplicitCapacity()方法传入的值是经过calculateCapacity()方法计算后的值,该方法实现如下:

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是使用无参构造构建的,那么我们就需要返回DEFAULT_CAPACITY和minCapacity中较大的值。那么在第一次插入的时候,显然DEFAULT_CAPACITY较大,默认值为10,那么我们第一次插入,数组就会扩容为10。而使用有参构造参数为0的方法的话,在这一步返回的仅仅是1。

grow()方法的实现很简洁,如下:

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);
}

除去最后一行,前面都是对newCapacity进行赋值,并且判断是否溢出之类的。

需要注意的是,newCapacity的选取,如果不考虑扩容溢出等情况,newCapacity的值为:oldCapacity + (oldCapacity >> 1),即旧容量的1.75倍。最后一行就是将旧数组的元素拷贝到新数组即可。

该方法的最后一个判断,将新的大小和MAX_ARRAY_SIZE的值比较,如果超过了这个值,那么newCapacity就会被赋为hugeCapacity()方法的返回值。MAX_ARRAY_SIZE的值为Integer.MAX_VALUE - 8hugeCapacity()方法传入的参数是最少需要的数组容量。在hugeCapacity()方法中有如下:

return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;

如果最少需要的值大于MAX_ARRAY_SIZE的话,就只会扩容到Integer.MAX_VALUE,否则就扩容到MAX_ARRAY_SIZE。这里就说明了ArrayList的容量上限为Integer.MAX_VALUE。如果达到了该值,就不会再为ArrayList分配空间。

删除元素

这里的删除有两种方法,一种是根据下标删除元素,另一种就是根据元素删除。

我们首先来看第一种,如下:

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

核心的删除就是System.arraycopy()方法,直接将index+1及其后面的所有元素向前移动了一个单位,那么index位置的元素就被覆盖住了,这时数组的最后一个位置应当是空的,那么就直接赋为null,并且size自减1。那么原本的最后一个元素就会被GC回收。

第二种remove的方式是根据元素进行删除,如下:

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

整体的逻辑是,判断传入的元素是否为null,如果是null,就选择删掉List中第一个null(ArrayList中允许有多个null),不是null的话就遍历元素,删除第一个equals的元素。注意这里使用的是equals()方法,如果不是基本类型的话,我们就需要关注equals()方法的具体实现。

删除的核心逻辑是fastRemove()方法,该方法传入要删除元素的下标:

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

这个方法很简单,逻辑和第一个remove()方法一致,也就是将后面的元素向前移动而已。

注意,在删除时,底层数组并没有进行索容,事实上,在扩容之后,数组的容量就永远不会小于该容量。

迭代器

要实现迭代器,只需要实现Iterator接口即可。ArrayList中实现了一个Itr类作为迭代器,并且在ArrayList中有一个iterator()方法,用于返回这个类的一个实例。

private class Itr implements Iterator<E>

Iterator接口一般来说需要实现三个方法:

  • hasNext(),是否还有值未迭代
  • next(),迭代下一个值
  • remove(),删除正在迭代的值

讲解方法之前,先看一些重要的属性。

int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

cursor表示迭代过程中的下一个元素的下标;lastRet表示上一次迭代过程的索引位置;expectedModCount表示期望的修改次数(版本号),这个值的初始值设置为数组的修改次数(版本号)。

hasNext()

public boolean hasNext() {
    return cursor != size;
}

该方法的实现非常简单,当前的cursor总是指向下一个元素,若cursor等于size,说明当前迭代的元素为最后一个元素,hasNext()返回false即可,否则一直有值未迭代,返回true。

next()

public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

该方法返回下一个迭代的元素,该方法开头首先调用checkForComodification()方法以确保在迭代过程中ArrayList没有被修改,该方法实现如下:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

这里仅仅是将modCount与expectedModCount相比较,两个值不同的情况下就会抛出ConcurrentModificationException异常。

修改数组的一些操作,例如add()或者remove()方法,都会使数组的modCount改变,如果在迭代过程中进行了这些操作,那么下一次next()方法就会抛出异常,这被称为fail-fast机制。

我们继续看next()方法。注意下面的操作:

if (i >= size)
    throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
    throw new ConcurrentModificationException();

首先判断i与size的大小,如果i大于等于size,说明越界了,需要抛出异常。接着又将该List的elementData的引用赋给了局部变量elementData,又判断了一次i与这个elementData的length大小。

这时有人就会问了,唉这不是多此一举吗,两次的i都是和同一个数组的大小相比较,返回的结果肯定一样啊。这可就不一定了,我们需要考虑的是,多线程的情况。有可能,在判断第一个的时候,没有发生越界,但是此时该线程被打断,另一个线程删掉了一些数据,造成了减小,那么这个线程在进行第二次判断的时候,就可能造成越界,注意这里抛出的异常是ConcurrentModificationException,说明其主要问题不在于越界,而在于迭代期间修改。

remove()

那么我们如何在迭代期间安全地删除数值呢,此时就可以使用Iterator的remove()方法:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

这里的删除很常规,主要还是调用ArrayList的remove()方法,但是在删除完成后,重新对expectedModCount进行了赋值,使得在下一次检查时保证相等。

猜你喜欢

转载自blog.csdn.net/qq_40856284/article/details/106736713