前言
ArrayList
可以说是日常开发中最经常使用到的一个集合了,下面来分析一下它的结构和几个常用方法。
ArrayList
的继承关系如下所示:
需要注意的ArrayList
实现了RandomAccess
这个接口,其源码如下:
public interface RandomAccess {
}
可以看到这个接口里并没有任何属性或者方法,那为什么还有实现呢?主要是为了做一个标记,用来表明实现该接口的类支持快速(通常是固定时间)随机访问。此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
整体结构
ArrayList
底层其实是一个数组,比较简单,如下所示:
ArrayList
的特点如下:
- 允许
null
值; - 有序,可根据索引快速访问;
- 非线程安全;
- 快速失败,若是在遍历过程中,对集合进行结构性修改(增,删),会导致快速失败(
fast-fial
),关于fast-fail
和fast-safe
相关知识可以这里;
成员变量
//默认容量
private static final int DEFAULT_CAPACITY = 10;
//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认空数组 无参构造时用到
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//元素数组 真正用于存储元素
transient Object[] elementData;
//数组最大长度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//元素个数
private int size;
构造方法
//无参构造
public ArrayList() {
// 默认使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA 此时大小为0
this.elementData = DEFAULTDEFAULTCAPACITY_EMPTY_ELEMENTDATACAPACITY_EMPTY_ELEMENTDATA;
}
//带有 capacity入参的构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//等于0是 使用EMPTY_ELEMENTDATA 此时大小为0
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//集合作为入参的构造方法
public ArrayList(Collection<? extends E> c) {
//elementData 是保存数组的容器,默认为 null
elementData = c.toArray();
//如果给定的集合(c)数据有值
if ((size = elementData.length) != 0) {
//如果集合元素类型不是 Object 类型,我们会转成 Object
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 给定集合(c)无值,则默认空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
关于这几个构造函数需要注意的点:
ArrayList
无参构造初始化时,默认大小数组是空数组,而不是常说的10,ArrayList
也是懒加载,只有真正存放数据时才会去进行扩容;- 在使用集合作为入参的构造函数时,,当给定集合内的元素不是
Object
类型时,默认会将其转换为Object
类型;
常用方法
add(E e)
public boolean add(E e) {
//检测目前的数组是否还能继续存放元素,不够执行扩容,size 为当前数组的大小
ensureCapacityInternal(size + 1);
//直接赋值,线程不安全的
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 调用无参构造时进入该if分支
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//这也说明了,当使用无参构造时,第一次add时,数组会长度会由0增加到10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//确保容积足够
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 更新modCount
modCount++;
//如果期望的最小容量大于目前数组的长度,那么就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容,并把现有数据拷贝到新的数组里面去
private void grow(int minCapacity) {
//当前数组的长度
int oldCapacity = elementData.length;
//新数组长度为1.5倍的旧长度
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新数组长度小于期望的最小容量 那么新数组长度直接设为期望容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新数组长度大于最大允许的数组长度 新数组容量设为Integer的最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 通过复制进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
//private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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)
的逻辑还是比较清晰和简单的:
- 先判断是否需要扩容,需要的话就进行扩容,否则直接赋值即可;
- 扩容时,新数组的长度为旧数组长度的1.5倍;
- 扩容时调用的
Arrays#copyOf
方法其底层最后调用的是一个native
方法,System#arraycopy
; ArrayList
中的数组的最大值是Integer.MAX_VALUE
,超过该值时JVM
不会再继续分配内存空间了;- 新增时,没有对入参做校验,因而
ArrayList
允许为null
;
add(int index, E e)
public void add(int index, E element) {
//检查下标是否越界
rangeCheckForAdd(index);
//检查是否需要扩容
ensureCapacityInternal(size + 1);
// 数组元素位置调整
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//索引位置处涉及值
elementData[index] = element;
//更新size
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
没啥好分析的,看一下注释就行。
add(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c) {
//转为Object数组
Object[] a = c.toArray();
//获取集合长度
int numNew = a.length;
// 检查是否需要进行扩容
ensureCapacityInternal(size + numNew);
//将c添加到数组中
System.arraycopy(a, 0, elementData, size, numNew);
//更新size
size += numNew;
return numNew != 0;
}
get(int index)
public E get(int index) {
//检测下标是否越界
rangeCheck(index);
//直接返回索引位置处的数据
return elementData(index);
}
remove(int index)
public E remove(int index) {
//检测下标是否越界
rangeCheck(index);
// 更新modCount
modCount++;
//索引位置处的值
E oldValue = elementData(index);
// numMoved 表示删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去
// 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起
int numMoved = size - index - 1;
if (numMoved > 0)
// 从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMoved
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//置为null 帮助gc
elementData[--size] = null; // clear to let GC do its work
// 返回旧值
return oldValue;
}
remove(Object o)
public boolean remove(Object o) {
// 如果o为null
if (o == null) {
// 正序遍历,将第一个null对象移除
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 否则的话遍历数组进行查找,然后删除
for (int index = 0; index < size; index++)
// 这里是根据 equals 来判断值相等的,相等后再根据索引位置进行删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// 可以看出没有返回值的 remove(int index)
private void fastRemove(int index) {
modCount++;
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
}
trimToSize()
public void trimToSize() {
// 更新modCount
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
ArrayList
中,一般情况下数组长度elementData.length
都是大于等于数组中实际元素个数的size
的,所以可以通过trimToSize
方法将这个数组转变为数组长度和元素个数一样的数组,减少内存的占用,如下所示:
set(int index,E e)
public E set(int index, E element) {
//下标校验
rangeCheck(index);
//获取旧值
E oldValue = elementData(index);
//直接设置
elementData[index] = element;
return oldValue;
}
subList(int fromIndex,int toIndex)
/**
* 返回集合中指定 [fromIndex, toIndex) 位置元素构成的集合
* 如果 fromIndex == toIndex,返回空集合
*/
public List<E> subList(int fromIndex, int toIndex) {
//下标校验
subListRangeCheck(fromIndex, toIndex, size);
//返回SubList
return new SubList(this, 0, fromIndex, toIndex);
}
/*
* 检测子集的下标是否越界
*/
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size)
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
}
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
..........
}
返回集合中指定的 [fromIndex
, toIndex
) 位置之间的集合。 如果 fromIndex == toIndex
,则返回集合为空。
iterator() && listIterator()
/**
* 以正确的顺序返回此集合中的所有元素的迭代器
* 返回的迭代器为 fast-fail
*/
public Iterator<E> iterator() {
return new Itr();
}
/**
* 返回此集合中的所有元素的 list 迭代器
* 返回的 list 迭代器为 fast-fail
*/
public ListIterator<E> listIterator() {
return new ListItr(0);
}
通过这两个方法可以获取一个迭代器用于遍历集合。
迭代器 Iterator
在使用ArrayList
时我们经常需要遍历集合来完成某些操作,通常有两种方式,一种是直接使用foreach
,一种是使用Iterator
接口,但两种本质上是一样的,foreach
底层实现还是Iterator
接口。
Itr
Itr
是ArryList
中的一个私有内部类,实现了Iterator
接口用以实现遍历。
成员变量
int cursor; // 迭代过程中,下一个元素的位置,默认从 0 开始
int lastRet = -1; // 新增场景:表示上一次迭代过程中,索引的位置;删除时置为 -1
int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;
//modCount 表示数组实际的版本号
主要方法
迭代器主要就三个方法:
hasNext
:是否还有下一个元素;next
:下一个元素值;remove
:删除当前迭代的值;
hasNext()
public boolean hasNext() {
//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果
//不等,说明还可以迭代
return cursor != size;
}
next()
@SuppressWarnings("unchecked")
public E next() {
//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
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;
// 返回元素值 同时将lastRet设置为 i
return (E) elementData[lastRet = i];
}
//检测 ArrayList 中的 modCount 和当前迭代器对象的 expectedModCount 是否一致
// 不等的话直接抛出异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
next
方法做了两件事:
- 是否还能继续迭代;
- 定位到当前迭代的值,并为下一次迭代做好准备;
remove()
public void remove() {
// 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了
if (lastRet < 0)
throw new IllegalStateException();
//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
// -1 表示元素已经被删除,这里也防止重复删除
lastRet = -1;
// 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
// 这样下次迭代时,两者的值是一致的了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
需要注意的点是:
lastRet
= -1 的操作目的,是防止重复删除操作;- 删除元素成功,数组当前
modCount
就会发生变化,这里会把expectedModCount
更新为modCount
的值,下次迭代时两者的值就会一致了;
ListItr
ListItr
是Itr
的子类,除了基本的三个方法之外,还额外实现了一些其他的方法。我们都知道,在遍历集合时,若是对集合结构进行修改则会触发fast-fail
机制,但若是我们的确有这个需求,一边遍历一边修改集合的结构,那怎么办呢?ListItr
中提供了add
这个方法。
add(E e)
public void add(E e) {
//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
checkForComodification();
try {
//获取当前遍历到的位置
int i = cursor;
//插入到数组中
ArrayList.this.add(i, e);
//再向下移动一个位置 因而本次遍历过程中访问不到这个元素 只能在下一次遍历中才能访问
cursor = i + 1;
//更新lastRet
lastRet = -1;
// 添加元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
// 这样下次迭代时,两者的值是一致的了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
使用add
这个方法在遍历时添加时,本次遍历是访问不到添加的那个元素的,只能在下一次遍历时才能访问,保证了本次遍历的正确性,也防止出现快速失败。
其他方法
ListItr
还实现了ListIterator
这个接口,因而还可以向前遍历,这里只给出源码,也很简单,看一下就行。
//判断是否还有前一个元素
public boolean hasPrevious() {
return cursor != 0;
}
//下个元素的索引
public int nextIndex() {
return cursor;
}
//前一个元素的索引
public int previousIndex() {
return cursor - 1;
}
//迭代时,前一个元素的值
@SuppressWarnings("unchecked")
public E previous() {
//校验modCount
checkForComodification();
//前一个位置索引
int i = cursor - 1;
if (i < 0)//参数校验
throw new NoSuchElementException();
//实际存储数据的数组
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)//参数校验
throw new ConcurrentModificationException();
//更新cursor
cursor = i;
//返回当前迭代的值,并设置 lastRet
return (E) elementData[lastRet = i];
}
总结
ArrayList
的特点总结如下:
-
底层是动态数组,默认大小为10,扩容时每次都是当前数组长度的1.5倍;
-
使用无参构造或者
capacity=0
的有参构造时,ELEMENTDATA
都是空数组,只有在第一次添加元素时才去扩容; -
扩容主要方法为
grow(int minCapacity)
,不支持缩容; -
允许元素为
null
; -
实现
RandomAccess
接口,支持随机访问,平均时间复杂度为O(1)
; -
增加和删除元素过程中,效率低下,平均时间复杂度为
O(n)
;