【Java】集合类 - ArrayList

Posted by 西维蜀黍 on 2019-05-15, Last Modified on 2023-02-28

ArrayList介绍

ArrayList是一种线性数据结构,它的底层是用数组实现的,相当于动态数组。与Java中的数组相比,它的容量能动态增长。类似于C语言中的动态申请内存,动态增长内存。

当创建一个数组的时候,就必须确定它的大小,系统会在内存中开辟一块连续的空间,用来保存数组,因此数组容量固定且无法动态改变。ArrayList在保留数组可以快速查找优势的基础上,还解决了自动扩容问题。

ArrayList特点

  1. 快速查找:在物理内存上采用顺序存储结构,因此可根据索引快速地查找元素。
  2. 容量动态增长: 如下图所示,当数组容量不够用时(表1),自动创建一个比原数组容量大的新数组(表2),并将数组中的元素复制到新数组(表3),再将新的元素也放入新数组(表4),最后将新数组赋给原数组引用即可。

ArrayList继承关系

ArrayList继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable这些接口,实现了所有List接口的操作,并ArrayList允许存储null值。

除了没有进行同步,ArrayList基本等同于Vector。在Vector中几乎对所有的方法都进行了同步,但ArrayList仅对writeObject和readObject进行了同步,其它比如add(Object)、remove(int)等都没有同步。

  • AbstractList提供了List接口的默认实现(个别方法为抽象方法)。
  • List接口定义了列表必须实现的方法。
  • 实现了RandomAccess接口:提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
  • 实现了Cloneable接口:可以调用Object.clone方法返回该对象的浅拷贝。
  • 实现了 java.io.Serializable 接口:可以启用其序列化功能,能通过序列化去传输。未实现此接口的类将无法使其任何状态序列化或反序列化。序列化接口没有方法或字段,仅用于标识可序列化的语义。

ArrayList的实现

对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面进行具体的介绍:

私有属性

// 当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组的容量大小,为10。
private static final int DEFAULT_CAPACITY = 10

// 当ArrayList的构造方法中显示指出ArrayList的数组长度为0时,类内部将EMPTY_ELEMENTDATA 这个空对象数组赋给elemetData数组。
private static final Object[] EMPTY_ELEMENTDATA = {};

// 当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 保存ArrayList中数据的数组
transient Object[] elementData; 

// ArrayList中实际数据的数量
private int size;

// ArrayList中的对象数组的最大数组容量为Integer.MAX_VALUE – 8。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE  8;

很容易理解,elementData存储ArrayList内的元素,size表示它包含的元素的数量。 有个关键字需要解释:transient。

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能对象内部有一些特殊的成员变量,我们不想在序列化时保存它们。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上transient关键字。

构造函数

ArrayList提供了三种方式的构造器,可以构造一个指定初始容量的空列表、构造一个默认初始容量为10的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。

// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
  	// 如果initialCapacity大于0,则创建一个大小为initialCapacity的对象数组赋给elementData。
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {	//如果initialCapacity等于0,则将EMPTY_ELEMENTDATA赋给elementData。
        this.elementData = EMPTY_ELEMENTDATA;
    } else {	//如果initialCapacity小于0,抛出异常(非法的容量)。
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

// ArrayList构造函数。默认容量是10。
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 创建一个包含collection的ArrayList
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    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 {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

添加/插入元素

ArrayList是基于数组实现的,当添加/插入元素的时候,如果数组容量足够大,则直接添加/插入该元素即可,

如果数组容量不够,以 add(E e) 为例,可以看到add(E e)中先调用了ensureCapacityInternal(size+1)方法,之后将元素赋给elementData[size],而后size自增。

比如,当第一次添加元素时,size为0,而调用add时将elementData[0]赋值为e,size设置为1。这时,将元素的索引赋给elementData[size],就会出现数组越界的情况。这里关键就在ensureCapacityInternal(size+1)中了。

在数组最后添加元素

当调用 add(E e) 方法向数组中添加元素时,默认是添加到数组中最后一个元素的后面。

// 添加元素e
public boolean add(E e) {
  	// 保证在添加元素前,数组的长度容量能满足要求,即对数组的长度容量最小要求(minCapacity)= 当前元素已占用的长度+1
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

在指定位置添加元素

当调用下面这个方法以在指定索引位置,向数组中添加元素时,会先查找索引位置,然后将元素添加到索引处,最后把添加前索引后面的元素追加到新元素的后面。

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

在数组最后添加多个元素

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

调整数组容量 ensureCapacity

从上面向ArrayList中添加/插入元素的代码中,我们可以看到,每当向数组中添加元素时,在内部都要调用ensureCapacityInternal(int minCapacity) ,以去检查,添加元素后,元素的个数是否会超出当前数组的长度,如果超出,则先将数组进行扩容,再添加元素。

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

private static int calculateCapacity(Object[] elementData, int minCapacity) {
  	// 当集合中还没有元素时,取10(默认集合容量)和minCapacity中的最大值作为当前需要的最小集合容量
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 确定ArrarList的容量
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

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

// 若当前容量不足以容纳当前的元素个数,设置 新的容量=“(原始容量x3)/2 + 1”
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);
}

从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

元素修改

set(int index, E element) 方法的作用是指定下标索引处的元素的值。在ArrayList的源码实现中,方法内首先判断传递的元素数组下标参数是否合法,然后将原来的值取出,设置为新的值,将旧值作为返回值返回。

public E set(int index, E element) {
    // 检验索引是否合法
    rangeCheck(index);
    // 旧值
    E oldValue = elementData(index);
    // 赋新值
    elementData[index] = element;
    // 返回旧值
    return oldValue;
}

元素读取

get(int index) 方法是返回指定下标处的元素的值。get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0)。如果所引致合法,则调用 elementData(int index) 方法获取值。在 elementData(int index) 方法中返回元素数组中指定下标的元素,并且对其进行了向下转型。

// 返回此列表中指定位置上的元素。
public E get(int index) {
    rangeCheck(index);
    checkForComodification();
    return ArrayList.this.elementData(offset + index);
}

元素删除

remove(int index) 方法的作用是删除指定下标的元素。在该方法的源码中,将指定下标后面一位到数组末尾的全部元素向前移动一个单位,并且把数组最后一个元素设置为null,这样方便之后将整个数组不再使用时,会被GC,可以作为小技巧。而需要移动的元素个数为:size-index-1。

// 删除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);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

ArrayList的优缺点

ArrayList的优点

  • ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快。
  • ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已。
  • 根据下标遍历元素,效率高。
  • 根据下标访问元素,效率高。
  • 可以自动扩容,默认为每次扩容为原来的1.5倍。

ArrayList的缺点

  • 插入和删除元素的效率不高。
  • 根据元素下标查找元素需要遍历整个元素数组,效率不高。
  • 线程不安全。

Reference