目录
2.3.2. ArrayList(int initialCapacity)
2.3.3. ArrayList(Collection c)
2.4.2. add(int index, E element)
2.4.4. addAll(int index, Collection c)
2.5.3. removeRange(int fromIndex, int toIndex)
2.5.4. removeAll(Collection c)
一. 前言
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参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。