ArrayList是如何完成扩容的

先认识一下ArrayList的数据结构吧,要分析ArrayList的扩容机制,我们需要提前知道的属性有:

ArrayList的成员变量/常量

// 默认的初始化容量
private static final int DEFAULT_CAPACITY = 10; 
// new出ArrayList 对象时,指定了初始容量为0
private static final Object[] EMPTY_ELEMENTDATA = {};
// 直接使用无参构造函数时的数据
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 直接使用无参构造函数时将该对象的
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 真正存放数据的数组对象
transient Object[] elementData;
// 集合的大小
private int size;

ArrayList的构造方法

/**
 * 用户自己指定初始容量参数的构造函数。
 */
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);
   }
}
/**
 * 默认构造函数,使用初始容量10构造一个空集合
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
  *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
  *如果指定的集合为null,throws NullPointerException。 
  */
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;
    }
}

可能有的朋友已经注意到,在三个构造函数中,会为elementData指定一个空数组,在无参构造函数中这个空数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而在有参构造函数中这个空数组却是EMPTY_ELEMENTDATA,那么它两有什么区别呢?我会在文章最后谈谈我的理解,现在我们还是回头看ArrayList是如何完成一次扩容的。

ArrayList扩容分析

我们知道,如果ArrayList要进行扩容,肯定要满足一个条件,那就是ArrayList的容量 “ 不够用 ” 了,所以我们自然想到了ArrayList中常见的添加元素的四种方法:
1. add(E e)
2. add(int index, E e)
3. addAll(Collection<? extends E> c)
4. addAll(int index, Collection<? extends E> c)
对于ArrayList的扩容,其核心思想就是把原数组用一个更大容量的新数组来代替,然后将原数组的数据拷贝到新的数组中来,所以这里我就以add方法举例

/**
 * 将指定的元素添加到list的末尾
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

显然方法体中的第二行代码是将元素添加的元素加在list末尾,那么扩容应该就是在ensureCapacityInternal方法中了

/**
 * minCapacity:最小容量
 */
private void ensureCapacityInternal(int minCapacity) {
	// 如果是通过无参构造函数创建的ArrayList对象
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    	// 获取默认的容量和传入参数的较大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // modCount记录ArrayList add 和remove 的次数

    // overflow-conscious code
    //如果最小容量大于了当前数组的最大长度,显然需要扩容了
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

我们来详细分析一下上面的两个方法,在前面我们已经说到过,ArrayList有三种构造方法,我们可以把他们分为有参和无参两类,在使用无参构造方法时,elementData 会被赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA;当使用有参构造方法时,我们先假设初始容量也为0,那么elementData会被初始化为EMPTY_ELEMENTDATA,在执行add方法的时候,add方法调用ensureCapacityInternal方法,ensureCapacityInternal方法会判断elementData是不是等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,如果等于则让最小容量等于默认的容量DEFAULT_CAPACITY和传入参数两者中的较大值。这里我们可以思考,在第一次,第二次一直到第10次,最小容量都会取为DEFAULT_CAPACITY,在判断之后会调用ensureExplicitCapacity,在ensureExplicitCapacity方法中,grow就是最后扩容的方法了。但是在扩容之前,会判断一下现在最小容量是不是超过了elementData数组的长度,只有超过时,才会去执行扩容操作。下面我们接着看看扩容的方法

private void grow(int minCapacity) {
    // overflow-conscious code
    // 扩容前容量
    int oldCapacity = elementData.length;
    // 扩容后容量,这里使用了位运算,每次尝试扩容约1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果发现newCapacity仍然小于最小容量,则直接使用最小容量作为扩容后的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    /**
     * 如果newCapacity 很大,甚至已经大于了大于了MAX_ARRAY_SIZE,则试着用最小容量去判断一下。
     * 在hugeCapacity方法中,如果最小容量minCapacity 也大于MAX_ARRAY_SIZE ,则一步到位,使用Integer的最大值作为扩容后的容量
     * 如果最小容量minCapacity小于MAX_ARRAY_SIZE 则使用MAX_ARRAY_SIZE作为扩容后的容量,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:
    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;
}

由以上我们可以得出一个结论:

  1. 如果使用无参构造方法创建了一个ArrayList对象,那么在第一次执行add操作的时候,ArrayList会进行扩容,并且容量直接扩容为10。而使用有参构造函数(初始容量指定为0)进行扩容时,第一次add操作会直接触发扩容操作,容量会指定为1。
  2. ArrayList的每一次扩容都会执行位运算计算新容量,位运算的结果是新容量约为旧容量的1.5倍,然后在新容量、最小容量、MAX_ARRAY_SIZE 与
    Integer.MAX_VALUE之间取一个合适的值作为扩容后的容量
  3. 容量大于Integer.MAX_VALUE会抛出OutOfMemoryError

以上差不多就是ArrayList的整个扩容过程了,其实在分析其扩容的过程中,不免产生下面的疑问,为什么要定义两个空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA?
要回答这个问题还得看看JDK1.7中ArrayList的构造方法

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

public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

对比JDK1.8的代码可以发现,在JDK1.8中完全用DEFAULTCAPACITY_EMPTY_ELEMENTDATA取代了EMPTY_ELEMENTDATA,那么EMPTY_ELEMENTDATA就没用了吗?其实也不是在JDK1.7中,如果指定初始容量是0的话,每次都会会new 一个Object数组,如果内存中存在大量的这样的数组显然对性能有一些损耗,所以从性能的角度来看,就直接将EMPTY_ELEMENTDATA的引用赋值给ArrayList空示例。其实进一步想,一个空数组完全能够实现上面的功能,并且在JDK1.8的源码中还做了很多elementData 是不是等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA的判断,源码的作者为什么要这么做我目前也没有想明白,希望了解这一点的读者能在评论区留言赐教一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值