JAVA后端开发知识总结(持续更新…)


ArrayList扩容机制和主要方法(源码解析)



文章目录

  • ArrayList扩容机制和主要方法(源码解析)
  • 一、ArrayList基本概述
  • 二、ArrayList的扩容机制
  • 三、ArrayList的常用方法分析
  • 参考文档



一、ArrayList基本概述

  ArrayList是实现了List接口的基于动态数组的数据结构,可以用来存放各种类型的数据,ArrayList按照插入的顺序来存放数据。但是ArrayList不是线程安全的。

  • ArrayList的主要属性
// 数组的默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;

// 定义一个空数组以供其它需要用到的地方调用 
private static final Object[] EMPTY_ELEMENTDATA = {};

// 定义一个空数组
// 用来判断 ArrayList第一次添加数据的时候要扩容多少
// 使用默认构造器的情况下返回这个空数组 
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// ArrayList的底层结构 
// 使用默认构造器时,第一次添加数据,容量扩容为 DEFAULT_CAPACITY
transient Object[] elementData;

// 已经使用的容量大小
private int size;

  DEFAULTCAPACITY_EMPTY_ELEMENTDATA:使用默认构造函数时返回的空数组。如果是 ArrayList第一次添加数据,数组扩容为 DEFAULT_CAPACITY = 10。

  EMPTY_ELEMENTDATA:出现在需要用到空数组的地方,例如,当在构造函数中指定初始容量为0的时候就会返回它。

  • ArrayList的默认构造函数
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

  默认构造函数一开始使用参数DEFAULTCAPACITY_EMPTY_ELEMENTDATA 返回一个空数组,所以 ArrayList 在创建之初没有指定初始容量的情况下,就会返回一个长度为0的空数组。但它在第一次添加数据时就会返回一个初始容量为10(扩容后的结果)的数组

二、ArrayList的扩容机制

  所有添加数据的操作都要需要判断,当数组容量不足以容纳新的数据就需要进行扩容操作。

  • ensureCapacityInternal

  只有调用了默认构造函数,才可能返回默认为10的容量,如果是构造函数中传入的 Capacity为0,则不会生成默认大小。此时隐含有 minCapacity = size + 1的信息。

// 判断是否需要扩容
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 判断当前数组是否是默认构造函数法生成的空数组
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果是则取容量为 max(10, minCapacity)
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 不是就根据原来的值传入下一个方法完成后面的扩容判断
        return minCapacity;
}
  • ensureExplicitCapacity

  判断当前ArrayList是否需要进行扩容,如果修改后的数组容量大于当前的数组长度,就需要调用grow() 进行扩容。

// 判断当前ArrayList是否需要进行扩容
private void ensureExplicitCapacity(int minCapacity) {
	// 快速报错机制:防止多个进程同时修改同一个容器的内容
	// 不一致会抛出ConcurrentModificationException
	modCount++;
	if (minCapacity - elementData.length > 0)
	    // 容量过小,需要扩容
            grow(minCapacity);
}
  • 扩容核心——grow
  1. 如果当前数组是由默认构造函数生成的空数组且第一次添加数据,则 minCapacity默认等于10,此时数组的容量会从0扩容为10。之后的数组扩容才是1.5倍扩容
  2. 如果当前数组是由带参构造函数生成且初始容量为0,则minCapacity等于1,此时数组的容量会从0变成1。但是设定初始容量为0,会导致前四次扩容每次都 +1,只有在第5次添加数据进行扩容的时候才是按照当前容量的1.5倍进行扩容。
  3. 当扩容量大于ArrayList定义的最大值后进行判断,根据与MAX_ARRAY_SIZE的比较确定是返回Integer最大值还是MAX_ARRAY_SIZE。ArrayList允许的最大容量就是Integer的最大值
// 用来决定扩容量
private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // 1.5倍扩容
        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);
}

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

三、ArrayList的常用方法分析

  • add(E e)

  每次添加数据前,ArrayList都会先调用ensureCapacityInternal方法来判断是否需要扩容,接着再尾插,所以ArrayList是按插入的顺序排序。

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  
        elementData[size++] = e;
        return true;
}
  • add(int index, E element)
  1. index表示要插入的索引,先判断数组是否越界,然后调用ensureCapacityInternal方法进行扩容逻辑。
  2. 之后调用System.arraycopy方法来进行复制操作,该方法为本地(native)方法。ArrayList每次调用该方法添加数据时都会进行数组的复制,复制时把相对于index后面的数据都向后移动一位
  3. 过多的复制导致ArrayList在对数据的插入操作效率比较差,ArrayList在随机插入数据的效率上比不了LinkedList。
// 按 index进行插入操作
public void add(int index, E element) {
	// 越界判断
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  
        // 复制操作
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
}
  • remove(int index)

  该方法同样使用System的本地方法arraycopy进行复制,它把相对于index后几位的数据全部向前移动一位,最后返回被删除的数据。

// 删除指定位置的元素
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);
        // 便于GC回收最后一个的空间
        elementData[--size] = null;
        return oldValue;
}
  • remove(Object o)
  1. 该方法是ArrayLsit对Object对象的删除,该方法在对Object对象删除时区分了Null与非空的情况。
  2. 不管是哪种情况,只调用fastRemove删除一次就返回。
  3. 非空时该方法采用equals来判断数组中的对象是否相等,如果这个被操作的类没有重写hashCode和equals,无法成功删除数据。所以对类对象(尤其是自定义的) 进行操作时必须重写这两个方法。
// 删除对象
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;
}

// 具体删除操作
private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 依然需要arraycopy
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        elementData[--size] = null; 
    }
  • iterator()
  1. 该方法实现了Iterator接口,并对几个方法都进行了自定义实现,同时保证了Java容器的快速报错机制
  2. 快速报错机制能够防止多个进(线)程同时修改同一个容器的内容。在使用迭代器Iterator或者forEach进行迭代遍历时,如果有其它进程想要进行修改,就会抛出ConcurrentModificationException异常。
  3. iterator()方法在迭代遍历时会调用checkForComodification方法判断当前ArrayList是否是同步的。
  4. 迭代遍历时不能进行add和remove等操作,但是对于Iterator迭代器可以使用remove进行操作,因为此时操作的不是ArrayList而是它的Iterator对象。
public Iterator<E> iterator() {
        return new Itr();
}

private class Itr implements Iterator<E> {
        int cursor;       // 下一个要返回元素的索引
        int lastRet = -1; // 最后一个返回元素的索引; -1 代表没有
        // 快速报错机制的标志,如果最后两者相等,则说明是同步的
        int expectedModCount = modCount;
	
	@SuppressWarnings("unchecked")
        public E next() {
            // 进行同步判定
            checkForComodification();
            ...
        }
	// 同步判定函数
	final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

}

参考文档

《jdk1.8ArrayList主要方法和扩容机制(源码解析)》