重温JAVA集合框架-ArrayList

192 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

前言

多数情况下,ArrayList做为JAVA集合框架中最常用的类,项目中经常会使用到。那么在使用过程中你是否知道其原理呢?本文就从以下几点来回顾下ArrayList

  • ArrayList概述
  • ArrayList创建
  • ArrayList原理(源码分析)
  • 添加元素
  • 自动扩容
  • 修改元素
  • 删除元素
  • ArrayList注意事项
  • ArrayList总结

ArrayList概述

ArrayList的类变量和实例变量


private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

transient Object[] elementData; 

private int size;

protected transient int modCount = 0;
  • DEFAULT_CAPACITY:默认初始容量。
  • EMPTY_ELEMENTDATA:用于空实例的共享空数组实例。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:用于默认大小的空实例的共享空数组实例。我们将它与EMPTY_ELEMENTDATA区别开来,以了解添加第一个元素时要膨胀多少。
  • MAX_ARRAY_SIZE:要分配的数组的最大大小。有些虚拟机在数组中保留一些头字。尝试分配更大的数组可能会导致OutOfMemoryError:请求的数组大小超过虚拟机限制
  • elementData:数组缓冲区,数组列表的元素被存储在其中。数组列表的容量就是这个数组缓冲区的长度。当添加第一个元素时,任何带有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组列表将被扩展为DEFAULT_CAPACITY
  • size:数组列表的大小(包含元素的数量)。
  • modCount:此列表在结构上被修改的次数。大家都知道ArrayList遍历过程中,是不能修改集合的,这会导致遍历过程报错。就是通过这个值实现的,如果该值发生变化,那么遍历过程就会抛出ConcurrentModificationException异常。

ArrayList的继承实现关系

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
}
  1. AbstractList:实现了List接口的一些方法。
  2. List:有序集合(也称为序列),提供了一些有序集合的方法。
  3. RandomAccess:表示它们支持快速(通常是常数时间)随机访问。该接口的主要目的是允许泛型算法在应用于随机或顺序访问列表时改变其行为以提供良好的性能。
  4. Cloneable:用于向Object.clone()方法指示该方法对该类实例进行字段对字段复制是合法的。
  5. Serializable:集合可以被序列化。

ArrayList的特点

  • List接口的可调整数组实现(动态扩容的数组结构)。
  • 实现所有可选的列表操作,并允许所有元素,包括null
  • 除了实现List接口之外,该类还提供了一些方法来操作内部用于存储列表的数组的大小。
  • 允许存放重复数据。
  • 每个ArrayList实例都有一个容量。容量是用于存储列表中元素的数组的大小。它总是至少和列表大小一样大。当元素被添加到数组列表中时,它的容量会自动增长。
  • ArrayList不是一个线程安全的集合。

ArrayList创建

ArrayList提供三种构造方法,源码如下。

/**
 * 构造具有指定初始容量的空列表。
 *
 * @param  initialCapacity  列表的初始容量
 * @throws IllegalArgumentException 如果指定的初始容量为负数
 */
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);
    }
}

/**
 * 构造一个初始容量为10的空列表。
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * 按照指定集合的迭代器返回元素的顺序,构造一个包含指定集合元素的列表。
 *
 * @param c 将其元素放入此列表中的集合
 * @throws NullPointerException 如果指定的集合为空
 */
public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // 替换为空数组。
        elementData = EMPTY_ELEMENTDATA;
    }
}
  • ArrayList():构造一个初始容量为10的空列表。即Object[]数组(也就是elementData实例变量)等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • ArrayList(int initialCapacity):构造具有指定初始容量的空列表。即Object[]数组(也就是elementData实例变量)等于传入的int值,也就是等于new Object[initialCapacity]
  • ArrayList(Collection<? extends E> c):构造一个包含指定集合元素的列表。如果传入的集合长度为0,则构建一个空数组,即elementData = EMPTY_ELEMENTDATA

ArrayList原理(源码分析)

本文是基于JAVA8的ArrayList来分析。

添加元素

我们知道ArrayList的元素是存储在elementData这个实例变量里(是一个Object[])。

add(E)方法就是将指定元素追加到此变量的末尾。

来看一下add(E)方法的源码。

add(E)方法会改变列表大小,所以会修改上文提到的实例变量modCount

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

来看一下这个ensureCapacityInternalensureCapacityInternal(size + 1)源码如下。

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

calculateCapacity(elementData, minCapacity) 实际就是返回elementData的长度和minCapacity中值较大的一个,这里不做过多的说明了。我们看一下ensureExplicitCapacity这个方法。ensureExplicitCapacity方法源码如下。

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

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

首先修改了modCount(被修改的次数)的值。 然后如果说添加元素后的大小minCapacity大于elementData数组的长度的话,则执行grow(minCapacity),我们来看一下grow(minCapacity)

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        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) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

grow方法:增加容量(扩容),以确保它至少可以容纳由最小容量参数指定的元素数量

这里先说明一下这些变量的含义,不然容易乱。

  • oldCapacity:原elementData长度。
  • newCapacity:扩容后的数组长度。
  • minCapacityadd()方法添加元素后的数组长度。
  • MAX_ARRAY_SIZE:要分配的数组的最大大小。

通过移位运算(oldCapacity >> 1)(可简单理解为oldCapacity除以2),获得新数组的大小newCapacity(即原数组长度的1.5倍)。然后经过判断newCapacity等于newCapacityoldCapacity中值大的那一个。这时如果newCapacity大于MAX_ARRAY_SIZE,则newCapacity等于 (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE

最后通过elementData = Arrays.copyOf(elementData, newCapacity) 完成最后的扩容

Arrays.copyOf主要实现如下,是一个native的方法,大概就是将一个数组复制到具有指定的长度的新数组里。

/**
 *src – 原数组
 *srcPos – 原数组中的起始位置
 *dest – 目标数组
 *destPos – 目标数组中的起始位置
 *length – 要复制的数组元素的个数
 */
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

这里以add()这个方法为引,介绍的比较多,在这里小结一下。

添加元素小结

  • add()方法是将数据添加到数组缓冲区(也就是elementData这个实例变量,是一个Object[])。
  • elementData是有大小限制的。
  • add()方法添加元素时,如果elementData大小不够,则通过移位运算扩容为原先的1.5倍大小,当然用不用这个值还有一些判断
  • 扩容后,通过Arrays.copyOf实现最终的elementData扩容。

修改元素

public E set(int index, E element) {
    rangeCheck(index);

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

修改元素就没有那么复杂了,就是先判断一下下标index是否大于elementData的长度(rangeCheck(index)),大于则抛出IndexOutOfBoundsException异常。否则将指定下标的元素设置为要修改的元素,并返回修改之前的元素。

删除元素

删除元素可以通过下标删除remove(int index)),可以通过元素删除remove(Object o)),其实通过元素删除就是比通过下标删除多了一步找下标的过程

这里主要讲解下通过元素删除。

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;
}

这里主要看一下fastRemove(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
}

remove方法跟add方法最终都是通过System.arraycopy实现的。

这里有一点要注意一下,ArrayList是允许重复值的,如果删除的值是重复值,remove方法也只能删除一个。

ArrayList注意事项

  • ArrayList遍历过程中,不要修改集合,因为修改集合会导致modCount变量发生变化,遍历过程则会抛出异常。
  • ArrayList是线程不安全的集合。
  • ArrayList有最大元素个数限制。

ArrayList总结

  • ArrayList底层通过Object[]实现,通过扩容实现数组长度可调整。
  • ArrayList是通过移位运算算出扩容后的长度,扩容为原长度的1.5倍
  • ArrayListaddremove核心实现为System.arraycopy方法。