ArrayList是在Java中最常用的集合之一,其本质上可以当做是一个可扩容的数组,可以添加重复的数据,也支持随机访问
ArrayList的类定义
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
其中,RandomAccess、Cloneable、Serializable三个接口都属于标记接口,没有任何需要手动实现的方法
有人以为clone()方法来自于Cloneable接口,实际上clone()方法来自于Object类
- RandomAccess接口表示ArrayList支持随机访问,即可以直接访问集合内任意位置的元素
- Cloneable接口表示ArrayList支持克隆,并重写了克隆方法
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
从上方代码第四行出可以看出,调用clone()方法时,也重新创建了一个新的数组作为集合数据的容器,新旧两个集合拥有各自的数据存储的内容空间,互不影响
- Serializable接口表示ArrayList可序列化
除此之外, 父类AbstractList也提供了部分通用方法的实现。但是这些方法有很多都进行了重写,所以本文不会过多关注AbstractList类。但是它提供了一个非常重要的成员变量:modCount,表示集合长度被修改的次数,在遍历集合段落中会详细讲解它的作用
ArrayList 的成员变量分析
ArrayList的成员变量包含
// 序列化ID
private static final long serialVersionUID = 8683452581122892189L;
// 集合的默认容量
private static final int DEFAULT_CAPACITY = 10;
// 用于初始化elementData的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量下用于初始化elementData的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存放实际元素的数组
transient Object[] elementData;
// 集合的大小
private int size;
// 集合的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
根据源码,我们可以得到两个比较直观的信息:
- 集合的默认容量是10,即调用无参构造器生成的ArrayList,在不扩容的情况下,最多能够存储10个数据
- 集合的本质是一个类型为Object的数组
要有一个值得注意的地方,成员变量中包含两个用于初始化elementData的空数组:EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA
在实例化ArrayList的时候,如果指定capacity(容量)为0,则使用EMPTY_ELEMENTDATA来初始化elementData;没有指定capacity,也就是使用默认容量capacity = 10的情况下,则使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA来初始化elementData
为什么要根据不同的情况使用不同的空接口。因为ArrayList要根据不同的情况进行扩容。通过new ArrayList(0)构造的集合,其容量就是0;但是通过new ArrayList()构造的集合,虽然初始化后elementData也是一个空数组,但是在添加第一个元素后,其会立即扩容为一个容量为10(默认capacity)的数组。这是两种不同的处理方式,所以要区分不同的空数组对象。在后续的构造函数和添加元素段落中还会继续讲到
最后,为什么集合的最大容量是Integer.MAX_VALUE — 8?为什么要节省这8个容量?因为有一些虚拟机会在数组中保留一些头信息或是描述信息,尝试分配更大的数组可能会导致OutOfMemoryError
构造器分析
ArrayList拥有三个构造器,它们本质上都是在对内部的elementData进行初始化操作
- 无参构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这是最简单的构造器,仅仅只是将elementData指向常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,以便于在添加第一个元素时,扩容至默认容量capacity = 10的大小
- 显式设置初始化容量的构造器
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
这个构造器首先需要保证手动指定的容量大于0,否则会抛出IllegalArgumentException异常;其次,判断手动指定的容量是否等于0,如果是,则直接将elementData指向静态常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,如果不是,则创建一个跟指定容量相同大小的Object类型的数组作为elementData即可
- 基于另一个集合创建ArrayList的构造器
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
这个构造器相对比较复杂。首先,将参数集合转换为数组对象。接下来,判断新数组对象是否包含元素,对应源码的第3行。如果没有元素,则直接将elementData指向静态常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA。如果数组对象包含元素,则要判定数组对象的类型是否属于Object数组类型(由于不同集合有不同的toArray方法的实现,所以toArray方法不一定返回的是Object类型的数组)。在类型不属于Object数组类型时,根据当前的数组对象的大小和实际元素,复制一个新的Object类型的数组作为elementData
重要方法
添加元素
在开始分析添加元素的方法之前,我们需要先来分析几个工具方法
- 确保集合内容容量充足的前置方法
/**
* 确保集合内容容量充足的前置方法
* @Param minCapacity 调用者指定的最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
在添加元素时,ArrayList会判断当前容量是否充足,在不充足的情况下进行扩容。ensureCapacityInternal(int)方法只是用来获取到当前所需要的最小容量。正如源码所展示的,当前所需要的容量并不一定是方法调用者传入的minCapacity的值,因为当集合通过无参构造器创建时,即elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,它的容量至少要达到10(默认容量的大小:DEFAULT_CAPACITY)。
拿到最小需要的容量后,判断当前集合是否满足条件,到底需不需要扩容,则交给了ensureExplicitCapacity(int)方法来处理
- 判断集合是否需要扩容的方法
/**
* 判断当前集合是否需要扩容的方法
* @Param minCapacity 实际需要的最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
之前提到了ArrayList从AbstractList父类中继承了一个非常重要的字段:modCount,这个字段表示的是集合内部数组对象长度可能被修改的次数,在所有有可能改变数组对象长度的方法中,modCount都会进行自增操作,用于保证集合迭代时不会因为集合的长度发生改变而出现奇怪的错误,其具体的用法会在后续遍历元素段落中详细分析
只要实际需要的最小容量大于当前集合的容量,就需要扩容。具体扩容的操作交给grow(int)来处理
- 对集合进行扩容的方法
/**
* 实际需要的最小容量
* @Param minCapacity 实际需要的最小容量
*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
对集合进行扩容,我们需要知道扩容后的容量是多少?虽然当前需要的最小容量是minCapacity,但是如果minCapacity小于原始容量的1.5倍,则ArrayList认为这个minCapacity的容量也是不安全的,很有可能会进行第二次扩容。为了减少扩容带来的消耗,此时ArrayList认为扩容后的容量应该为原始容量的1.5倍
不管是选择minCapacity还是原始容量的1.5倍作为扩容后的容量,都会存在一种特殊情况:扩容后的容量大于了集合允许的最大容量(MAX_ARRAY_SIZE),这种情况应该怎么处理?
前文说到,集合的最大容量为Integer.MAX_VALUE — 8,这节省的8个容量是用于某些虚拟机在数组中保存一些头信息或描述信息。如果集合的容量实在不够,那只能把原本节省下来的8个容量拿来使用,这也意味着,在某些虚拟机环境下,这样的操作会导致OutOfMemoryError错误。
这个操作是由hugeCapacity(int)方法来完成的,由于这个方法非常简单,在这里就不对其做单独的分析了
确定扩容后的容量后,根据这个容量创建一个新的数组,并把原始数据的数据拷贝过来,集合的扩容也就完成了
到这里可以看出,扩容本身就是一种低效率的操作(除了要开辟新的内存空间外,还得把数据一个一个的复制到新的空间中),并且随着原始数据增加,操作的速度也会越来越慢(copy10个数据绝对比copy1个数据慢),所以最好能在构造ArrayList时就预估好容量的大小,避免扩容带来的开销
现在我们再来分析添加元素的方法就比较简单了
// 在集合的末尾添加一个元素
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
// 在集合的指定位置,添加一个元素
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 + 1容量的数据。如果达不到要求就开始扩容。其次,我们发现在指定位置添加元素是一件非常麻烦的事情,因为从指定位置开始到集合末尾的数据,都得往后移动一位。所以在使用ArrayList的时候,尽量不要在集合中的某一个位置添加元素。如果确实存在这种需求,那么可以考虑使用LinkedList而不是ArrayList(后续我们也会对LinkedList的源码进行分析)
这里有个很简单的方法rangeCheckForAdd(int),其用来判断参数index是否合法,不对其做单独的分析
获取元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
判断指定坐标是否越界(依赖于非常简单的rangeCheck(int)方法),然后直接从内部数据中获取数据
删除元素
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null;
return oldValue;
}
删除方法遇到了跟add(int, E)方法同样的问题,即要把指定位置开始至整个集合末尾的所有元素向前移动一位
到此可以看出,对于ArrayList来说,添加元素、删除元素都不是那么便捷的事情,ArrayList的优势还是在于能够快速的访问元素
由于现在较操作前少了一个元素,所以在移动元素之后需要把当前集合的最后一位元素的值设置为null
还有一个删除元素的方法
public boolean remove(Object o) {
if (o == null) {
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;
}
这个方法本质上也是查找到指定元素在集合中的位置,再根据这个位置使用fastRemove(int)方法来执行删除操作
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;
}
而fastRemove(int)方法的原理跟remove(int)方法是一致的
注意,删除操作也会改变集合的长度,所以每个删除方法中都有modCount++操作
遍历元素
在JDK1.5之后,通常使用foreach循环(也叫增强for循环)来对集合进行遍历
List<E> list = new ArrayList<>();
for(E e: list) {
System.out.println(e);
}
foreach循环本质上是在调用迭代器
List<E> list = new ArrayList<>();
Iterator<E> iter = list.iterator();
while(iter.hasNext()) {
System.out.println(iter.next());
}
ArrayList调用iterator()方法返还的是一个类型为ArrayList$Itr的迭代器。这个类是ArrayList中的一个私有内部类
private class Itr implements Iterator<E> {
int cursor; // 下一个返回的元素的索引
int lastRet = -1; // 上一次返还的元素的索引,如果没有则为-1
int expectedModCount = modCount; // 预期的集合长度被修改的次数
}
终于要到modCount的用法了
当一个modCount为0(长度没有被修改过)的ArrayList调用Iterator()方法后,我们会得到这样的一个Itr对象
// 伪代码
Itr: {
cursor: 0,
lastRet: -1,
expectedModCount: 0
}
此时我们可以调用hasNext()方法来判断是否还有元素未访问,即是否能够继续迭代
public boolean hasNext() {
return cursor != size;
}
如果上一次访问的元素是集合的最后一个元素,即上一次访问的元素索引为size - 1,那么就不能再继续迭代了。这种情况下,因为cursor属性代表下一次访问元素的索引,所以可以说,cursor的值等于上一次访问元素的索引 + 1,也就是说当cursor = (size - 1) + 1时,就可以表示集合已经迭代完毕。反过来,当cursor != (size -1) + 1时,就表示集合还可以继续迭代
当我们通过hasNext()方法确定集合还能够迭代时,可以通过next()方法取出迭代的元素的值
public E next() {
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;
return (E) elementData[lastRet = i];
}
除去验证的代码,重要的是,我们能够看到,cursor始终指向下一次返回元素的索引,同时,lastRet指向当前返还元素的索引
假设,此时有另一个程序对当前集合中的某一处进行了添加操作(调用了add(int, e)方法),如果对这样的情况放任不管,那么迭代器迭代的数据肯定会有错误。所以,有了一个方法:checkForComodification()来确保迭代时,集合的长度没有发生改变
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
此时,因为集合调用了add(int, e)方法,所以集合的属性modCount = modCount + 1,而迭代器的属性是expectedModCount是在迭代器实例化的时候就已经创建好的,expectedModCount = 0,因此,modCount != expectedModCount,表示迭代器生成之后,集合发生了长度方面的改变,会影响集合的遍历操作,抛出ConcurrentModificationException异常。从AbstractList处诞生的modCount属性,一直到了这里才有了自己的用武之地
但是,有些时候,我们确实需要在遍历的过程中操作数据,比如说:遍历某个集合,把符合某种条件的数据删除掉。对于这样的情况,迭代器提供了一个通过它自己来修改集合元素的方法:remove()
注意,迭代器只提供了对集合元素进行删除操作的方法,没有添加元素的方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 确保集合本身没有被其他方式修改过
checkForComodification();
try {
// 删除上一次返回的元素
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// modCount被修改了,expectedModCount也要跟着修改
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
值得注意的是,执行删除操作后,lastRet的值变为了-1,所以,不能通过迭代器进行连续的删除操作
List<E> list = new ArrayList<>();
Iterator<E> iter = list.iterator();
// 这是错误的做法
iter.next();
iter.remove();
// 连续删除是不支持的
iter.remove();
// 这是正确的做法
iter.next();
iter.remove();
iter.next();
iter.remove();
ArrayList还有两个获取迭代器的方法:listIterator()和listIterator(int),这两个方法返还的都是ArrayList$ListItr类型的迭代器
public ListIterator<E> listIterator() {
return new ListItr(0);
}
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
而ArrayList$ListItr类同样也是ArrayList的私有内部类
private class ListItr extends Itr implements ListIterator<E> {
// 省略了属性和方法……
}
ListItr继承自Itr,是Itr的扩展,实现了从后往前遍历、添加元素、修改元素、返还索引等方法,其思路大致与Itr相同,因此不在此详细分析了
其它方法
ArrayList还有几个常用且有效的方法值得分析
- 修改指定位置元素的值得方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
该方法修改指定位置元素的值,并把修改前的值返回。注意,修改方法并没有modCount++的操作,因为修改集合内某一元素的值不会对集合的长度发生影响,也就影响不到迭代器的操作了
- 清理集合内剩余空间的方法
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
}
}
有时候,我们确定集合内的元素不会再发生变化,而集合还有剩余容量时可以调用这个方法来清理剩余容量。分为两种情况:
- 当集合的尺寸为0的时候,elementData指向常量** EMPTY_ELEMENTDATA**
- 当集合的尺寸不为0的时候,根据当前尺寸创建一个新的数组,复制原数组数据,将新的数组赋值给elementData
注意,这也是一个可能会影响集合长度的方法 ,所以也有modCount++操作
- 手动扩容的方法
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
有些时候我们会在集合实例化之后才确认到具体的容量,而按照默认的扩容方式(每次扩容50%),可能需要多次扩容才能达到预期的容量。前文说过,扩容是一个低效率的操作,为了避免多次执行这样的操作,所以我们可以主动调用ensureCapacity(int)方法来进行扩容
参数minCapacity代表预期的容量。但是对于通过无参构造器创建的ArrayList(也就是说其容量为10),如果minCapacity小于10,则ArrayList依然认为自己的容量应该是10,不会进行任何操作
小结
本文较为详细的分析了ArrayList的源码,也提出了一些使用ArrayList的技巧,希望能够对各位开发者带来帮助。下一步我会继续分享LinkedList的源码分析,请大家多多支持
网友评论