ArrayList 源码深度解析(基于JDK 1.8)

简言

以下所有源码基于JDK 1.8 ;
以前为了搞懂 ArrayList 底层,甚至手敲了一遍源码,现在再看一遍源码,并记下自己的理解。

ArrayList 介绍

  • 底层操作的是数组

  • ArrayList 是线程不安全

  • ArrayList 是一个实现了List接口的可调整大小的数组,可以包含null元素。

  • ArrayList 提供了一系列用于操作列表(数组)的方法,如增、删、查、改…

  • 优点:随机访问和修改的速度快(通过下标)

  • 缺点:随机删除和插入数据的效率低(可能需要移动数据)

建议:在涉及到查和改的操作比较多的时候,推荐使用 ArrayList; 在涉及到随机删除和插入数据的业务比较多的时候,推荐使用 LinkedList;


ArrayList 的字段

先来看看 ArrayList 中的字段,ArrayList中的字段不多,其中核心的字段我认为只有两个,elementData和size;

  • elementData: 用来存放元素的数组

  • size: 列表(数组)的总元素数

在整个ArrayList中的大多数方法都是围绕着这两个字段来进行操作;

另外还要小记一下,如果在创建ArrayList实例时不指定容量,初始容量会默认为10;

/**
 * 列表的默认初始容量为 10
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 无参构造方法使用到,默认容量的空数组实例
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 用于 共用 的空数组实例
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 存放元素的数组
 */
transient Object[] elementData;

/**
 * 数组的大小(存放的元素数)
 */
private int size;

ArrayList 的构造方法

要想玩,必先有,要想有,必先new;

ArrayList提供了3个构造方法,以满足不同的需求;

  • ArrayList() 无参构造方法,创建一个初始容量为10的空列表,不过容量不是在此构造方法中分配,在grow()方法中分配容量,后面会提到;
/**
 * 构造一个初始容量为10的空列表 
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
  • ArrayList(int initialCapacity) 用于创建指定容量列表的构造方法;

    执行流程:根据传过来的值创建相对应的列表,
    ​ 大于0时,手动创建指定容量的列表;
    ​ 等于0时,使用共用的空数组实例;
    ​ 小于0时,为非法容量值,抛出 IllegalArgumentException 异常

/**
 * 构造一个指定长度的列表。 <br>
 * @param initialCapacity 列表的初始容量
 */
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);
    }
}
  • ArrayList(Collection<? extends E> c) 接收一个集合的构造方法,用于创建包含指定集合中的元素的列表;

执行流程:首先将指定集合的元素返回给当前列表(elementData),再修改当前列表的总元素数(size), 如果指定集合的内容为空, 则再将当前列表置空

/**
 * 构造一个包含指定集合的元素的列表
 * @param c 指定的集合,包含要加入到当前列表的元素
 * @throws NullPointerException 如果指定的集合为空(此处的空不是没有元素,而是 c = null)
 */
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 {
        // 如果指定集合的内容为空,则使用默认的空数组实例来替换
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

增(add()、addAll()、扩容)

有了列表,就需要为其添加数据,毕竟空的列表是不好玩的,相信谁也不想见到IndexOutOfBoundsException,这玩意太烦人了;

从广度来分增加操作有两种,再细一点分有四种:

  1. 一次性增加一个元素

    I. 在列表尾部追加元素 ,add(E e)

    II. 在列表的任一位置插入元素,即随机插入,add(int index, E element)

  2. 一次性增加多个元素

    I. 在列表尾部追加一波元素,addAll(Collection<? extends E> c)

    II. 在列表的任一位置插入一波元素,addAll(int index, Collection<? extends E> c)

现在对这四个增加操作进行一个个的分析;


  • add(E e) 在列表的末尾追加元素;

执行流程:先检查当前列表的容量是否满足增加一个元素,不满足则对列表进行扩容,然后再追加元素到当前列表

/**
 * 将给定的元素追加到列表的 '末尾'
 * @param e 要添加到列表中的元素
 * @retuen 添加成功返回 true
 */
public boolean add(E e) {
    // 检查列表的容量是否满足再增加一个元素,不满足则扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将指定元素追加到末尾
    elementData[size++] = e;
    return true;
}

在追加元素之前,首先调用ensureCapacityInternal() 方法检查当前列表是否需要扩容;

再继续看ensureCapacityInternal() 方法的源码:

/**
 * @param minCapacity 期望的最小容量
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

在ensureCapacityInternal() 方法中又调用了两个方法,calculateCapacity() 方法用来判断当前列表是否是空的默认容量列表,并取适合的容量值,ensureExplicitCapacity() 方法确保列表的容量满足再增加元素;

再继续看calculateCapacity() 方法的源码:

/**
 * 如果当前的列表是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(使用无参构造方法指定的),
 * 则操作列表所请求的最小期望值与列表的初始容器默认值(10)比较,返回这两个数的最大值。
 * @param elementData 当前的列表
 * @param minCapacity 操作列表(add())时请求期望的最小容量
 * @return 适合的容量值
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果列表是默认容器DEFAULTCAPACITY_EMPTY_ELEMENTDATA(使用无参构造函数创建给定的空列表),
    // 则列表中默认大小(10)和给定的大小(minCapacity)进行比较,取最大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

calculateCapacity() 方法的返回值作为ensureExplicitCapacity() 方法的参数再进行调用 ;

再继续看ensureExplicitCapacity() 方法的源码:

/**
 * 确保列表(elementData)的容量满足操作列表所要请求的最小容量值(minCapacity) <br>
 * 不满足,则对列表(elementData)进行扩容
 * @param minCapacity 期望的最小容量
 */
private void ensureExplicitCapacity(int minCapacity) {
    // 在涉及到列表的修改操作,modCount的值都会变化一次,
    // 用于保证列表在迭代时,有并发操作产生的影响导致迭代的顺序不一致,可暂时略过该属性
    modCount++;

    // 防溢出代码
    // 如果操作列表所要请求的最小容量值(minCapacity) 大于 列表的总容量时,则需要扩容
    // 调用 grow() 进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

要ensureExplicitCapacity() 方法中调用grow()方法进行列表的扩容;

扩容的原因:由于ArrayList的底层操作的是数组,而数组又是定长的,且不能动态增长,当要添加的数据过多时,当前的数组长度不满足需求,会导致异常发生。

扩容的方案:创建一个长度满足需要的新数组,并将当前数组(列表)中的所有数据移动到该新数组中,再返回(elementData 重新指向新数组)

扩容的长度:如果每次扩容的量只满足当前的需求,那下一次操作列表(add())时,则又需要重新扩容,并重新移动数组的数据,这样是不合理的;扩容的量不能太大,又不能太小,经过科学家的研究,扩容的量为原数组长度 的 1.5 倍是较为合理的(扩容 1.5 倍)

再继续看grow() 方法的源码:

/**
 * 新的字段,用于控制列表(数组)的最大长度
 * 分配给列表容量的大小最大不能超过Integer.MAX_VALUE,否则会出错 <br>
 * 尽量不要大于 MAX_ARRAY_SIZE
 * Integer.MAX_VALUE = 2147483647
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 对列表的容量进行扩容,以确保列表容量能够满足所要请求的最小容量值
 * @param minCapacity 期望的最小容量
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // oldCapacity >> 1, 右移一位,近似于除2(oldCapacity / 2)
    // 每次扩容增加旧容量值的一半(扩容1.5倍),即
    // 新容量值 = 旧容量值 + 旧容量值的一半
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果默认的扩容值还是不满足所要请求的最小容量值,
    // 则扩容值取请求的最小容量值,而不是默认的扩容值
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果扩容值大于列表容量所能扩容的最大值,
    // 则扩容值为容器所能扩容的最大值(该操作在hugeCapacity()方法完成)
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 最后将原列表的元素,复制到长度为newCapacity的新列表
    elementData = Arrays.copyOf(elementData, newCapacity);
}

在扩容的时候,如果扩容的量大过默认的最大值(MAX_ARRAY_SIZE),则取整型的最大值Integer.MAX_VALUE,该值也是列表最大容量的边缘值

/**
 * 取最大扩容值 <br>
 * MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
 * @param minCapacity
 * @return 
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

add(E e) 方法在经过一系列的检查、扩容之后,在列表末尾追加一个元素操作即完成;

再来看看如何在列表的任意位置插入一个元素:


  • add(int index, E element) 接收一个的索引,并在列表的该索引处插入一个值;

执行流程:先检查索引是否有效,再检查容量是否够用,是否需要扩容,再将该指定索引处及后面的元素向后移动一位,再插入指定元素到该索引处,最后总元素数值 + 1

/**
 * 在列表中指定的位置插入(增加)一个元素,
 * 当前索引处及后面的所有元素向后移一位.<br>   
 * @param index 指定的位置
 * @param element 要插入的元素
 */
public void add(int index, E element) {
    // 检查给定的索引位置是否有效
    rangeCheckForAdd(index);
    // 检查列表的容量是否满足再增加一个元素,不满足则扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 使用arraycopy()方法进行元素的移动
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // 在指定的位置插入指定元素
    elementData[index] = element;
    // 总元素数 + 1
    size++;
}

调用 rangeCheckForAdd() 方法来检查要插入元素的索引是否有效;

再继续看 rangeCheckForAdd() 的源码:

/**
 * 检查给定的索引是否越界,包含检查负数. <br>
 * 当要插入的索引值等于size时,表示将值插入到最后一个位置,在原列表中,该索引处是没有值的
 */
private void rangeCheckForAdd(int index) {
    // 此处index > size, 索引值可以取到size, rangeCheck()方法是 >= 
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 下标索引越界异常的详细信息,会指示操作员哪里出错
 */
private String outOfBoundsMsg(int index) {
    return "Index: "+index+", Size: "+size;
}

从源码中可看得出,若索引值是无效的,则抛出 IndexOutOfBoundsException 异常;

add(int index, E element) 方法在检查完索引值是否有效后,会再继续检查当前列表的容量是否满足插入一个元素,该操作在ensureCapacityInternal() 方法中完成,在上述追加操作时已对该方法进行分析过,就不再多言;

在检查完容量操作后会使用System类的arraycopy()方法对原数组的元素进行移动,将指定索引处及后面的元素向后移动一位,再将指定的值插入到列表的该索引处;


  • addAll(Collection<? extends E> c) 在列表的末尾追加一波元素,这一波元素存放在指定的集合©中

执行流程:先获取指定集合中的所有值和其长度,再检查当前列表的容量是否能够容纳指定集合中的所有元素,不满足则扩容,最后将指定集合中的所有元素追加到当前列表的末尾

/**
 * 将指定集合的所有元素追加(插入)到当前列表的末尾
 * @param c 将其所有元素追加(插入)到当前列表的集合
 * @return 如果指定的集合存在着要插入的元素(其长度不为0),则返回true
 */
public boolean addAll(Collection<? extends E> c) {
    // 获取包含给定集合的所有元素的数组,并得到其数组长度
    Object[] a = c.toArray();
    int numNew = a.length;
    // 检查当前列表的容量是否满足再增加a.length长度的元素,不满足则扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount
    // 将给定集合的所有元素插入到当前列表的末尾
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

ensureCapacityInternal() 扩容方法已在追加元素时分析过,就不再多言


  • addAll(int index, Collection<? extends E> c) 将指定集合中的所有元素插入到列表的指定位置

执行流程:先检查要插入的位置的索引值是否有效,然后获取指定集合中的所有值和其长度,再检查当前列表的容量是否能够容纳指定集合中的所有元素,不满足则扩容,然后再移动列表中的元素,最后再将集合中的所有元素插入到当前列表。

/**
 * 将指定集合中的所有元素从指定位置开始插入到当前列表
 * @param index 要插入到当前列表的位置索引 
 * @param c 将其所有元素添加(插入)到当前列表的集合
 * @return 如果指定的集合存在着要插入的元素(其长度不为0),则返回true
 */
public boolean addAll(int index, Collection<? extends E> c) {
    // 检查要插入的位置的索引是否有效
    rangeCheckForAdd(index);

    // 获取包含给定集合的所有元素的数组,并得到其数组长度
    Object[] a = c.toArray();
    int numNew = a.length;
    // 检查当前列表的容量是否满足再增加a.length长度的元素,不满足则扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount

    // numMoved:需要移动的元素数(索引index的后续元素)
    // 使用arraycopy()方法来移动元素
    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew, numMoved);

    // 将指定集合的所有元素(a)插入到当前列表
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

删(remove()、removeAll()、retainAll()、clear())

删除有5种操作

1). 删除指定索引处的值,remove(int index)

2). 删除列表中与指定元素匹配的元素,且是最先出现在列表的,remove(Object o)

3). 删除与指定集合的元素匹配的当前列表元素,removeAll(Collection<?> c)

4). 删除与指定集合的元素不匹配的当前列表元素,retainAll(Collection<?> c)

5). 清空当前列表,clear()


  • remove(int index) 删除列表中指定索引处的值

执行流程:先检查指定的索引是否有效,再获取该索引处的值(用于最后返回该值),再将索引处的后续元素向前移动一位(直接覆盖该索引处的原值),再将列表中的最后一个位置置空(交由GC去回收内存),最后返回索引处的旧值

/**
 * 移除(删除)指定索引处的值,指定索引处的后续元素向前(左)移动一位(直接覆盖指定索引处的旧值)。
 * @param 要移除的元素的索引
 * @return 列表中移除的元素
 */
public E remove(int index) {
    // 检查指定的索引位置是否有效
    rangeCheck(index);

    modCount++;
    // 获取该索引处的值,最后返回该值
    E oldValue = elementData(index);

    // numMoved: 要移除的索引(index)值的后续元素数
    // 使用arraycopy()方法来移动元素
    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;
}

删除操作首先调用 rangeCheck() 方法来检查指定的索引是否有效,该检查索引的方法与插入元素中的检查索引方法(rangeCheckForAdd())略有不同;

再继续看 rangeCheck() 的源码:

/**
 * 检查给定索引是否在范围内。
 * 如果没有,则引发适当的运行时异常。
 * get()、remove()、set()方法中调用该方法检查索引 
 * 如果索引为负值,则抛出ArrayIndexOutOfBoundsException异常。
 * @param index
 */
private void rangeCheck(int index) {
    // 此处是index >= size, rangeCheckForAdd()方法是 >
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

  • remove(Object o) 删除列表中与指定元素匹配的元素,且是最先出现在列表的

执行流程:首先判断该指定要移除的值是否为空,为空的话,则单独进行处理(因为 null 值不能使用equals()方法与null进行比较,会报NullPointerException异常),不为null,则使用equals()方法与列表中的值进行比较,如果列表中有该指定的值,最后返回true;移除操作在 fastRemove() 方法中

/**
 * 移除列表中跟指定值匹配的元素,且是最先出现在列表中的元素(只移除一个元素).<br>
 * @param o 要从列表中移除的元素
 * @return 如果此列表中有该元素,则返回true
 */
public boolean remove(Object o) {
    // 由于List可以存放null值,而null值的判断方式跟其他类型的不一样,
    // 因此需先判断指定要移除的值是否为null;
    if (o == null) {    
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                // fastRemove()移除操作在该方法中
                fastRemove(index);
                return true;
            }
    } else {    // 处理指定的非空值
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

删除指定的元素分为两种情况,删除null元素和普通元素,两种操作都是使用遍历数组来查找匹配元素,且都是使用fastRemove() 方法来删除匹配的元素;

再继续看 fastRemove() 的源码:

/**
 * 处理元素的移动,并将列表尾部元素置空
 * @param index 要移除的元素的索引
 */
private void fastRemove(int index) {
    modCount++;
    // numMoved: 要移除的索引(index)的后续元素数
    // 再使用arraycopy()方法来移动元素
    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() 方法的源码可看出,直接将指定索引处后面的元素向前移动一位覆盖指定索引处的值,再将列表中最后的那个元素置空,交由GC去回收;


  • removeAll(Collection<?> c) 删除与指定集合的元素匹配的当前列表元素

执行流程:先检查指定的集合是否为空(c == null),再调用 batchRemove() 方法来删除匹配的元素

/**
 * 删除当前列表中包含给定集合的所有元素,
 * 既当前列表中有跟给定集合的元素一样的都给删除
 *
 * @param c 包含要从列表中删除的元素的集合
 * @return true
 */
public boolean removeAll(Collection<?> c) {
    // 检查给定的集合是否为null
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

在removeAll() 方法中首先调用 Objects类的 requireNonNull() 方法来判断指定的集合是否为空,此处的空不是内容为空,而是指定集合指向 null (c = null);

可继续看requireNonNull() 的源码:

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

在removeAll() 方法中对指定集合判空后,再调用batchRemove() 方法来删除匹配的元素;

再继续看 batchRemove() 的源码,在batchRemove() 方法中有两种情况,当complement为true时,当前列表只保留与指定集合中匹配的元素(retainAll() 方法的操作);当complement为false时, 则删除当前列表与指定集合中匹配的元素(removeAll()方法的操作)

执行流程:先判断给定的条件(complement)是保留还是删除与给定的集合中匹配的元素,然后将要保留的元素移到列表的前面,最后删除无效的元素(finally中完成)

/**
 * 根据给定条件(complement)进行保留或删除集合(c)中的匹配元素 <br>
 * @param c 指定要保留或删除哪些元素的集合
 * @param complement true 只保留指定集合内存在的元素; false 删除指定集合内存在的元素; 
 * @return true 操作完成
 */
private boolean batchRemove(Collection<?> c, boolean complement) {
    // 获取当前列表(其实操作的都是同一个列表)
    // 可以理解为左边的列表(elementData)用来保存留下来的元素,右边的列表用来操作比较
    final Object[] elementData = this.elementData;
    // r: 要遍历的(右)列表的索引
    // w: 最终存放元素的(左)列表的索引
    int r = 0, w = 0;
    // 标志位,操作完成后变为true,并返回
    boolean modified = false;
    try {
        // 遍历列表来保留符合条件的元素,此处的条件有两个,即
        // 当 complement 为 true 时:当前列表只保留在给定集合 c 中存在的元素。
        // 当 complement 为 false 时:当前列表只保留在给定集合 c 中不存在的元素。
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // r != size 时,说明在上述操作中出错抛出了异常,就算出错也要保证数据的完整性
        if (r != size) {
            // 从列表出错的那个位置开始,将其后面的所有元素都插入到新列表(当前保留元素的列表)
            System.arraycopy(elementData, r, elementData, w, size - r);
            w += size - r;
        }
        // w != size时,说明有元素需要删除(不是所有元素都被保留)
        if (w != size) {
            // clear to let GC do its work
            // 将当前列表中无需保留的元素置空,交由GC(垃圾回收)去处理
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

该batchRemove() 方法有点长,只要搞懂第二个参数complement 为 true 和 false 两种情况所发生的操作,即可搞懂 batchRemove() 方法;

看完了removeAll(),再来看retainAll()


  • retainAll(Collection<?> c) 只保留指定集合中存在的元素

执行流程:先检查指定的集合是否为空(c == null),再调用 batchRemove() 方法来保留与指定集合中匹配的元素,并删除不匹配的元素

/**
 * 仅保留指定集合中存在的元素
 *
 * @param c 包含要保留的元素的集合
 * @return true 操作成功
 */
public boolean retainAll(Collection<?> c) {
    // 检查给定的集合是否为null
    Objects.requireNonNull(c);
    return batchRemove(c, true);
}

batchRemove() 方法已在 removeAll() 中分析过,就不再多言;

removeAll() 方法和 retainAll() 的区别在于调用batchRemove() 方法时,removeAll() 传过去的参数是false,retainAll() 传过去的参数是true,batchRemove() 方法根据这个参数来判断是要保留与指定集合匹配的元素,还是要删除与指定集合匹配的元素。


  • clear() 清除当前列表的所有元素,使当前列表变成一个空列表

执行流程:遍历数组,将所有元素置空

/**
 * 将列表清空,使列表变成一个空列表.<nr>
 * 注意: 只是移除列表中的所有值,列表还是存在的 <br>
 */
public void clear() {
    modCount++;

    // 使用for循环将列表中的所有元素的值置空,然后交由GC(垃圾回收机制)来处理
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

改(set())

  • set(int index, E element) 将指定索引处的值修改为指定的值

执行流程:先检查指定的索引是否有效,再获取该索引处的旧值,再将该索引处的值修改为指定的新值,最后返回该索引处的旧值。

/**
 * 修改指定索引处的值
 * @return 该索引处的旧值
 */
public E set(int index, E element) {
    // 先检查给定的索引是否在范围内, index < size(列表中的元素数)
    rangeCheck(index);

    // 然后获取该索引处的旧值
    E oldValue = elementData(index);
    // 再修改为新值
    elementData[index] = element;
    return oldValue;
}

可再继续看 rangeCheck() 源码,在上述分析已出现过该源码:

/**
 * 检查给定索引是否在范围内。
 * 如果没有,则引发适当的运行时异常。
 * 如果索引为负值,则抛出ArrayIndexOutOfBoundsException异常。
 * @param index
 */
private void rangeCheck(int index) {
    // 此处是index >= size, rangeCheckForAdd()方法是 >
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

查(contains())

  • contains(Object o) 查询当前列表中是否有该指定的值
/**
 * 判断列表中是否包含该值
 * @return true 如果当前列表中有指定的值
 */
public boolean contains(Object o) {
    // 如果indexOf(o)返回的值是该指定元素在当前列表中的索引(如果有)
    // 如果indexOf(o)的返回值大于等于0,说明当前列表中有该值,则返回 true
    return indexOf(o) >= 0;
}

从contains() 方法的源码中可看出只有一条语句,

再继续看 indexOf() 的源码:

/**
 * 获取最先出现在列表中指定的值的索引,如果没有则返回 -1 
 * @return 列表中与指定元素匹配的索引,且是最先出现在列表的
 */
public int indexOf(Object o) {
    // 首先判断要查的对象是否为空
    // null 不能使用 equals() 跟null 进行比较,会报NullPointerException异常
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

从indexOf() 的源码中可看出有两种情况,当指定的元素为 null 和 指定元素为普通元素,两种情况都是要遍历数组来找出匹配的元素,并返回该匹配元素所在的下标;


获取(get())

  • get(int index) 获取当前列表中指定索引处的值

执行流程:首先检查指定索引是否有效,再直接使用索引来获取指定的值

/**
 * 如果索引有效, 则返回列表中指定索引处的值
 */
public E get(int index) {
    // 检查给定的索引是否在范围内, index < size(列表中的元素数)
    rangeCheck(index);

    return elementData(index);
}

从源码可看出先调用 rangeCheck() 方法来检查指定索引是否有效,该方法已在remove()方法分析过,就不再多言; get() 方法的第二步是调用 elementData() 方法来获取指定索引处的值;

再继续看 elementData() 的源码:

/**
 * @param index 指定的索引
 * @return 列表中指定索引处的值
 */
@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

从 elementData() 方法的源码中可看出,直接使用索引值来获取元素,那效率是非常之高的。


大小(size())

  • size() 获取列表的总元素数;

    我觉得说是获取列表的长度是不严谨的,长度一般也可指容量,而列表的容量不一定等于列表的使用量(总元素数)

/**
 * @return 列表的元素总数
 */
public int size() {
    return size;
}

判空(isEmpty())

  • isEmpty() 判断当前列表是否存在元素
/**
 * @return true 如果列表为空 
 */
public boolean isEmpty() {
    return size == 0;
}

压缩(trimToSize())

  • trimToSize() 将当前列表的容量压缩到极致,此时 容量 = 总元素数
/**
 * 调整当前列表的容量为列表的总元素数
 */
public void trimToSize() {
    // 涉及到列表的修改操作的,modCount的值都会变化一次,
    // 用于保证列表在迭代时,有并发操作产生的影响导致迭代的顺序不一致
    modCount++;
    // 如果列表的实际元素总数小于列表的容量,则减少容量值
    if (size < elementData.length) {
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

数组(toArray())

toArray() 方法可将当前列表的元素以数组的形式返回,该方法有两种实现 ;

  • toArray() 将当前列表的所有元素按一定顺序以数组的形式返回
/**
 * 将当前列表中的元素以数组的形式返回
 */
public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}
  • toArray(T[] a) 将当前列表的所有元素按一定顺序拷贝到指定数组(a)

执行流程:如果指定的数组长度比当前列表的元素数小,则以指定的数组类型重新分配一个新数组来存放拷贝的当前列表元素并返回,如果指定的数组长度大于或等于当前列表的元素数,则直接将当前列表的元素拷贝到指定数组

/**
 * 将当前列表中的元素拷贝到指定的数组中,如果指定的数组中有元素,
 * 会在拷贝的过程覆盖指定数组中的元素
 * @param a 要存放列表元素的数组(建议该数组的长度 = 列表的元素数)
 */
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    // 如果指定的数组长度小于当前列表的元素数,则分配一个新的数组来存储
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    // 指定的数组长度大于或当前列表的元素数,存储到指定数组中
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

迭代器(iterator())

  • iterator() 以正确的顺序返回当前列表的迭代器
/**
 * 以正确的顺序返回该列表中的元素的迭代器
 */
public Iterator<E> iterator() {
    return new Itr();
}

从iterator()源码可看出,new 了一个 Itr,ArrayList迭代器是以内部类的形式实现的;

再继续看 Itr 内部类的源码,该类实现了 Iterator 接口,从而具有了可迭代的特性;

/**
 * 抽象列表的一个优化版本 (内部类的形式) <br>
 * 实现了Iterator接口,使得列表有了可迭代的特性
 */
private class Itr implements Iterator<E> {
    int cursor;       // 要返回的下一个元素的索引
    int lastRet = -1; // 当前返回元素的索引;如果没有返回 -1
    // modCount 字段在上述中经常出现自加操作,在迭代器中就发挥了它的作用
    int expectedModCount = modCount;

    Itr() {}

    /**
     * 如果下一个元素的索引不等于当前列表的长度(元素总数),
     * 则还有下一个元素可遍历
     */
    public boolean hasNext() {
        return cursor != size;
    }

    /**
     * 获取下一个元素
     */
    @SuppressWarnings("unchecked")
    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;
        return (E) elementData[lastRet = i];
    }

    /**
     * 删除此迭代器返回的最后一个元素
     */
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

    /**
     * 检测当前线程操作列表进行迭代时,是否有其他线程进行修改当前列表的操作,
     * 在这种操作下迭代的结果是不确定的,这种改变是不被允许的 
     * @throws ConcurrentModificationException 检测到对象的并发调用修改的方法引发,而这种修改是不允许的。
     */
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

小结

谨记 ArrayList 的容量在扩容时都会增长 0.5 倍,若 0.5 倍还不满足才会扩容至满足需求的容量

Vector 则是扩容 1 倍

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值