ArrayList

ArrayList

概述

ArrayList 我们几乎每天都会使用到,在接口中返回一个列表,通常使用 ArrayList 装载对象返回。可以说是开发中最为常用的数据结构工具。ArrayList 允许 put null 值,会自动扩容,可以简化很多工作,ArrayList 为非线程安全,适合作为函数局部变量使用。从整体上看 ArrayList 比较简单,底层使用数组结构实现。ArrayList 具有以下几个基本概念:

  • DEFAULT_CAPACITY 表示数组的初始大小,默认是 10;
  • size 表示当前数组的大小,类型 int,没有使用 volatile 修饰,非线程安全的;
  • elementData 数据存储的地方,是一个数组,数据的操作都围绕该属性进行;
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个数组,空构造使用该数组进行初始化;
  • modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1;

//DEFAULT_CAPACITY 表示数组的初始大小,默认是 10 在扩容的时候会用到
private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

// DEFAULT_CAPACITY 表示数组的初始大小,默认是 10 在扩容的时候会用到
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 数据存储的地方,使用数组存储,数据的操作都围绕该属性进行
transient Object[] elementData; // non-private to simplify nested class access

// 数组的大小
private int size;

核心操作

初始化

无参数直接初始化

public ArrayList() {
    // 直接使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 空数组构造,这是我们常见的写法
    // 注意这种写法默认数组的长度是为 0 ,并非 10,每次添加都需要扩容,所以工作中不太建议这么写
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

指定大小初始化

public ArrayList(int initialCapacity) {
    // 指定长度为 initialCapacity, 根据长度进行初始化
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 这里为什么不用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA?
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

指定初始数据初始化

public ArrayList(Collection<? extends E> c) {
    //elementData 是保存数组的容器,默认为 null
    elementData = c.toArray();
    //如果给定的集合(c)数据有值
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        //如果集合元素类型不是 Object 类型,我们会转成 Object
        if (elementData.getClass() != Object[].class) {
            elementData = Arrays.copyOf(elementData, size, Object[].class);
        }
    } else {
        // 给定集合(c)无值,则默认空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

ArrayList 提供了三种构造方法:

  • 使用无参构造器初始化时,使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 初始化 elementData,此时空间大小(数组的长度)是 0;
  • 指定大小 initialCapacity 初始化,将使用该大小来初始化 elementData,此时空间大小(数组的长度)是 initialCapacity;
  • 指定初始数据初始化,将给定的初始数据转化为数组,使用 Arrays.copyOf 拷贝到 elementData;

添加元素

public boolean add(E e) {
    // 确保内部空间容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

这里很简单,首先确保空间容量,对数组的空间大小做一个校验,然后对数组对应下标赋值,最后将 size + 1;所以核心在 ensureCapacityInternal 上;

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

这里也很简单,对 elementData 做了判断,如果 elementData 等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,也就是如果使用空参数构造方法初始化的话,比较 DEFAULT_CAPACITY 和 minCapacity,将大值赋给 minCapacity,然后走 ensureExplicitCapacity 函数。

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

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        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);
}

从这段代码可以看出,每次修改 elementData 数组,modCount 属性默认 + 1,也就是说如果想知道,ArrayList 被扩容了几次,只要查看 modCount 的大小即可。此外,如果要扩容达到的空间大小大于数组当前的空间大小就会走 grow 函数进行扩容。扩容的核心点在在于:ArrayList 会将当前的空间大小增加原来的一半(oldCapacity >> 1),也就是说如果原来空间大小是 22,经过一次扩容之后将会修改为 33,当然如果扩容的大小仍然小于目标大小,那么就直接使用目标大小。 此外值得注意的是,如果使用空参数构造 ArrayList,那么第一次扩容的大小是 10。

此外我们需要关注 Arrays.copyOf 函数,查看这个函数的底层实现,可以知道该函数调用了 System.arraycopy;

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

而 System.arraycopy 函数是 Native 方法,底层调用 C 函数;说明如下:

public static native void arraycopy(Object src,  int srcPos, Object dest, int destPos, int length);

src: 源数组。
srcPos: 源数组中的起始位置。
dest: 目标数组。
destPos: 目标数据中的起始位置。
length: 要复制的数组元素的数量。

删除元素

public E remove(int index) {
    // 校验 index >= size ,如果是则抛出异常
    rangeCheck(index);
    // 记录修改次数
    modCount++;
    // 获取数值中对应下标的的元素
    E oldValue = elementData(index);
    // 拷贝数据,用 elementData 数组 index 后一位的所有元素覆盖从 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;
}

这里需要关注 System.arraycopy 函数,从上来的介绍可以知道 arraycopy 函数从 src 数组将 srcPos 为开始位置,长度为 length 的元素拷贝到 以 destPos 为起点的 dest 数组上。总结下来,ArrayList 的 remove 核心操作是拷贝数据,用 elementData 数组 index 后一位的所有元素覆盖从 index 之后的所有元素。然后将 size - 1,最后将数组的末尾元素指向空。

总结

从我们上面新增或删除方法的源码解析,对数组元素的操作,只需要根据数组索引,直接新增和删除,所以时间复杂度是 O (1)。ArrayList 其实就是围绕底层数组结构,各个 API 都是对数组的操作进行封装,让使用者无需感知底层实现,只需关注如何使用即可。

posted @ 2021-12-03 18:03  yaomianwei  阅读(7)  评论(0编辑  收藏  举报