ArrayList 源码解析(JDK1.8)

news2024/9/24 5:23:28

目录

一. 前言

二. 源码解析

2.1. ArrayList概览

2.2. 属性

2.3. 构造方法

2.3.1. ArrayList()

2.3.2. ArrayList(int initialCapacity)

2.3.3. ArrayList(Collection c)

2.4. 新增元素

2.4.1. add(E e)

2.4.2. add(int index, E element)

2.4.3. addAll(Collection c)

2.4.4. addAll(int index, Collection c)

2.5. 删除元素

2.5.1. remove(int index)

2.5.2. remove(Object o)

2.5.3. removeRange(int fromIndex, int toIndex)

2.5.4. removeAll(Collection c)

2.6. 查找元素

2.7. 转换成数组

2.8. 其他常见方法

2.9. 遍历方式

三. Fail-Fast机制


一. 前言

    ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。由于Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。

从类图中我们可以看出,ArrayList实现了4个接口和继承了1个抽象类;4个接口分别为:
List接口,主要提供数组的添加、删除、修改、迭代遍历等操作。
Cloneable接口,表示ArrayList支持克隆。
RandomAccess接口,表示ArrayList支持 快速的随机访问。
Serializable接口,表示ArrayList支持序列化的功能。

继承的抽象类为 AbstractList,主要提供的是迭代遍历相关的操作;如果我们单纯看 List --> AbstractList --> ArrayList 这一实现/继承关系来看的,是不是有一种模板方法模式的感觉呢? List接口定义了操作行为,AbstractList抽象类提供了可重用的算法骨架;而最终的 ArrayList 实现类根据自己的情况自定义实现接口。

注意:实际上 ArrayList 大量重写了 AbstractList 抽象类提供的方法实现;所以 AbstractList 对 ArrayList来说意义不大,但 AbstractList 的其他很多子类享受到了这个福利。

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

二. 源码解析

2.1. ArrayList概览

在进行源码解析前,我们先预览一遍 ArrayList 中的所有方法吧,并且挑选比较核心的方法进行讲解。

/****************** ArrayList中的构造函数 ***************/
// 默认构造函数
ArrayList()

// capacity是ArrayList的默认容量大小。当由于增加数据导致容量不足时,容量会添加上一次容量大小的一半。
ArrayList(int capacity)

// 创建一个包含collection的ArrayList
ArrayList(Collection<? extends E> collection)

/****************** ArrayList中的API ********************/
// Collection中定义的API
boolean             add(E object)
boolean             addAll(Collection<? extends E> collection)
void                clear()
boolean             contains(Object object)
boolean             containsAll(Collection<?> collection)
boolean             equals(Object object)
int                 hashCode()
boolean             isEmpty()
Iterator<E>         iterator()
boolean             remove(Object object)
boolean             removeAll(Collection<?> collection)
boolean             retainAll(Collection<?> collection)
int                 size()
<T> T[]             toArray(T[] array)
Object[]            toArray()

// AbstractCollection中定义的API
void                add(int location, E object)
boolean             addAll(int location, Collection<? extends E> collection)
E                   get(int location)
int                 indexOf(Object object)
int                 lastIndexOf(Object object)
ListIterator<E>     listIterator(int location)
ListIterator<E>     listIterator()
E                   remove(int location)
E                   set(int location, E object)
List<E>             subList(int start, int end)

// ArrayList新增的API
Object               clone()
void                 ensureCapacity(int minimumCapacity)
void                 trimToSize()
void                 removeRange(int fromIndex, int toIndex)

2.2. 属性

ArrayList的属性很少,只有2个属性:elementData 和 size;其中 elementData 代表的是存放元素的数组;而 size 代表的是 elementData 数组中元素的数量。比如下图所示: elementData数组的长度是9,但是只存放了5个元素,这时候 size属性就是5。

我们在使用 ArrayList 的时候会经常调用其 size() 方法,返回的就是 elementData 数组中已使用的元素的数量,也即是当前的 5;并且当我们新添加元素的时候,size所指代的也恰巧是新元素的下标(数组下标从0开始)。

/**
  * 存放元素的数组
  * 在新增元素的时候,如果遇到数组不够会创建新的数组,并且将原数组的元素拷贝到新数组;最后将该变量指向新数组
  */
transient Object[] elementData;

/**
  * 数组的大小
  * size代表的是已使用elementData的元素的数量,也即是size()方法的大小
  */
private int size;

2.3. 构造方法

ArrayList一共有3个构造方法,分别如下所示。

2.3.1. ArrayList()

    无参构造方法,平时使用的时候,如果你的IDEA有安装 sonarLint 扫描,会被提示:创建集合的时候要指定初始化容量哦!

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    // 实际创建的时候是空数组,在首次添加元素的时候,才会初始化容量为10的数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

无参构造方法使用了 DEFAULTCAPACITY_EMPTY_ELEMENTDATA对象,查看源码可知其定义:private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {} 是一个空数组。

经常有朋友会有疑问:“我之前学习的时候被灌输的是----当未指定初始化大小的时候,ArrayList默认的大小是10呀!”

这句话一定程度上来说是没问题的,只是缺失了一些补充描述:当未指定初始化大小的时候,ArrayList先是初始化为一个空数组;但在首次添加元素时,ArrayList才会初始化为一个容量为10的数组。这么做的原因是节省内存,一些场景可能只是单纯的定义了数组并没有真实的使用,直接初始化容量为10的数组会造成不必要的浪费。

细心的朋友可能会问:“既然都是空的数组,那为什么不直接使用 EMPTY_ELEMENTDATA 空数组;而是要重新定义一个新的空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA呢?”

这个问题设计者有其特殊的考虑,后面在介绍数组扩容的时候会详细介绍两者的不同;这里先简单讲一点,DEFAULTCAPACITY_EMPTY_ELEMENTDATA的首次扩容是 10, EMPTY_ELEMENTDATA的首次扩容是按照1.5倍扩容,从0开始;两者的起点不同。

2.3.2. ArrayList(int initialCapacity)

ArrayList(int initialCapacity)构造方法,根据传入的 initialCapacity 初始化容量来创建 elementData数组。如果我们在使用 ArrayList 的时候,预先知道元素的数量,应该尽可能的使用该构造方法,这样可避免数据扩容,从而提升性能,同时也是合理的利用内存空间。

private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 初始化容量大于0,创建Object数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 初始化容量为 0 时,使用 EMPTY_ELEMENTDATA 对象
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 参数异常
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

这里需要留意的是:当初始化容量 initialCapcity 为 0 的时候,使用了一个 EMPTY_ELEMENTDATA对象,查看源码可知其定义: private static final Object[] EMPTY_ELEMENTDATA = {} 是一个空数组。在添加元素的时候,会进行扩容创建需要的数组。

2.3.3. ArrayList(Collection<? extends E> c)

ArrayList(Collection<? extends E> c) 构造方法,使用传入的集合 c 作为 ArrayList的 elementData,当 elementData 数组不空的时候,有一个类型转换的过程,因为 elementData是 Object[]类型的数组。

public ArrayList(Collection<? extends E> c) {
    // element指向c.toArray(),toArray()方法会创建一个新的数组
    elementData = c.toArray();

    // 数组不为空
    if ((size = elementData.length) != 0) {
        // 如果集合元素的类型不是Object.class类型,则拷贝成Object.class类型
        if (elementData.getClass() != Object[].class) {
            elementData = Arrays.copyOf(elementData, size, Object[].class);
        }
    } else {
        // 数组为空,使用 EMPTY_ELEMENTDATA 对象
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

2.4. 新增元素

新增元素的方法主要有如下4个,分别为:
public boolean add(E e) 在数组后面顺序新增一个元素。
public void add(int index, E element) 在指定 index 位置添加元素。
public boolean addAll(Collection<? extends E> c) 在数组后面顺序添加多个元素。
public boolean addAll(int index, Collection<? extends E> c) 在指定 index位置,添加多个元素。

2.4.1. add(E e)

继承于 AbstractList 抽象类,在数组后面顺序添加单个元素。

public boolean add(E e) {
    // size是当前数组的元素个数,+1代表新增一个元素
    ensureCapacityInternal(size + 1);
    // 这个操作相当于 element[size] = e, size++
    elementData[size++] = e;
    // 返回添加成功
    return true;
}

方法只有3句代码,看起来比较简单;主要关注一下 ensureCapacityInternal 方法做了什么事情;经过上面的介绍我们知道,ArrayList的底层是使用数组实现,并且其特性是支持动态扩容的;那么当我们新增元素的时候应该需要考虑:现有的数组容量是否支持新增元素,如果容量不满足,则要考虑扩容操作

我们来看看JDK的设计者是怎么做的:

STEP-1,首先是 ensureCapacityInternal(int minCapacity) 方法,该方法主要功能:确保数组内部容量足以满足本次添加元素操作。方法实现上首先是调用了 calculateCapacity(Object[] elementData, int minCapacity) 方法,计算满足本次新增操作所需的容量,然后调用 ensureExplicitCapacity(int minCapacity) 方法,保证容量足够。

/**
* 确保数组容量足够添加元素
*
* @param minCapacity 最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
    // 计算容量
    int capacity = calculateCapacity(elementData, minCapacity);
    // 确保容量足够 -- 如不足够则触发扩容
    ensureExplicitCapacity(capacity);
}

STEP-2,然后我们来看看 calculateCapacity(Object[] elementData, int minCapacity) 方法是如何计算本次新增操作所需的容量的。

如果 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA——没有指定初始化容量时;则会判断是最小容量 minCapacity 和 DEFAULT_CAPACITY 的大小,取大值。

/**
 * 计算容量
 * @param elementData 元素数组
 * @param minCapacity 最小容量
 * @return int
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是DEFAULT_CAPACITY,那么就会从10开始
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // DEFAULT_CAPACITY 定义:private static final int DEFAULT_CAPACITY = 10;
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 如果不是则直接返回需要的最小容量
    return minCapacity;
}

STEP-3,ensureExplicitCapacity如何确保数组容量足够本次新增操作的? 由代码可以看出,当所需的最小容量minCapacity大于 elementData.length 数组长度的时候,则触发 grow()执行扩容。

/**
 * 确保数组剩余容量足够
 *
 * @param minCapacity 最小容量
 */
private void ensureExplicitCapacity(int minCapacity) {
    // 在父类 AbstractList中定义了用以记录数组修改的次数(注意是次数)
    modCount++;

    // minCapacity代表的是本次(新增)操作所需要的最小容量,elementData.length代表的是当前元素数组的长度
    // 可能写成 minCapacity > elementData.length 更好理解
    if (minCapacity - elementData.length > 0) {
        // 扩容可指定最小的容量(满足当前需求)
        grow(minCapacity);
    }
}

STEP-4,看看grow是怎么扩容的

/**
 * 扩容
 * 旧容量经过运算扩展为1.5后与最小容量minCapacity进行比较
 * 如果大于则采用旧容量扩展1.5倍后的大小,否则采用最小容量minCapacity
 * @param minCapacity 新增操作所需最小容量
 */
private void grow(int minCapacity) {
    // 旧容量大小 elementData 数组的长度
    int oldCapacity = elementData.length;
    // 新容量大小 == 旧 + 旧的位运算(向右移动1位),大致上是原大小的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果计算出来的新容量小于指定的最小容量,则创建指定的最小容量大小的新数组
    // 可理解为 newCapacity < minCapacity,源码中写成了减法的形式。
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    // 如果新容量大于最大值(会出现内存不足的情况)
    // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        // hugeCapicaty方法用以当 newCapacity 超过指定范围的时候,确保创建的数组不会溢出
        newCapacity = hugeCapacity(minCapacity);
    }
    // 数组拷贝,将原有的数据搬到新的数组上
    elementData = Arrays.copyOf(elementData, newCapacity);
}

经过一轮讲解,我们大体上知道ArrayList是如何新增元素的,首先计算出新增元素所需要的最小容量,然后判断当前 elementData 数组是否支持本次新增操作(容量是否足够),当容量不足的时候则触发扩容机制,将原有数组的元素拷贝到新数组上,然后往后追加新的元素。

2.4.2. add(int index, E element)

public void add(int index, E element) 方法继承于 AbstractList抽象类,在指定 index 位置新增元素 element。

/**
 * 在指定index位置添加元素element
 * @param index
 * @param element
 */
public void add(int index, E element) {
    // 参数合法性校验
    rangeCheckForAdd(index);
    // 确保容量可用----如果不可用会触发扩容机制
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 数组index位置后的元素都往后移1位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // 指定位置添加element元素
    elementData[index] = element;
    // size + 1
    size++;
}

/**
 * 参数合法性校验
 * index > size 不允许,只能是对已存在的index进行添加
 */
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0) {
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
}

方法比较简单,首先是参数的合法性检查,然后就是上面介绍的 ensureCapacityInternal 方法,用以确保数组容量满足本次新增操作;需要额外讲解的是 System.arraycopy() 方法。

System.arraycopy(Object src, int scrPos, Object desc, int destPos, int length) 方法,用于将指定源数组中的元素从指定位置到目标数组的指定位置。
src:源数组
srcPos:源数组复制的起始位置
desc:目标数组
descPos:目标数组填充数据的起始位置
length:要复制源数组的元素数量

例如:

// 原数组:int[] arr1 = {1,2,3,4,5,6,7,8,9,0};
// 目标数组:int[] arr2 = new int[4];
// 操作:将原数组第二个位置以后的4个数据copy到目标数组
System.arrayCopy(arr1, 1, arr2, 0, 4);

2.4.3. addAll(Collection<? extends E> c)

public boolean addAll(Collection<? extends E> c) 继承于 AbstractCollection 抽象类,在数组后面 顺序添加多个元素。当我们确定会添加多个元素的时候,建议使用该方法而不是单个元素逐个的添加,避免可能多次扩容造成的性能下降。

/**
 * 在数组后面顺序添加多个元素
 * @param c 集合c
 * @return boolean
 */
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    // 确保数组容量足够本次添加元素操作,和添加单个元素的一样;只不过size不再是 + 1 而是 + c.length
    ensureCapacityInternal(size + numNew);  // Increments modCount
    // 拷贝数组
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

和 add(E e) 很类似,只不过是将添加1个元素变成了添加多个元素罢了。

2.4.4. addAll(int index, Collection<? extends E> c)

public boolean addAll(int index, Collection<? extends E> c) 方法继承于 AbstractList 抽象类,用以在指定 index 位置添加多个元素。

/**
 * 在指定index位置添加多个元素c
 * @param index
 * @param c
 * @return boolean
 */
public boolean addAll(int index, Collection<? extends E> c) {
    // index 参数合法性检查
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    // 确保数据容量满足本次添加元素操作 ---- 不满足则扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount
    // 原有index后的元素往后移动,移动numNew个位置
    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
    // 空闲出来的位置,填充需要添加的元素
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

首先还是先进行容量可达操作,让数组的容量满足本次的扩容操作;其次,因为需要在 index 位置新增多个元素,则原来在 index 位置后的的元素都应该往后顺延 c.length 个位置。

2.5. 删除元素

删除元素的方法主要有4个,分别为如下所示:
public E remove(int index) 移除指定位置的元素,并返回该位置的原元素。
public boolean remove(Object o) 移除首个为 o 的元素,并返回是否移除成功。
protected void removeRange(int fromIndex, int toIndex) 批量移除 [fromIndex, toIndex)内的多个元素,这里需要特别注意: toIndex不包括的噢。
public boolean removeAll(Collection<?> c) 批量移除指定的多个元素。

2.5.1. remove(int index)

public E remove(int index) 继承于 AbstractList 抽象类,删除指定位置的元素,并返回该位置上的原有元素。

/**
 * 删除指定位置index的元素,并返回该元素
 * @param index
 * @return E
 */
public E remove(int index) {
    // index 合法性校验,不合法则抛出相关异常
    rangeCheck(index);
    // 修改数组的次数 + 1 
    modCount++;
    // 获取index下标对应的value,elementData方法其实就是 return (E) elementData[index]
    E oldValue = elementData(index);

    // 每删除一个元素,都需要对原有的数组进行移动,所以从这里也可以看出ArrayList并不是特别适合于删除操作比较多的场景
    // 需要移动的元素的数量
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    // 数组的最后一个位置设置为null  帮助GC
    // 这个操作相当于 elementData[size] = null;  size --;
    // 不建议开发中这么写,代码还是要以可读性为准
    elementData[--size] = null;

    return oldValue;
}

private void rangeCheck(int index) {
    // index 合法性校验
    if (index >= size) {
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
}

代码还是比较简单易读的,凡是涉及到 index 的首先都是万年不变的参数合法性检查,然后再获取index对应下标的元素;再对index位置后的元素往前移动,最终返回被删除元素值。

需要关注的是:ArrayList的删除操作,都会伴随着数组元素的移动操作(除非你每次都删除最后一个元素,就当我没说),而这种移动操作是有性能上的消耗的;因此也可以看出 ArrayList 并不是那么适合于删除特别频繁的场景。

2.5.2. remove(Object o)

public boolean remove(Object o) 方法继承于 AbstractCollection 抽象类,移除首个为 o 的元素,返回是否移除成功。

/**
 * 移除首个为 o 的元素
 * @param o
 * @return  boolean
 */
public boolean remove(Object o) {
    if (o == null) {
        // 从头遍历elementData数组,找到第一个o元素的index,调用fastRemove方法进行删除
        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;
}

/**
 * 纯粹的移除元素的操作,不做参数校验(由上层调用者自己做参数合法性检查)
 * 操作次数 + 1
 * index后的元素向前移动
 */
private void fastRemove(int index) {
    // 数组操作次数 + 1
    modCount++;
    // 需要迁移的元素的数量
    int numMoved = size - index - 1;
    if (numMoved > 0) {
        // 元素前移,填充index位
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    }
    // 清理最后一个位置,设置为null,方便后续的GC
    // 相当于 elementData[size] = null; size --;
    elementData[--size] = null;
}

方法不难,看看是怎么实现的:首先是从头遍历elementData数组,找到 第一个 匹配的 o 元素,返回其下标,调用 fastRemove(int index)方法,进行删除操作。

需要注意的是:我们可以看出,ArrayList是支持添加为 null 的元素的。

2.5.3. removeRange(int fromIndex, int toIndex)

protected void removeRange(int fromIndex, int toIndex) 继承于 AbstractList抽象类,批量移除 [fromIndex, toIndex)位置的多个元素,注意不包括 toIndex 位置的元素。

/**
 * 移除 [fromIndex, toIndex) 范围内的元素
 * @param fromIndex  包括
 * @param toIndex 不包括
 */
protected void removeRange(int fromIndex, int toIndex) {
    // 操作次数 + 1
    modCount++;

    // toIndex后面的元素都需要被移动到前面
    int numMoved = size - toIndex;
    // 因此可以看到为啥右边是),因为它并没有 + 1
    System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved);

    // 计算新的数组长度
    int newSize = size - (toIndex - fromIndex);
    // 数组往前移动后,后面所有空闲的位置都设为null
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    // 更新新的长度
    size = newSize;
}

我们可以从源码中看出为什么右边 toIndex 并没有被包含,因为 arraycopy 方法的第二个参数是从 toIndex开始,并没有 + 1;后面的代码比较简单可直接看注释。

2.5.4. removeAll(Collection<?> c)

public boolean removeAll(Collection<?> c) 方法,继承于AbstractCollection抽象类,批量删除指定的多个元素。

/**
 * 批量删除指定的多个元素
 * @param c
 * @return boolean
 */
@Override
public boolean removeAll(Collection<?> c) {
    // 集合c判断操作
    Objects.requireNonNull(c);
    // 调用真实的删除方法(该方法与 retainAll 其实共用一个逻辑,关键在于参数2:complement的取值是false还是true)
    return batchRemove(c, false);
}

/**
 * 真实执行删除的方法
 * 批量移除操作
 * @param complement 补充信息,这个方法在removeAll和retainAll中都被使用到,该字段用以确认是remove还是retain
 * 当complement为false的时候,用以删除 当complement为true的时候,用以保留
 */
private boolean batchRemove(Collection<?> c, boolean complement) {
    // 定义一个引用变量指向elementData
    final Object[] elementData = this.elementData;
    // r是简单的遍历参数,w是重新填充数组元素的index参数
    int r = 0, w = 0;
    // 是否被修改
    boolean modified = false;
    try {
        for (; r < size; r++) {
            // removeAll是false,retainAll的是true
            // 假设原为 1 2 3 4 5 6,想删除 1 3 5,那么当c.contains(elementData[r]) == true的时候,我不希望它被赋值到新的数组中的,
            // 所以会设置成false,并且他们的相对位置是不变的
            if (c.contains(elementData[r]) == complement) {
                // 相当于是把新的元素从头开始放置
                // elementData[w] = elementData[r];  w++;
                elementData[w++] = elementData[r];
            }
        }
    } finally {
        // 什么时候会进入到这里呢?大概是try的过程中发生了异常
        if (r != size) {
            System.arraycopy(elementData, r, elementData, w, size - r);
            w += size - r;
        }
        // w与size不相等,说明是removeAll
        if (w != size) {
            // 清空数组后面的无用index的元素,帮助GC
            for (int i = w; i < size; i++) {
                elementData[i] = null;
            }
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

代码看起来可能会有点长,但是逻辑还是比较的清晰的,看看代码注释应该问题不大。

删除操作的源码讲解就到这里,主要的流程大体上是:
1. 找到需要删除的元素所在的下标 index,执行删除操作。
2. 下标 index 后的元素向前移动,并且更新 size 等变量。

2.6. 查找元素

public int indexOf(Object o) 方法,查找 首个 为指定元素 o 的位置,源码如下:

/**
 * 查找首个为指定元素 o 的位置
 * @param o 被查找元素
 * @return int 如果存在则返回对应index 不存在返回-1
 */
@Override
public int indexOf(Object o) {
    // 从这里我们其实也可以看出,ArrayList是支持存储null值的
    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;
            }
        }
    }
    // 不存在则返回 -1
    return -1;
}

代码就是单纯的遍历数组匹配元素返回下标 index,需要额外注意的是:我们可以看出 ArrayList 是支持存储 null 值的。

可能还有小伙伴会用到 lastIndexOf(Object o) 方法,用以查找最后一个为指定元素的位置;代码其实也很相似,只不过是从数组的后面开始遍历罢了。

/**
 * 查找最后一个为指定元素的下标位置
 * @param o
 * @return int 下标位置,不存在则返回-1
 */
@Override
public int lastIndexOf(Object o) {
    // 最后一次出现元素o的下标,做法很简单,反过来遍历第一个出现就是了
    if (o == null) {
        for (int i = size-1; i >= 0; i--) {
            if (elementData[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = size-1; i >= 0; i--) {
            if (o.equals(elementData[i])) {
                return i;
            }
        }
    }
    // 不存在则返回 -1
    return -1;
}

2.7. 转换成数组

转换为数组的方法有2个,分别如下:
public Object[] toArray() 将 ArrayList 转换为 Object[] 数组。
public <T> T[] toArray(T[] a) 将 ArrayList 转换为指定 T 泛型的数组。

/**
 * 将ArrayList转换成Object类型数组
 * @return Object[]
 */
@Override
public Object[] toArray() {
    // 返回的是 Object[] 类型,需要注意:转换成数组就相当于是将 ArrayList 的底层elementData暴露出去
    return Arrays.copyOf(elementData, size);
}


public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

我们在调用 toArray() 的时候,可能会遇到异常 java.lang.ClassCastException;这是因为 toArray()方法返回的类型是 Obejct[],如果我们将其转换成其他类型,可能就会抛出异常。 这是因为 Java并不支持向下转型,所以当我们需要将 ArrayList转换成数组并且指定类型的时候,应该使用下面的这个方法。

真实的场景中,我们其实更希望返回指定泛型的数组,所以来瞅瞅 public <T> T[] toArray(T[] a) 是怎么做的,并且看看有哪些地方是需要留意的。

/**
 * 将ArrayList转换成指定泛型的数组
 * @param a
 * @return T[]
 */
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    // <1>如果传入的数组小于 size 的大小,直接拷贝一个新的数组返回
    if (a.length < size) {
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    }
    // <2>否则直接拷贝数组即可
    System.arraycopy(elementData, 0, a, 0, size);
    // <3>如果传入的数组大于 size 的大小,则后面都置为null
    if (a.length > size) {
        a[size] = null;
    }
    // 返回传入的a,但是考虑到<1>的时候会返回新的数组,所以即使<2>返回a数组,最好还是按照 a = list.toArray(a); 来使用
    return a;
}

我们可以看到,当传入的数组a的长度小于 ArrayList 集合的元素的长度时候,直接拷贝生成一个新的数组返回;否则将 elementData 复制到 a 数组中。

考虑到<1>处有可能会返回一个新的数组,所以即使 <2>返回的就是 a数组;使用的时候最好还是按照 a = list.toArray(a)。

2.8. 其他常见方法

contains(Object o) 方法,判断集合中是否包含某个元素。
get(int index) 方法,获取指定位置的元素。
set(int index, E element) 方法,设置指定位置的元素。
clear() 方法,清空数组。
subList(int fromIndex, int toIndex) 方法,创建子数组。

/**
 * 判断集合中是否含有元素 o
 * 可以看出,contains方法其实就是依赖的 indexOf方法
 * @param o
 * @return boolean
 */
@Override
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

/**
 * 获取指定index位的元素
 * @param index
 * @return E
 */
@Override
public E get(int index) {
    // 参数检查
    rangeCheck(index);

    // 相当于直接返回 elementData[index];
    return elementData(index);
}

/**
 * 设置指定index位置的元素,并且返回该index位旧元素值
 * 并返回旧元素的值(相当于替换)
 * @param index
 * @param element
 * @return E
 */
@Override
public E set(int index, E element) {
    // 参数检查
    rangeCheck(index);
    // 数据的简单操作
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}


/**
 * 清空集合
 */
@Override
public void clear() {
    // 数组操作次数 + 1
    modCount++;
    // 遍历数组,全部设置为null,同时更新size值
    for (int i = 0; i < size; i++) {
        elementData[i] = null;
    }

    size = 0;
}


 /**
  * 创建子数组
  * 注意: subList并不是只读数组,而是和父数组共享相同的 elementData 的数组,换句话说,对subList的操作会影响到 父数组
  * 只不过是fromIndex 和 toIndex限制了查看的范围
  * @param fromIndex 开始下标
  * @param toIndex 结束下标
  *                是一个 [fromIndex, toIndex)的效果
  */
public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

2.9. 遍历方式

通过 Interator 迭代器遍历

Integer value = null;
Iterator it = list.iterator();
while (it.hasNext()) {
    value = (Integer) it.next();
}

随机访问,通过索引值遍历,由于 ArrayList 实现了 RandomAccess 接口,所以它支持通过索引值去随机访问元素

Integer value = null;
int size = list.size();
for (int i = 0; i < size; i++) {
    value = (Integer) list.get(i);
}

for循环遍历

Integer value = null;
for (Integer i: list) {
    value = i;
}

测试比较三者的效率,得出在遍历 ArrayLis t的时候,使用随机访问(即通过索引访问)的效率最高,而使用迭代器的效率最低。

三. Fail-Fast机制

ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1110760.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通过内网穿透远程控制家中Home Assistant智能家居系统

文章目录 前言1. 安装Home Assistant2. 配置Home Assistant3. 安装cpolar内网穿透3.1 windows系统3.2 Linux系统3.3 macOS系统 4. 映射Home Assistant端口5. 公网访问Home Assistant6. 固定公网地址6.1 保留一个固定二级子域名6.2 配置固定二级子域名 前言 Home Assistant&…

【纠错】遗传算法求解VRP计算车辆容量限制的代码有bug

关联文章 关联的博客文章为&#xff1a;《遗传算法求解带时间窗的VRP问题&#xff08;python&#xff09;》 原出错函数 源程序代码如下&#xff1a; def vehicle_capacity_restraint(chrom):# 计算一条染色体的车辆容量限制individual copy.deepcopy(chrom)split_flag_no…

Simian使用方法

1.下载 链接1&#xff1a;官网下载 链接2&#xff1a;压缩包 2.操作 1.双击exe启动 2.打开控制台&#xff0c;winR 输入cmd 3.输入操作语句 G:\1111\simian-2.5.10\bin\simian-2.5.10.exe -includes"G:\1111\test\*.cpp" -threshold3 > output.txt G:\1111\si…

Vuex中多个参数显示undefined的解决方案

笔者今天在使用Vuex中的mutations改变state里面的全局状态的值&#xff0c;获取到的数据却怎么都是第一个参数是可以获取到,但是第二个就获取不到&#xff0c;就显示undefined 问题代码 mutations: {multiparameter(state,id,newStatus) {console.log("数据的Key&#x…

【Java】java中的集合框架组成部分

集合框架中 单列与多列的区别 单列——一行只有一列数据 多列(双列)——一行两列数据 key value 集合框架组成部分&#xff1a; 1. Collection(存放单列数据) List 接口——存放数据可以允许重复的 ArrayList 底层基于 数组 数据结构实现LinkedList 底层基于 链表 数据结构实现…

seq“ 和 “time“ 字段

在RTP&#xff08;Real-time Transport Protocol&#xff09;报文中&#xff0c;"seq" 和 "time" 字段分别表示以下内容&#xff1a; 1. **Seq&#xff08;Sequence Number&#xff09;字段**&#xff1a;Seq字段是RTP头部中的一个16位字段&#xff0c;用…

Python---练习:判断是否为一个合法三角形(if else)

案例 判断是否为一个合法三角形 需求&#xff1a;输入三角形的3边&#xff0c;如果两边的长度大于第三条边&#xff0c;则代表是一个合法三角形 思路&#xff1a; 先确定什么是一个合法三角形-----就是任意两边的和&#xff0c;大于第三边。 就像下图&#xff0c;a b 展…

分享一个基于Python+Django的高校食堂外卖点餐系统的设计实现(源码、调试、开题、lw、ppt)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1f495;&…

Hadoop3教程(二十四):Yarn的常用命令与参数配置实例

文章目录 &#xff08;132&#xff09;YARN常用命令查看任务查看日志查看容器查看节点状态rmadmin更新配置查看队列 &#xff08;133&#xff09;生产环境核心配置参数&#xff08;135&#xff09;生产环境核心参数配置案例&#xff08;140/141&#xff09;Tool接口案例参考文献…

Flink之Window窗口机制

窗口Window机制 窗口概述窗口的分类是否按键分区按键分区窗口非按键分区 按照驱动类型按具体分配规则滚动窗口Tumbling Windows滑动窗口 Sliding Windows会话窗口 Session Windows全局窗口 Global Windows 时间语义窗口分配器 Window Assigners时间窗口计数窗口例子 窗口函数 W…

一篇文章带你弄懂编译和链接

一篇文章带你弄懂编译和链接 文章目录 一篇文章带你弄懂编译和链接一、环境二、翻译环境1.编译①预处理②编译③汇编 2.链接 三、运行环境 一、环境 翻译环境和运行环境 翻译环境&#xff1a;源代码被转换成可执行的机器指令。 运行环境&#xff1a;用于实际执行代码。 二、…

使用 Android Studio 开发一款弹钢琴app

A. 项目描述 本项目主要实现了【钢琴键盘的模拟】、【弹奏引导】以及【乐曲库】等功能。 钢琴键盘模拟&#xff1a;提供全尺寸键盘&#xff0c;并且根据用户的喜好来调整键盘的颜色样式。 弹奏引导&#xff1a;用户可以根据键盘上的提示符号 &#x1f446; 来学习演奏。对于钢…

Win32 简单日志实现

简单实现日志保存, 支持设置日志文件数量, 单个日志文件大小上限, 自动超时保存日志, 日志缓存超限保存 CLogUtils.h #pragma once#include <string> #include <windows.h> #include <vector> #include <map> #include <mutex> #include <tc…

04、MySQL-------MyCat实现分库分表

目录 九、MyCat实现分库分表1、分库分表介绍&#xff1a;横向&#xff08;水平&#xff09;拆分**垂直分表**&#xff1a;水平分表&#xff1a;**分库分表** 纵向&#xff08;垂直&#xff09;拆分分表字段选择 2、分库分表操作&#xff1a;1、分析图&#xff1a;2、克隆主从3、…

【Docker从入门到入土 1】Docker管理命令

Part1 一、容器1.1 虚拟化技术的演变1.2 容器简介1.3 为什么要用到容器&#xff1f;1.4 常用的容器引擎1.5 容器和虚拟机 二、Docker2.1 什么是docker&#xff1f;&#xff08;面试题&#xff09;2.2 Docker和虚拟机的区别&#xff08;面试常问&#xff0c;记住&#xff09;2.3…

SLAM 14 notes

4.23 推导 f ( x ) f(x) f(x)在点a处的泰勒展开 f ( x ) ∑ n 0 ∞ f ( n ) a n ! ( x − a ) n f(x) \sum_{n0}^\infty \frac{f^{(n)}a}{n!}(x-a)^n f(x)∑n0∞​n!f(n)a​(x−a)n l n x lnx lnx的n阶导数 l n ( n ) x ( − 1 ) n − 1 ( n − 1 ) ! x n ln^{(n)}x \fr…

C++进阶语法之函数和指针【学习笔记(二)】

文章目录 1、C 函数1.1 函数的定义1.2 函数原型&#xff08;function prototypes&#xff09;1.3 参数&#xff08;parameter&#xff09;——值传递&#xff08;pass by value&#xff09;1.4 重载&#xff08;overloading&#xff09;1.5 函数传参——传递数组&#xff08;ar…

drawio都能做那些事情和模板示例

drawio都能做那些事情和模板示例 你可以使用drawio&#xff0c;并使用drawio提供的扩展模板库和大量的形状库&#xff0c;针对很多不同的工业领域创建不同类型的图表。 针对如下的内容中的所有的图&#xff0c;均可以下载源文件并导入到drawio中再次编辑&#xff08;供学习者…

如何远程通过内网穿透实现微信公众号在本地的完整调试

文章目录 前言1. 配置本地服务器2. 内网穿透2.1 下载安装cpolar内网穿透2.2 创建隧道 3. 测试公网访问4. 固定域名4.1 保留一个二级子域名4.2 配置二级子域名 5. 使用固定二级子域名进行微信开发 前言 在微信公众号开发中&#xff0c;微信要求开发者需要拥有自己的服务器资源来…

什么是兼容性测试? 重要性是什么?

在数字化时代&#xff0c;用户使用各种不同类型的设备和操作系统来访问应用程序和网站。为了确保用户体验的一致性和应用程序的可用性&#xff0c;兼容性测试变得至关重要。本文将深入探讨兼容性测试的概念及重要性。 一、什么是兼容性测试? 兼容性测试是一种软件测试方法&…