概述
ArrayList
是基于数组实现的,并且支持 动态扩容
的动态数组。相比于数组而言,因为其支持 自动扩容
的特性,成为我们在开发中最常用的集合类之一。
本篇的讲解是基于 JDK1.8
,话不多说, 就让我们翻开 ArrayList
的源代码,遨游一番吧~
Let's GO !!!
类图
从类图中我们可以看出,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
{
// ....
}
源码解析
在进行源码解析前,我们先全览一篇 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)
属性
ArrayList
的属性很少,只有2个属性: elementData
和 size
;其中 elementData
代表的是存放元素的数组;而 size
代表的是 elementData
数组中元素的数量。比如下图所示: elementData
数组的长度是8,但是只存放了4个元素,这时候 size
属性就是4。
我们在使用 ArrayList
的时候会经常调用其 size()
方法,返回的就是 elementData
数组中 已使用 的元素的数量,也即是当前的 4;并且当我们新添加元素的时候,size
所指代的也恰巧是新元素的下标(数组下标从0开始)。
/**
* 存放元素的数组
* 在新增元素的时候,如果遇到数组不够会创建新的数组,并且将原数组的元素拷贝到新数据;最后将该应用变量指向新数组
*/
transient Object[] elementData;
/**
* 数组的大小
* size代表的是已使用elementData的元素的数量,也即是size()方法的大小
*/
private int size;
构造方法
ArrayList
一共有3个构造方法,分别如下所示。
1. ArrayList(int initialCapacity)
ArrayList(int initialCapacity)
构造方法,根据传入的 initialCapacity
初始化容量来创建 elementData
数组。如果我们在使用 ArrayList
的时候,预先知道元素的数量,应该尽可能的使用该构造方法,这样可避免数据扩容,从而提升性能,同时也是合理的利用内存空间(一点不浪费~)。
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. ArryaList()
无参构造方法,平时使用的时候,如果你的IDEA有安装 sonarLint
扫描,会被提示:创建集合的时候要指定初始化容量噢~
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开始;两者的起点不同。
3. ArrayList(Collection<? extends E> c)
ArrayList(Collection<? extends E> c)
构造方法,使用传入的集合 c 作为 ArrayList的 elementData
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;
}
}
当elementData
数组不空的时候,有一个类型转换的过程,因为 elementData
是 Object[]
类型的数组。
新增元素
新增元素的方法主要有如下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
位置,添加多个元素
public boolean 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
数组是否支持本次新增操作(容量是否足够),当容量不足的时候则触发扩容机制,将原有数组的元素拷贝到新数组上,然后往后追加新的元素。
接下来我们再来看看剩下的几个新增元素的方法
public void 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);
public boolean 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个元素变成了添加多个元素罢了~
public boolean 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
个位置。
删除元素
删除元素的方法主要有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)
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 --;
// 不建议开发中这么写,写这样是想坑谁呢 emmm 代码还是要以可读性为准
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)
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)
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)
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;
}
代码看起来可能会有点长,但是逻辑还是比较的清晰的,各位朋友看看代码注释应该问题不大~~
删除操作的源码讲解就到这里,总体来说还是比较的简单的;主要的流程大体上是:
-
找到需要删除的元素所在的下标
index
,执行删除操作。 -
下标
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 <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);
// 额 这个的目的是? 有点没看懂
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<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
遍历方式探讨
ArrayList
支持三种遍历方式,下面逐个讨论:
- 通过
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 integ : list) {
value = integ;
}
测试比较三者的效率,得出在遍历 ArrayList
的时候,使用随机访问(即通过索引访问)的效率最高,而使用迭代器的效率最低
总结
本文主要讲解了 ArrayList
的属性、构造方法、核心方法。
ArrayList
只有2个属性,分别为 elementData
用以存储元素和 size
代表存储元素的数量。
ArrayList
有3个构造方法,分别是无参构造方法 public ArrayList()
、指定初始化常量的构造方法 public ArrayList(int initialCapacity)
、指定元素集的构造方法 ArrayList(Collection<? extends E> c)
;需要特别留意的是:无参构造方法首次初始化的容量是0,只有在第一次添加元素的时候,才会扩容成容量为10的数组。
核心方法主要介绍了几类,分别为:新增元素、删除元素、查询元素、以及一些常用的方法。
新增元素的时候,由于 ArrayList
是支持动态扩容的动态数组,所以新增元素的时候会确保数组的剩余容量是否支持本次新增元素操作,如果不满足则会触发扩容机制,扩容一般是按照原来1.5倍。
我们在删除元素相关的源码中可以看出,ArrayList
在每次删除元素都,都会伴随着大量的移动元素的操作,所以从中我们也可以看出 ArrayList
并不是那么适用于新增、删除比较频繁的场景。
查询元素和一些比较常见的方法,代码都比较简单;相信朋友们可以直接读懂。
另外需要注意的是:我们在将 ArrayList
转换成数组的时候,调用 toArray()
方法有可能会抛出异常java.lang.ClassCastException
,这是因为该方法返回的类型是 Object[]
,不支持向下转换类型,所以当我们需要返回指定泛型的数组时,可以使用T[] toArray(T[] a)
方法。
最后需要留意的是 ArrayList
是支持存储 null
的。