飞道的博客

[JDK源码分析] ArrayList底层实现原理

433人阅读  评论(0)

ArrayList是List接口的动态数组实现,允许存放所有元素,包括null。除了实现List接口外,ArrayList类还提供了操作数组大小的方法。功能与Vector类似,但是不同步。

类定义

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList类继承1个抽象类,实现4个实现。

  • Serializable接口:标记接口,表示可序列化。
  • RandomAccess接口:标记接口,表示列表支持快速随机访问,LinkedList则不支持随机访问,没有该接口。
  • Cloneable接口:标记接口,表示可复制。
  • List接口:作为列表规范,规定需要实现的功能,LinkedList同样实现List接口。
  • AbstractList抽象类:提供List接口骨架实现,尽可能较少实现随机访问数据所需的操作。对于连续访问数据(如链表),优先使用AbstractSequentialList,内部主要是迭代器实现。

【扩展】对于随机存储数据和连续访问数据,可以参考Collections.binarySearch()方法的源码。

// Collections
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD) 
        // for循环遍历实现
        return Collections.indexedBinarySearch(list, key);
    else
        // 迭代器实现
        return Collections.iteratorBinarySearch(list, key);
}

性能比较:ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快!!

类注释解读

ArrayList是List接口的动态数组实现,允许存放所有元素,包括null。除了实现List接口外,ArrayList类还提供了操作数组大小的方法。功能与Vector类似,但是不同步。

时间复杂度:get、set以及迭代器操作时间复杂度是O(1)。add操作以均摊常数时间运行,也就是说,添加n个元素需要O(n)时间。其他操作时间复杂度为O(n),以线性时间运行。与 LinkedList 实现相比,该实现的常数因子更低。

扩容:ArrayList实例的容量capacity是指存储元素的数组大小,总是大于等于列表大小。随着不断添加元素,容量会自动扩展。在添加大量元素之前,ArrayList采用ensureCapcity方法来增加实例容量,有效的减少递增式再分配的数量。

线程安全性:该实现不是同步的。多个线程同时访问一个ArrayList实例,并且存在至少一个线程对列表做结构性修改时,必须进行同步。针对于ArrayList,结构性修改是指任何添加或删除一个或者多个元素,或者显式调整底层数组大小的操作;修改元素值不是结构性修改。一般通过对自然封装该列表的对象进行同步操作来完成,如果不存在这样的对象,应该采用Collections.synchronizedList将当前列表封装起来(最好是在创建时完成,以防意外对列表的不同步访问)。

ArrayList的迭代器操作具有快速失败(Fast-Fail)机制,在创建迭代器后,除非是自身remove或add操作,任何其他对列表的结构性修改,迭代器都会抛出ConcurrentModificationException。

属性与常量

ArrayList架构比较简单,底层就是一个Object数组–elementData。

  • DEFAULT_CAPACITY 表示数组初始容量,默认为10。
  • size表示列表元素个数,没有volitile修饰,线程不安全。
  • modCount是AbstractList抽象类中的字段,用于记录当前数组结构性修改次数。
    【注】elementData是transient修饰的,不被序列化。

其他常量

// 可分配的最大数组大小,由于有些虚拟机会保留位置,分配更大数组会导致OutOfMemoryError
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

// 共享的空数组实例,用于空实例。
private static final Object[] EMPTY_ELEMENTDATA = {};
// 共享的空数组实例,用于默认大小的空实例。
// 与EMPTY_ELEMENTDATA做区分,以便了解在添加第一个元素时需要扩展多少数组容量。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

类初始化

无参构造方法 – 默认容量

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

初始化为共享空数组DEFAULTCAPACITY_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,创建指定大小的Object数组;等于0,初始化共享空数组EMPTY_ELEMENTDATA;其他抛出参数不合法异常。
指定集合作为参数的构造方法

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 如果c.toArray返回的不是Object[]类型,转为Object
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 集合元素个数为0,使用空数组代替
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

需要特别注意的是,ArrayList无参构造初始化时,默认创建空数组,在首次添加元素时才会初始为10。

新增与扩容实现

ArrayList类add(E e)方法,将指定元素添加到列表末尾,时间复杂度为O(1),但考虑到动态数组扩容的性质,添加操作为均摊O(1)。

public boolean add(E e) {
    // 判断底层数组容量是否足够,不够则进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 直接赋值--线程不安全
    elementData[size++] = e;
    return true;
}

在源码中,添加元素操作分为了两步:扩容和赋值。

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果底层数组仍为默认空数组,设定为初始值10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    // 列表结构性修改次数加1
    modCount++;

    // 如果期望最小容量大于当前数组容量,则扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 扩容操作
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 扩容后容量是当前数组容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 如果扩容后容量仍然小于期望最小容量,设为期望容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    // 如果扩容后容量大于JVM所能分配数组容量的最大值Integer.MAX_VALUE - 8,则使用Integer的最大值Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        
    // 创建新数组,复制原数组所有元素到新数组后,最后指针指向新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

在新增过程中,

  • 没有对值做校验,因此ArrayList是允许空值的。
  • 数组操作,需要特别注意边界值,例如数组下界不能小于0,上界不能大于Integer.MAX_VALUE。
  • 扩容结束的赋值语句elementData[size++] = e;采用的是直接赋值,没有锁,此处线程不安全。
  • 数组拷贝操作,内部调用System#arraycopy方法,是一个native方法。
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

源码问题:计算最小期望容量方法中,当elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,应该直接返回默认值,而不需要Math.max判断。因为只有ArrayList无参构造初始化时,elementData才是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果底层数组仍为默认空数组,设定为初始值10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

删除操作

ArrayList删除元素,可以通过底层数组索引删除,通过值来删除以及其他批量删除。

通过底层数组索引删除

public E remove(int index) {
    // 边界校验
    rangeCheck(index);
    // 记录结构性修改
    modCount++;
    // 暂存原该索引位置元素
    E oldValue = elementData(index);

    // 计算要移动的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 置空末尾元素,便于GC回收内存
    elementData[--size] = null; 

    return oldValue;
}

通过元素值来删除

public boolean remove(Object o) {
    // 值为null
    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;
}

根据值删除,存在值为空和非空两种情况,在遍历时分别通过==和equals方法来匹配。需要特别注意的是,删除的是匹配到的第一个元素,而不是所有匹配元素。

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方法与remove(int index)方法代码几乎一致。同时我们也发现了System.arraycopy的使用,这一操作在涉及到数组操作中有着广泛应用,例如String。

其他–查询和更新元素

获取索引位置元素

public E get(int index) {
    // 边界校验,index大于等于size,抛出IndexOutOfBoundsException
    rangeCheck(index);
    return elementData(index);
}
E elementData(int index) {
    return (E) elementData[index];
}

elementData数组中元素是Object类型,获取指定类型需要强转。

更新指定索引位置元素

public E set(int index, E element) {
    // 边界校验
    rangeCheck(index); 

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

ArrayList迭代器

ArrayList中提供了多种迭代器实现,例如Itr迭代器、ListIterator迭代器。Itr迭代器是AbstractList.Itr的优化版本,而ListIterator迭代器继承Itr,并对其提供了一些扩展,例如add和set操作。

Itr迭代器

private class Itr implements Iterator<E> {
    // 下一个元素的索引位置
    int cursor;       
    // 最新被返回的元素索引,如果没有更多元素,设为-1
    int lastRet = -1; 
    // 期望结构修改次数
    int expectedModCount = modCount;

    Itr() {}
    ...
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

迭代器变量中,expectedModCount是校验数组结构修改的关键变量。而lastRet变量用于标识是否还有元素可以删除,迭代开始值为-1,无元素;remove操作后,lastRet值也置为-1,表示不能连续删除。

Itr迭代器实现Iterator接口,主要包含了3个方法。

hasNext操作

// 是否还有未迭代的元素
public boolean hasNext() {
    // 如果下一个元素位置是size,表示没有元素可以迭代
    return cursor != size;
}

next操作

// 返回下一个元素值
public E next() {
    // 校验迭代过程中,数组是否有结构性修改
    checkForComodification();
    // 当前元素索引位置
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    // 迭代索引更新
    cursor = i + 1;
    // 返回元素值,并设置lastRet
    return (E) elementData[lastRet = i];
}

remove操作

public void remove() {
    // 如果数组中没有元素
    if (lastRet < 0)
        throw new IllegalStateException();
    // 校验迭代过程中,数组是否有结构性修改
    checkForComodification();

    try {
        // 调外部类remove,删除当前迭代元素
        ArrayList.this.remove(lastRet);
        // 因为是删除操作,迭代索引位置不变
        cursor = lastRet;
        // -1表示元素已经被删除
        lastRet = -1;
        // 修改期望modCount值
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

我们可以关注到:

  • 迭代器的删除操作调用了外部类ArrayList的remove(int index)方法,方法必然会导致数组结构性修改(modCount++)。迭代器的remove操作会对expectedModCount进行重新赋值,这也就是自身迭代器add和remove操作不会抛ConcurrentModificationException的原因。
  • remove操作中,将lastRet置为1,避免重复删除。
  • remove操作通常与next方法组合使用,不单独使用。

Fail-fast机制

迭代器有expectedModCount属性用于保存迭代器被调用时的modCount值。如果在迭代过程中,迭代器的增add、删remove、改set、查next/previous 4种操作方法会对modCount进行校验是否被修改。如果被修改,抛出ConcurrentModificationException。
所谓的Fail-Fast机制核心就是下面两句

int expectedModCount = modCount;
条件表达式判断 
modCount == expectedModCount

modCount属性继承自AbstractList,标识当前List结构修改的次数。这是一种fail-fast行为,用于防止并发导致不一致的结果。

protected transient int modCount = 0;

【注】注意的是modCount属性并不是volatile的。查阅ConcurrentModificationException源码注释,我们可以发现该异常并不总是表示对象被不同线程并发修改。同一个线程也可能触发该异常。例如,假如集合具有fail-fast迭代器,如果在进行迭代操作时修改数据,迭代器会抛出该异常。

* Note that this exception does not always indicate that an object has
* been concurrently modified by a <i>different</i> thread.  If a single
* thread issues a sequence of method invocations that violates the
* contract of an object, the object may throw this exception.  For
* example, if a thread modifies a collection directly while it is
* iterating over the collection with a fail-fast iterator, the iterator
* will throw this exception.



转载:https://blog.csdn.net/LIZHONGPING00/article/details/103790652
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场