ArrayList原理剖析

提问

  1. 基于什么实现的?数组?链表?队列?
  2. 为什么能一直add元素?

分析

实现方式

定义的变量:

维护了一个数组:

transient Object[] elementData; // non-private to simplify nested class access

private int size;

ArrayList内部所有的add、remove、set、get都是对elementData这个数组进行操作,所以ArrayList是基于数组实现的没错了。

两个长度:size ====>当前列表的长度 elementData.length ====>数组长度
数组的长度 ≥ List的长度

默认长度和两个默认数组:

/**
 * 默认的数组长度,当我们直接创建一个ArrayList对象时,容量为10
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 直接创建无参ArrayList时,内部指向该数组
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 创建带初始长度的ArrayList,或者传入另一个列表为参数创建对象时,如果长度为0或者传入列表长度为0,内部指向该数组
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

构造方法

ArrayList定义了三个构造方法,分别为无参构造,传入一个int类型参数构造方法,传入一个列表作为参数构造方法。

  1. 传入初始长度,这个方法一般用在我们知道列表长度的情况下,避免申请过多无用内存空间
public ArrayList(int initialCapacity) {
    //如果长度大于0,则创建该长度的数组
    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. 无参构造,将elementData指向默认数组,长度为0
  2. 传入一个列表。通过Arrays.copyOf()方法将传入的列表copy到新的数组,然后elementData指向该地址

操作方法

各个方法的内部实现,都是对数组的操作

get

直接取出数组对应下标的值

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    return (E) elementData[index];
}

set

同样的对数组操作,将数组中该下标对应的值替换为新的值

public E set(int index, E element) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    E oldValue = (E) elementData[index];
    elementData[index] = element;
    return oldValue;
}

add

下标为当前size+1,将需要add的值设置为数组中该下标对应的值。因为数组长度是固定的,所以其中涉及到最核心重要的扩容增长策略,ensureCapacityInternal(size + 1)方法

public boolean add(E e) {
    //扩容算法,传入当前长度+1
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //扩容完成后赋值
    elementData[size++] = e;
    return true;
}

addAll

方法,首先将列表参数转为数组,然后使用System.arraycopy方法将该数组拷贝到elementData中,其中同样涉及到扩容增长策略,只是传入的参数不一样

public boolean addAll(Collection<? extends E> c) {
    //先转为数组
    Object[] a = c.toArray();
    int numNew = a.length;
    //扩容算法,传入当前长度+需要add的元素的数量
    ensureCapacityInternal(size + numNew);  // Increments modCount
    //拷贝数组
    System.arraycopy(a, 0, elementData, size, numNew);
    //列表长度修改
    size += numNew;
    return numNew != 0;
}

remove

同样是对数组进行了拷贝操作,类似于将数组中需要移除的元素后每一个元素向前移动一位,覆盖掉原来的值。

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    modCount++;
    E oldValue = (E) elementData[index];

    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

    return oldValue;
}

扩容

前面addaddAll方法中,会有 ensureCapacityInternal1方法进行扩容算法
先看扩容相关代码:

//①传入列表长度值
private void ensureCapacityInternal(int minCapacity) {
    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)
        grow(minCapacity);
}

//③对数组进行扩容
private void grow(int minCapacity) {
    // overflow-conscious code
    //获取当前长度
    int oldCapacity = elementData.length;
    //计算新长度为旧长度 * 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果新长度小于需要的长度,使用传入的长度,适用于第一次创建后添加元素的扩容和addAll方法元素很多超过原有1.5倍的情况下
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //如果计算后长度大于最大列表长度
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //通过copyOf方法将原有数据拷贝到一个新的数组对象中,再赋值给elementData,至此,扩容完成。
    elementData = Arrays.copyOf(elementData, newCapacity);
}

分析:

  1. 首先我们传入了一个值,代表我们列表在add或者addAll之后的长度
  2. 不能每次add就扩容一次数组,否则花销太大,所以对数组的扩容不会每次长度只增加一。
  3. 如果当前为空数组,取默认值和传入值中的最大值。为了使第一次直接创建长度为10的数组,否则增加一个元素扩容一次,花销大
  4. 计算是否需要进行扩容
  5. 扩容操作,正常情况下,数组容量扩充1.5倍,两种特殊情况:
    1. 第一次add元素,直接扩容为默认长度10
    2. addAll元素很多,加起来长度超过原有1.5倍,直接扩容到该长度
  6. 通过数组拷贝方法将原有所有数据拷贝到扩容后的新数组中,最后将该数组赋值给elementData
  7. 扩容完成

总结

  1. ArrayList是基于数组实现的,数组默认长度是10,所有操作都是对所维护的数组的操作
  2. add、addAll方法都有可能触发数组扩容
  3. 长度不够时,扩容长度一般为现有长度的1.5倍
  4. 扩容、删除等都是通过拷贝数组方式实现的,所以列表不要太长,否则每次的拷贝对性能消耗太大!

猜你喜欢

转载自blog.csdn.net/lizebin_bin/article/details/88876878