前言:我相信大家对ArrayList并不陌生,这是我们最经常使用的一个基于数组实现的列表,但我们平常可能也只是局限于使用,并没有深入去了解过它的实现。所以出于应对面试或者是为了学习数据结构,学习一些集合源码对我们来说都是很有帮助的。

类图关系

image.png

成员变量

既然如此,那我们话不多说,让我们先从ArrayList开始学习吧。首先,正如前言所说,ArrayList是一个基于数组实现的列表,那么在它的内部肯定会维护一个数组,这个数组最开始时并没有初始化,但它拥有一个默认的初始化长度10。

//默认容量大小
private static final int DEFAULT_CAPACITY = 10;

//对象数组
Object[] elementData;

//元素个数
private int size;

//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

//默认容量大小的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

构造函数

我们了解了成员变量后,接下来我们去看下它的主要构造函数和作用吧。ArrayList主要有两个构造函数,分别是无参构造函数和一个可以自定义初始化容量的构造函数。当我们调用无参构造函数时,它会为数组初始化一个默认初始化长度为10的数组。但值得我们注意的是,它只是为我们的赋值一个空数组,只是再后续比较容量函数中将默认的10拿去比较而已。

//无参构造函数,
public ArrayList() {
        //赋值一个空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//传入初始化容量
 public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //创建一个初始化容量的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果等于0,则将这个空数组赋值给对象数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //其他为非法
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
}

成员方法

1.如何添加一个元素 ?

既然这样,那让我们看下如果我们不设初值时,ArrayList是怎么初始化第一个数组的吧。首先让我们一步步的看add(E e)方法。它主要分为两步走,分别是如下的两步。

第一步:确定内部容量的大小
第二步:添加元素

public boolean add(E e) {
        //确定内部容量大小
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //先赋值,再移动指针
        elementData[size++] = e;
        //插入成功则返回true
        return true;
}

紧接着让我们再去看下它是确定内部容量大小的吧。在计算容量capacity时,它会判断调用的是什么构造函数;如果是无参的,它会计算容量。如果是有参的它直接返回传进去的minCapacity,而这个minCapacity是数组中元素的个数。正如下面这个方法所做的事情。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //如果是默认大小的空数组,即调用无参构造函数时
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        /*
            将默认的容量大小与元素个数进行比较,取最大值返回
        */
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //这个是调用有参构造函数。
    return minCapacity;
}

在我们看完计算容量大小的方法后,我们可以返回到上一个确定容量的方法了。也就是 ensureExplicitCapacity(int minCapacity) 这个方法。在这个方法中所做的事情主要是判断是否需要扩容。如果需要扩容,则会调用扩容方法。

private void ensureExplicitCapacity(int minCapacity) {
        //被修改的次数加一
        modCount++;

        // overflow-conscious code
        /*
            如果你传过来的这个最小容量比数组大小大了,他就会进行扩容,
            这里的最小容量就是元素个数+1,比如数组大小是10,size+1=11>10就会进行扩容
        */
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}

在完成上面那些操作后,就会进行尾插入操作,至此一个完整添加操作就完成了。最后执行插入的方法如下

//进行尾部插入
elementData[size++] = e;
2.如何进行扩容 ?

在介绍如何添加元素时,我们题到如果元数个数大于数组长度时,会进行扩容操作。那么ArrayList是如何进行扩容的呢?接下来让我们了解一下前面提到的 grow(int minCapacity) 扩容方法。在扩容方法中,会按旧容量的1.5倍大小进行一次扩容。如果扩容后的容量还是小于元素个数,则会将元素个数赋值给新的容量。如果容量比规定的最大容量MAX_ARRAY_SIZE还大,则新的容量等于Integer的最大值。在初始化newCapacity后,则会调用Arrays.copyOf()方法将老数组的元素搬到扩容后的新数组返回。

private void grow(int minCapacity) {
        // overflow-conscious code
        //旧容量为数组大小
        int oldCapacity = elementData.length;
        //新数组按就数组1.5倍扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果扩容后的数组容量还是小于元素个数,则直接将这个元素个数赋值给新的容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新的容量比规定最大的容量还大,则会调用hugeCapacity的方法,返回的是Integer的最大值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //调用Arrays的copyOf方法复制一个新的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
}
3.如何按索引添加一个元素?

在我们理解完一个基本的添加方法和扩容方法后,让我们看一下如何指定索引的位置添加一个元素?其实指定索引的位置添加元素主要的思路是,将指定索引后的元素往后搬,空出一个位置再将我们要插入的元素插入到index上。

/*
   我们看一下Sysytem类的arraycopy方法,其中的四个参数分别代表是:
   @src : 源数组
   @srcPos : 源数组的下标
   @dest:目标数组
   @destPos:要移动的元素下标
   @length:要移动元素的个数
*/
arraycopy(Object src,int srcPos,Object dest, int destPos,int length);
 思路过程:我们index为1的位置插入5
                  原始数组:1,2,3,4 
                  搬移元素:1, ,2,3,4
                  插入元素:1,5,2,3,4

 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++;
 }
4.如何删除元素 ?

介绍完添加元素的操作,我们来介绍一下删除操作。删除操作remove(index)方法中,它的思路和上面讲得按索引位置添加元素刚好相反,它是将index的旧值返回,然后再将index位置后的元素往前覆盖。其中需要注意的点如numMoved这个变量是指需要搬迁的元素的个数。

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);
         //将这个元素设置为null方便 GC                 
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
}
文章末尾

至此,我已经简单介绍了ArrayList的一些重要的成员变量,构造函数和成员方法。相信大家在阅读完,会对ArrayList有了基本认识。ArrayList的实现在Java集合中还是相对简单,也是线程不安全的。如果有疑问的或者存在你认为可以改进的地方,可以找我交流或给我建议,让我们一起进步哟。


Fudada
0 声望1 粉丝

喜欢画画,喜欢后端。