ArrayList

71 阅读9分钟

1、关键字段

private static final int DEFAULT_CAPACITY = 10;  默认初始容量
private static final Object[] EMPTY_ELEMENTDATA = {};  共享空数组,下面简称EE
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};  默认容量的空数组,下面简称DEE
transient  Object[] elementData;  实际存储数据的数组
private int size;  所存储的元素个数
protected transient int modCount = 0;  对集合的操作次数

2、初始状态

有三个构造方法:

  1. 无参数
public ArrayList() {
    没有指定容量即表示使用默认容量,则指向 DEE
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
  1. 参数为指定容量
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {  指定容量>0时,new一个数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {  指定容量=0时,指向 EE。
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
  1. 参数为另一个集合
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 {
        若传入的集合长度为0,指向 EE
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

3、添加元素

  1. add(e):在现有元素末尾添加元素
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  先确保容量,如果容量不够就扩容。
    elementData[size++] = e;  确保完容量后添加新元素
    return true;
}
  1. add(intdex,e):在指定下标添加元素
public void add(int index, E element) {
    if (index > size || index < 0)  若下标不合法抛出数组越界异常
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    ensureCapacityInternal(size + 1);  同样先确保容量
    在非末尾插入元素的实现方式是将指定下标至末尾这一段数据往后错一位,然后将新元素加到错出来的空位中。往后错一位是通过copy指定下标至末尾这一段数据实现的
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
  1. addAll(collection):将指定集合中的数据添加至现有元素末尾
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();  将传入的集合转为数组
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  确保容量
    System.arraycopy(a, 0, elementData, size, numNew);  copy传入的数据至现有元素的末尾
    size += numNew;
    return numNew != 0;
}
  1. addAll(index,collection):将指定集合中的数据添加至指定下标
public boolean addAll(int index, Collection<? extends E> c) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    Object[] a = c.toArray();  将传入的集合转为数组
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  确保容量
    与在指定下标添加单个元素的实现方式相同,只不过是将指定下标至末尾这一段数据往后错传入集合长度位
    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);  将原数据往后错完位后,把传入的数据copy到空出来的位置
    size += numNew;
    return numNew != 0;
}

4、扩容

  1. 确保容量
供外部业务端主动确保指定容量的方法
public void ensureCapacity(int minCapacity) {
        先得到ArrayList可能的最小容量,依据就是该ArrayList实例是使用哪个构造方法创建的
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            ? 0  如果elementData不是DEE,说明不是使用无参构造方法创建的,则为0
            : DEFAULT_CAPACITY;  否则是使用无参构造创建的,为默认容量
       
        如果指定容量比可能的最小容量大,就调用ensureExplicitCapacity,否则什么也不需要做
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

ArrayList内部使用的,在各个添加元素的方法里会调用该方法
private void ensureCapacityInternal(int minCapacity) {

    //如果elementData是DEE,说明是通过无参构造方法实例化后第一次添加元素,那么就直接将目标容量定为默认容量10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    判断目标容量是否需要扩容
    ensureExplicitCapacity(minCapacity);
}

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

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)  如果目标容量比elementData长度大,就需要扩容
        grow(minCapacity);
}
  1. 实际扩容操作
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;  现有容量
    int newCapacity = oldCapacity + (oldCapacity >> 1);  计算新容量。首先将新容量设为现有容量的约1.5倍
    if (newCapacity - minCapacity < 0)  如果新容量比传入的目标容量小,则再将新容量设为目标容量
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)  如果新容量比MAX_ARRAY_SIZE还大,就根据传入的目标容量确定最终扩容容量
        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) 目标容量<0抛出内存溢出
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?  ArrayList的容量最大为Integer.MAX_VALUE
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

5、移除元素

  1. remove(index):移除指定下标的元素
public E remove(int index) {
    if (index >= size)  若下标不合法抛出数组越界异常(这里为什么不判断<0?)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    modCount++;
    E oldValue = (E) elementData[index];
    移除元素的实现方式同在非末尾插入元素,都是copy下标至末尾这一段数据
    int numMoved = size - index - 1;  计算需要copy的数据个数
    if (numMoved > 0)  如果个数>0,就copy这段数据,放到往前一位的位置,覆盖掉该位置的原数据
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null;  移动完数据后把最后一位无用数据置空

    return oldValue;  返回被移除(即被覆盖掉)的数据
}
  1. remove(o):移除与指定对象相同(相同指的是内容相同,即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])) {  for循环找到要移除元素的下标
                fastRemove(index);  实际执行移除操作的方法,执行后就return,所以只会移除第一个符合的元素
                return true;
            }
    }
    return false;
}

//简化版的remove(index)方法,少了下标异常判断和返回被移除的元素
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
}

6、总结点

1. 为什么要有EE和DEE两个空数组

首先说结论,这两个空数组都是为了节省内存。他们两个都是静态的,所以不管有多少ArrayList实例,使用的都仅仅是这两个对象。

EE的作用是提供一个全局共享的空数组对象,所有ArrayList实例里的elementData需要置为空数组时都可以指向它,避免了每个实例里都要new出空数组,这就节约了内存。

DEE的作用其实就是一个标志,表示该ArrayList实例是用无参构造方法创建的。可以把它想象成一个boolean类型的变量就会容易理解很多。使用 new ArrayList() 和 new ArrayList(0)这两个构造方法创建ArrayList对象,虽然从表面结果上来看是一样的,得到的都是一个空集合,他们内部的elementData都是一个长度为0的空数组,但是代表的实际意义是不同的,前者代表的是我不关心你的初始值,你就用你的默认初始值就好;而后者代表我明确指定了初始容量是0,你不要用你的默认初始值。这两种意义在后面扩容的时候会体现出来。创建完数组后首次添加数据时,由于初始是空数组,所以要触发扩容。常规的扩容计算公式是newCapacity = oldCapacity + (oldCapacity >> 1),即新容量是旧容量加上其自身约一半的容量。但是当使用无参构造方法创建实例后首次添加数据触发扩容时,不是遵循常规扩容公式,而是直接扩容至默认初始容量(10)。这里出现了两种扩容方式,一种是直接扩容至默认初始容量(只有使用无参构造方法创建实例后首次添加数据时触发的扩容会使用这种方式,其他扩容均使用常规计算公式),另一种就是常规扩容方式。那如何区分一次扩容操作是不是由使用无参构造方法创建实例后首次添加数据触发的呢?这就是DEE的作用。当elementData == DEE时就表示本次扩容操作是由使用无参构造方法创建实例后首次添加数据触发的。所以DEE仅仅是一个充当标志的作用,它也可以是boolean类型的,如果是boolean类型的那就是当DEE == true时就表示本次扩容操作是由使用无参构造方法创建实例后首次添加数据触发的。那为什么不用boolean类型呢?因为如果是boolean类型的话,就不能声明成static了,因为这个标志位在每个ArrayList实例里都不一样,不能所有实例共用一个。但是做成数组类型的就可以声明成static了,因为数组类型可以赋值给elementData,这里是一个非常巧妙的设计,用一个static的数组对象,来代替了N多个实例中的boolean类型数据,这在有大量ArrayList实例时就节省了内存。当然如果只有一个ArrayList实例的话,那这个设计就反而浪费内存了,因为一个数组对象占用的内存显然比一个boolean数据多。但是程序里不可能只有一个ArrayList实例的对吧。

2. 什么场景下不适合使用ArrayList

在需要频繁扩容、频繁在集合的中部甚至头部添加和删除数据,尤其是在中部或头部添加另一个集合数据的场景不适合。因为在非尾部添加删除数据、扩容 都涉及到数组复制,而数组复制是一个比较重的操作,会影响性能。在中部或头部插入另一个集合数据甚至会进行两次数组复制操作。

3. 执行如下代码会有什么结果

ArrayList<Integer> al = new ArrayList<>(10);
int a = al.size();
int b = al.get(0);

结果是a = 0,执行到第三行抛出数组越界异常。ArrayList中的size表示的是存储的元素的个数,而非elementData的长度。上述代码的情况是elementData是一个长度为10的数组,但是数组里没有存储元素,所以size是0。只有往集合里添加元素,size才会增加。而数组越界异常判断的是size的值,并不是elementData的长度,所以会抛出数组越界异常。