Java 基础——ArrayList 的扩容机制

参考文章:
ArrayList 源码 & 扩容机制分析

1.ArrayList 的 3 种创建方式

在讲解 ArrayList 的扩容机制之前,先来看 ArrayList 的 3 种创建方式,即对应 3 个构造函数:

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

	//...
	
	//默认初始容量大小
	private static final int DEFAULT_CAPACITY = 10;
	
	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	
	//1.创建时指定容量大小
	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);
	    }
	}
	
	//2.无参构造函数,使用初始容量 10 来构造一个空列表
	public ArrayList() {
	    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
	}
	
	//3.构造包含指定 collection 元素的列表,这些元素利用该集合的迭代器按顺序返回
	public ArrayList(Collection<? extends E> c) {
	    elementData = c.toArray();
	    if ((size = elementData.length) != 0) {
	        // c.toArray might (incorrectly) not return Object[] (see 6260652)
	        if (elementData.getClass() != Object[].class)
	            elementData = Arrays.copyOf(elementData, size, Object[].class);
	    } else {
	        // replace with empty array.
	        this.elementData = EMPTY_ELEMENTDATA;
	    }
	}

	//...
}

注意:
① 以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。
② JDK 6 new 无参构造的 ArrayList 对象时,直接创建了长度是 10 的 Object[] 数组 elementData。

2.扩容相关的源代码分析

2.1.add(E e)

先来看 add(E e) 方法

//向列表末尾添加元素,添加成功时返回 true
public boolean add(E e) {
	//调用了 ensureCapacityInternal 方法,确保数组下标不越界
	ensureCapacityInternal(size + 1);  // Increments modCount!!
	//添加元素
	elementData[size++] = e;
	return true;
}

注意 :JDK 11 移除了 ensureCapacityInternal 方法和 ensureExplicitCapacity 方法!

2.2 ensureCapacityInternal(int minCapacity)

再来看看 ensureCapacityInternal() 方法

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

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    	//返回默认的容量和传入参数的较大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

当要向列表中添加第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 变为 10。

2.3.ensureExplicitCapacity(int minCapacity)

ensureExplicitCapacity() 方法如下:

//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
    	//调用 grow 方法进行扩容
        grow(minCapacity);
}

具体分析如下:

  • 当添加第 1 个元素时,elementData.length 为 0,因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法;
  • 当添加第 2 个元素时,minCapacity = 2,此时 elementData.length 在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会执行 grow(minCapacity) 方法,既不会进行扩容。添加第 3、4、···直到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10;
  • 当添加第 11 个元素时,minCapacity = 11,此时 minCapacity - elementData.length = 11 - 10 > 0 成立,因此便进行扩容操作;

2.4.grow(int minCapacity)

grow(int minCapacity) 方法是 ArrayList 扩容时的核心方法,其代码如下:

//要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//扩容的核心代码
private void grow(int minCapacity) {
    // oldCapacity 为旧容量,newCapacity 为新容量
    int oldCapacity = elementData.length;
    //将 oldCapacity 右移一位,即相当于 oldCapacity / 2,整句运算式的结果就是将新容量更新为旧容量的 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //检查新容量 newCapacity 是否大于最小需要容量 minCapacity,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //如果 minCapacity 大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    /*
		(1) Arrays.copyOf()方法返回的数组是新的数组对象,原数组对象仍是原数组对象不变;
		(2) 该拷贝不会影响原来的数组, copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值;
	*/
    elementData = Arrays.copyOf(elementData, newCapacity);
}

在上面的代码中,下面这行代码用于计算扩容后的新容量 newCapacity:

int newCapacity = oldCapacity + (oldCapacity >> 1);

ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右:

  • 当 oldCapacity 为偶数时,就是 1.5 倍,例如 oldCapacity = 10,那么 newCapacity = 10 + 10 >> 1 = 15;
  • 当 oldCapacity 为奇数时,则是 1.5 倍左右, 例如 oldCapacity = 15,那么 newCapacity = 15 + 15 >> 1 = 22;

有关移位运算符 >> 的具体介绍,可以参考Java 基础面试题——运算符这篇文章。

2.5.hugeCapacity(int minCapacity)

hugeCapacity(int minCapacity) 方法如下,从上面 grow() 方法的源码可以知道:如果新容量 newCapacity > MAX_ARRAY_SIZE,则执行 hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE:

  • 如果 minCapacity 大于最大容量,则新容量 newCapacity 则为 Integer.MAX_VALUE;
  • 否则,新容量 newCapacity 则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

2.6.小结

下面简单总结一下 ArrayList 的扩容步骤:

  • 当向 ArrayList 中添加一个新元素时,ArrayList 会先检查当前数组中是否还有剩余的空间可以存储元素;
  • 如果有剩余空间,则将元素添加到数组 elementData 的尾部,然后更新数组 elementData 中的元素数量 size 即可。
  • 如果没有剩余空间,则底层通过 ArrayList 扩容的核心方法 grow(int minCapacity) 来进行扩容,具体步骤如下:
    • 将数组 elementData 的长度设置为旧容量 oldCapacity;
    • 然后取 oldCapacity 的大约 1.5 倍作为新容量 newCapacity,具体的计算方法是 newCapacity = oldCapacity + (oldCapacity << 1),这里通过使用移位运算符来加快计算速度;
    • 通过 Arrays.copyOf() 方法来创建一个长度为 newCapacity 的数组,并且将旧数组中的元素复制到新数组中,复制完成后,ArrayList 就会开始使用新数组来存储元素,并更新数组的大小和元素数量。

在 Java 8 之前,ArrayList 的扩容是通过 System.arraycopy() 方法来实现的,而在 Java 8 之后,则通过 Arrays.copyOf() 方法来实现的。有关这两者的区别可以参考 Java 基础——System.arraycopy() 与 Arrays.copyOf() 的联系与区别这篇文章。

3.扩容测试

class Test {
    
    //通过反射获取 list 的容量,即 elementData 数组的长度
    public static Integer getCapacity(ArrayList<Integer> list) {
        Integer length = null;
        Class clazz = list.getClass();
        Field field;
        try {
            field = clazz.getDeclaredField("elementData");
            field.setAccessible(true);
            Object[] object = (Object[]) field.get(list);
            length = object.length;
            return length;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return length;
    }
    
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        //记录 list 的容量,初始值为 0
        int preCap = 0;
        for (int i = 0; i < 100; i++) {
            list.add(i);
            int curCap = getCapacity(list);
            // curCap > preCap 说明上次添加元素过程中发生了扩容
            if (curCap > preCap) {
                System.out.println("capacity: " + curCap + ", size: " + list.size());
                preCap = getCapacity(list);
            }
        }
        System.out.println("添加 100 个元素后,capacity: " + getCapacity(list) + ", size: " + list.size());
    }
}

输出结果如下:

capacity: 10, size: 1
capacity: 15, size: 11
capacity: 22, size: 16
capacity: 33, size: 23
capacity: 49, size: 34
capacity: 73, size: 50
capacity: 109, size: 74
添加 100 个元素后,capacity: 109, size: 100

4.思考:为什么按大约 1.5 倍来扩容?

(1)扩容因子的大小选择,需要考虑如下情况:

  • 扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制;
  • 扩容容量不能太大,需要充分利用空间,避免浪费过多空间;

(2)为了能充分使用之前分配的内存空间,最好把增长因子设为 1< k < 2,当 k = 1.5 时,就能充分利用前面已经释放的空间。如果 k >= 2,新容量刚刚好永远大于过去所有废弃的数组容量。除此之外,并且充分利用移位操作(右移一位,在不溢出的情况下相当于除以 2),减少了浮点数运算,提高了效率。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码星辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值