简单的解剖了下ArrayList

简介

ArrayList是一个可动态调整数组大小的集合类,其类图关系如下:
在这里插入图片描述

  • List:声明是一个有序的集合,可以控制元素位置并索引访问。

  • RandomAccess:声明支持快速随机访问的标记接口,常用于列表类实现。该接口的主要目的是允许通用算法更改其行为,即必要时选择更好的算法进行性能上的提高,实现了该接口的列表使用for遍历比迭代器Iterator遍历效率高。
    RandomAccess.png

  • Serializable:启用类的可序列化特性。

  • Cloneable:声明类是可克隆的,且调用clone()方法时不会抛出CloneNotSupportedException

  • AbstractList:提供了List接口的基本实现,并尽可能的减少List接口"随机访问"数据存储支持的工作

ArrayList核心源码解析

从属性与方位两个维度进行源码解析并总结特点。

属性解析

DEFAULT_CAPACITY:默认容量10,用于构造函数初始化与容量运算。
EMPTY_ELEMENTDATA:共享的空数组,调用ArrayList有参构造函数参数容量值为0(即一般考虑不再进行容量扩展)时赋给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);
    }
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

DEFAULTCAPACITY_EMPTY_ELEMENTDATA:共享的空数组,与EMPTY_ELEMENTDATA区别在于该数组是用来容量运算的,调用ArrayList无参构造函数时会把该对象赋给elementData,添加元素时再重新计算扩容,所以一般建议使用有参构造函数赋予原始容量。

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

elementData:存储ArrayList的元素的数组缓冲区。
sizeArrayList包含的元素数量,elementData数组的元素数量。
MAX_ARRAY_SIZE:分配的最大数组大小,值为Integer.MAX - 8

方法解析

add(E)添加元素

/**
 * (1) 数组末尾添加元素 
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 容量值自增并将元素附加到数组末尾
    elementData[size++] = e;
    return true;
}

/**
 * (2) 确保内部的容量能满足所需最小容量minCapacity
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

/**
 * (3) 根据数组所需的最小容量minCapacity进行容量计算
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 若元素数组为引用的空数组,则返回默认容量(10)与minCapacity之间的最大值,不为空则返回minCapacity
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

/**
 * (4) 根据数组所需的最小容量minCapacity确保精确的容量
 */
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 判断添加元素后的元素数目是否大于数组长度,true则进行数组扩容,false则完成元素添加
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * (5) 根据数组所需的最小容量minCapacity判断是否扩容
 */
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 判断添加元素后的元素数目是否大于数组长度,true则进行扩容,false则完成元素添加
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}


/**
 * (6) 重新建一个至少可以容纳最小容量minCapacity的数组并进行数组元素拷贝,消耗大
 */
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 若所需最小容量minCapacity大于旧容量oldCapacity+oldCapacity右移1值,则新容量为minCapacity,反正新容量为旧容量运算值
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 大容量数组,一般不会调用到
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 新建一个长度为newCapacity的数组并将旧数组元素负责过来
    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. add()数组末尾添加元素
  2. ensureCapacityInternal()确保内部的容量能满足所需最小容量minCapacity
  3. calculateCapacity()根据数组所需的最小容量minCapacity进行容量计算
  4. ensureExplicitCapacity()根据数组所需的最小容量minCapacity确保精确的容量
  5. ensureExplicitCapacity()根据数组所需的最小容量minCapacity判断是否扩容,若需要则进行步骤6
  6. grow()重新建一个至少可以容纳最小容量minCapacity的数组并进行数组元素拷贝,消耗大,所以建议一般使用有参构建函数创建列表时设置好容量

由上述流程可以看出,ArrayListadd(E e)方法在容量足以确保的情况下效率是很高的,直接将新元素赋予数组元素的末尾下标+1即可,复杂度仅为O(1)。

add(int, E)增元素

public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}

该方法的主要核心在System.arraycopy()方法,该方法把elementData数组中的index位置起的size-index个元素(即index下标后的元素)复制到下标index+1,然后再把新的元素element赋到index下标位置。由于需要进行元素的位置逐个后移,所以性能耗费大,时间复杂度为O(n),n为指定位置后的元素数目。
如在非末尾位置插入元素的操作较多,选择LinkedList效果会比ArrayList更好。

addAll(Collection<? extends E>)添加元素

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

由上源码可以看到当添加集合元素时,也是需要进行数组拷贝的,不过是直接拷贝到列表数组末尾,时间复杂度由集合元素数目而定,即为O(n)。

remove(Object)删元素

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])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

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
}

虽然删除元素的主要方面命名为fastRemove(),但从其代码依然可以看出这方法并不fast,指定位置删除元素后还要进行元素前移,性能耗费与指定位置添加差别不大,时间复杂度为O(n),n为指定位置后的元素数目。
如删除元素的操作较多,选择LinkedList效果会比ArrayList更好。

set(int, E)改元素

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

替换指定下标数组元素,复杂度为O(1),效率高。

get(int)查元素

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

根据下标获取数组元素,复杂度为O(1),效率高。

总结

ArrayList有以下特点:

  • 添加元素性能因参数有所区别,但都需注意数组容量不足时ArrayList会进行扩容产生性能消耗
    • add(E)在数组末尾添加元素,复杂度O(1)
    • add(int, E)在数组指定位置添加元素,复杂度O(n),n为下标后的元素数目
    • addAll(Collection)在数组末尾添加集合元素,复杂度O(n),n为集合中的元素数目
  • 删除元素慢,remove()删除元素,后面元素需逐个移动,复杂度O(n),n为下标后的元素数目
  • 更改效率高,set(index, E)直接根据下标替换数组元素,复杂度O(1)
  • 查询效率高,get(index)直接根据下标获取数组元素,复杂度O(1)

综上,如果与指定下标元素增删操作更多的时候选择ArrayList会导致数组需要进行多次的元素移动,性能消耗十分大,该情况更适合使用LinkedList,因LinkedList增删元素时只需更改该元素上一个与下一个节点的指向即可,相当于从一个双向链表中摘除一个元素。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章