Android - 数据结构解析 ArrayList

158 阅读5分钟

描述

ArrayList是一个基于可变长度的数组实现的列表容器,实现了List接口。ArrayList实现了List接口中所有可选的操作,并且允许存储所有的元素类型,包含null。除了实现List的接口以外,这个类还提供了用于操作内部用于存储数据数组大小的方法。(这个类大致就相当于是Vector类,除了该类是线程不安全的,不支持多线程之间的同步访问)

下面这几个方法的时间复杂度都是常数时间:size, isEmpty, get, set, iterator, listIterator。添加元素的方法add的时间复杂度为O(n)。其他剩余的方法的时间复杂度大致上都是线性时间。和类似的数据结构LinkedList比起来,ArrayList的常量因子较低。

初始化

ArrayList总共有3个构造器,下面是对这3个构造器的解释。

  1. 无参构造器
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

可以看到,在调用无参数的构造器的时候,只是简单的把存储数据的这个内部数组指向了一个没有元素的“专门用于初始化占位”的静态数组而已,这可以尽可能保证节省内存空间的使用。

  1. 设置默认容量的构造器
private static final Object[] EMPTY_ELEMENTDATA = {};
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);
    }
}

可以看到,这里比较特殊的只有当传入的预期容量为0的时候,这里会指向一个跟无参数构造器类似的空数组,但他们并不是同一个数组。

  1. 基于传入的集合数据来创建ArrayList的构造器
public ArrayList(Collection<? extends E> c) {
    // 直接将传入的集合转化为数组对象赋值给当前新集合的数组对象
    // 这里c.toArray的底层实现为 System.arraycopy()
    elementData = c.toArray();
    // 首先将size的值更新为当前数组的最新值
    // 然后判断size是不是为0,即传入的集合有没有元素
    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 {
        // 如果传入的集合根本没有数据,那么跟#2中的构造器类似,内部的数组指向用于占位的空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

add

add有2个方法的重载。

  1. 只有1个参数的方法add(E e)
// 这个方法的作用是将传入的对象添加到集合的末端
public boolean add(E e) {
    // 需要的话对数组进行扩容,以保证新加入的对象可以被保存
    // 因此这里的参数传递的含义是扩容需要的目标最小的容量大小,为当前集合的长度+1
    // 详细分析见下一小节“扩容”
    ensureCapacityInternal(size + 1);
    // 将当前集合的size+1,并且将新元素放置在数组的末尾
    elementData[size++] = e;
    return true;
}
  1. 2个参数的方法add(int index, E element)
// 这个方法是将新元素插入到已有数组的指定索引号的位置,自动完成原有数组中前后元素的位置调整
public void add(int index, E element) {
    // 确保插入的位置是在0~size之间
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    // 扩容,如果需要的话
    ensureCapacityInternal(size + 1);
    // 把原数组中从index索引号开始到结尾的元素,放置到index+1的位置,即:从原来index位置的所有元素,向后移动1位,index的位置就空出来了
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 将新元素放到index索引的位置
    elementData[index] = element;
    // 集合长度+1
    size++;
}

扩容

当数组的容量不足以放下新对象的时候,那么就需要对现有的数组进行扩容。

private static final int DEFAULT_CAPACITY = 10;

// minCapacity的含义是,我们预期需要的最小的存储容量,传入的值为当前集合的长度+1
private void ensureCapacityInternal(int minCapacity) {
    // 判断是不是使用无参数的构造器初始化的ArrayList,因为另外2个构造器都默认指向的是EMPTY_ELEMENTDATA对象,代表调用者自己非常清楚我想要的ArrayList的初始容量就是0,不需要自动搞成10个长度的数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 这里表示通过无参数的构造器构造出来的ArrayList最小容量为10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    // 判断是否真的需要扩容,如果期望的新容量还没有当前使用中的数组大,那就没必要进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 能够给内部数组使用的最大长度值,这里减8主要是为了避免某一些虚拟机用到了数组的部分位置作为其他用处导致空间冲突。超过了这个大小,就会报OutOfMemoryError
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 走到这个方法那就代表一定会进行数组的扩容了,参数代表了预期扩容到的容量
private void grow(int minCapacity) {
    // 获取原来数组的长度
    int oldCapacity = elementData.length;
    // 计算需要扩容到的新数组的长度,可以看到,新容量=老容量*1.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 和传入的预期容量做比较
    if (newCapacity - minCapacity < 0)
        // 如果老容量变为原来的1.5倍以后仍然比预期容量小,那么以传入的预期容量为准
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 如果计算后的新容量超过了允许的最大容量,那么就会拿到可以使用的数组的最大容量
        newCapacity = hugeCapacity(minCapacity);
        
    // 将数组扩容到新的容量,底层最终会调用System.arraycopy完成新数组的创建
    elementData = Arrays.copyOf(elementData, newCapacity);
}

remove

remove方法也有2个方法的重载。

  1. 根据索引号删除对象
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);
    // 将size的值减1
    // 释放数组最后一个位置的对象,注意,这里并不是被删除的那个对象,就是原数组中保存的最后一个对象而已
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}
  1. 直接移除传入的对象
// 这个方法会通过遍历的方式先找到需要删除的元素的索引号,然后将其删除,但是在删除的时候,并不是直接调用#1的重载方法,而是新写了一个私有的fastRemove的方法,提升删除的效率
public boolean remove(Object o) {
    if (o == null) {
        // 因为ArrayList是允许存储null空值的,因此这里需要对null进行特殊处理
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                // 注意:这里只是删除了找到的第一个对应的元素
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            // 删除对象的时候是根据equals方法来判断的
            if (o.equals(elementData[index])) {
                fastRemove(index);
                // 注意:这里只是删除了找到的第一个对应的元素
                return true;
            }
    }
    return false;
}

// 这个方法就是在#1方法基础上移除了一些检查,也没有返回值,就不再重复解析了
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
}

清空对象

public void clear() {
    // 将数组中的每一个位置的对象都指向null,这样GC就会在下一次垃圾回收的时候释放这些空间
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

补充

modCount和ConcurrentModificationException

当我们在ArrayList的遍历过程中尝试删除元素的时候,可能就会遇到ConcurrentModificationException这样的异常,这个异常可以从下面迭代器中的实现得到解答。

int expectedModCount = modCount;

// 这个方法就是ArrayList的迭代器Iterator的获取下一个元素的实现
public E next() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    int i = cursor;
    if (i >= limit)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

可以看到,当实例化这个迭代器的时候,就会从ArrayList中获取到当前的modCount的值,也就是当前list中的元素被操作的次数。而在上面的源码分析中,我们能看到在增删改的过程中,这个值都会被记录。而我们在遍历不断的取值的时候,如果我们进行了删除操作,导致modCount的值已经被减1了,就会导致next方法中抛出这个异常。