目录
一. 前言
二. 源码解析
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 extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
{
// ....
}
在进行源码解析前,我们先预览一遍 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 iterator()
boolean remove(Object object)
boolean removeAll(Collection> collection)
boolean retainAll(Collection> collection)
int size()
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 listIterator(int location)
ListIterator listIterator()
E remove(int location)
E set(int location, E object)
List subList(int start, int end)
// ArrayList新增的API
Object clone()
void ensureCapacity(int minimumCapacity)
void trimToSize()
void removeRange(int fromIndex, int toIndex)
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;
ArrayList一共有3个构造方法,分别如下所示。
无参构造方法,平时使用的时候,如果你的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开始;两者的起点不同。
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 = {} 是一个空数组。在添加元素的时候,会进行扩容创建需要的数组。
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;
}
}
新增元素的方法主要有如下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位置,添加多个元素。
继承于 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 数组是否支持本次新增操作(容量是否足够),当容量不足的时候则触发扩容机制,将原有数组的元素拷贝到新数组上,然后往后追加新的元素。
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);
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个元素变成了添加多个元素罢了。
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 个位置。
删除元素的方法主要有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) 批量移除指定的多个元素。
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 并不是那么适合于删除特别频繁的场景。
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 的元素的。
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;后面的代码比较简单可直接看注释。
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 等变量。
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个,分别如下:
public Object[] toArray() 将 ArrayList 转换为 Object[] 数组。
public
/**
* 将ArrayList转换成Object类型数组
* @return Object[]
*/
@Override
public Object[] toArray() {
// 返回的是 Object[] 类型,需要注意:转换成数组就相当于是将 ArrayList 的底层elementData暴露出去
return Arrays.copyOf(elementData, size);
}
public static T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
我们在调用 toArray() 的时候,可能会遇到异常 java.lang.ClassCastException;这是因为 toArray()方法返回的类型是 Obejct[],如果我们将其转换成其他类型,可能就会抛出异常。 这是因为 Java并不支持向下转型,所以当我们需要将 ArrayList转换成数组并且指定类型的时候,应该使用下面的这个方法。
真实的场景中,我们其实更希望返回指定泛型的数组,所以来瞅瞅 public
/**
* 将ArrayList转换成指定泛型的数组
* @param a
* @return T[]
*/
@Override
@SuppressWarnings("unchecked")
public 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)。
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 subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
通过 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的时候,使用随机访问(即通过索引访问)的效率最高,而使用迭代器的效率最低。
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。