ArrayList
文章目录
1. 简介
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{...//方法体}
ArrayList
是Java集合框架中比较常用的数据结构,ArrayList
继承AbstractList
。实现了List
接口,同时还实现了RandomAccess
,Cloneable
和Serializable
接口。所以ArrayList 是支持快速访问、复制、序列化的
。底层是基于数组
实现容量大小动态变化
,允许null的存在,跟Vector一样都是数组,但是Vector是线程安全的。ArrayList不是线程安全的。
2. 成员变量
ArrayList
底层是基于数组实现容量大小动态变化
的
//默认初始容量大小为 10
private static final int DEFAULT_CAPACITY = 10;
transient Object[] elementData;//存储元素的集合
private int size;//实际元素个数
注意:上面的 size
是指 elementData
中实际有多少个
元素,而 elementData.length 为集合容量
,表示最多可以容纳多少个元素。不设置容量,默认初始容量大小为10
。
protected transient int modCount = 0;
这个变量是定义在 AbstractList
中的。记录对 List
操作的次数。主要使用是在 Iterator
,是防止在迭代的过程中集合被修改。如果使用for循环移除元素会报ConcurrentModificationException
错,所以删除还是老实使用迭代器删除元素。
private static final Object[] EMPTY_ELEMENTDATA = {};//有参构造函数初始化容量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//无参构造函数初始化容量
上面两个变量是用在构造函数里面的,但是大家发现两个都是空的数组,定义两个空的数组有什么用呢?思考一下。
简单来说,就是
第一次添加元素
时知道该elementData
从空的构造函数
还是有参构造函数
被初始化的。以便确认如何扩容。
2. 构造函数
2.1 无参构造函数
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
注意
:当使用无参构造函数时是把 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
赋值给 elementData
,注释是说构造一个容量大小为 10 的空的 list 集合,但构造函数了只是给 elementData 赋值了一个空的数组,其实是在第一次添加元素时容量扩大至 10 的。
2.2 有参构造函数
1.构造一个初始容量大小为 initialCapacity
的 ArrayList
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);
}
}
注意:当我们使用有参构造函数
时,先判断initialCapacity
容量是不是大于0,当 initialCapacity
大于零时初始化一个大小为 initialCapacity
的 object 数组并赋值给 elementData
。等于0时,则是把 EMPTY_ELEMENTDATA
赋值给 elementData
。而小于0时则直接抛出异常IllegalArgumentException
。
2.使用指定 Collection 来构造 ArrayList 的构造函数
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 第一步:将
Collection
集合转为数组并赋值给elementData
。 - 第二步:把
elementData
中元素的个数赋值给size
,并判断size是否等于0.
如果 size 为0,则把 EMPTY_ELEMENTDATA 赋值给 elementData,相当于new ArrayList(0)。,不等于0,执行第三步。 - 第三步:判断 elementData 的 class 类型是否为 Object[],不是的话则做一次转换。调用Arrays.copyOf方法进行类型转换。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
3. 常用方法解析
3.1 add操作
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
由上面的源码我们可以看出,每次添加元素到集合中都会先确认下集合容量大小,然后将size自增1.再集合尾部添加元素。
ensureCapacityInternal()
:函数中判断如果elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,就取DEFAULT_CAPACITY和minCapacity的最大值也就是10。这就是EMPTY_ELEMENTDATA
与DEFAULTCAPACITY_EMPTY_ELEMENTDATA
的区别所在。同时也验证了上面的说法:使用无惨构造函数时是在第一次添加元素时初始化容量为 10 的。ensureExplicitCapacity()
:函数中先对modCount
自增1,记录操作次数,然后如果最小容量minCapacity
大于elementData.length
的长度,则需要对集合进行扩容。因为我们第一次没有设置初始化容量,此时elementData 的长度为零,所以需要使用grow()
函数扩容。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
从grow()
函数源码分析可以得出,默认将扩容至原来容量的 1.5
倍。但是扩容之后也不一定适用,有可能太小,有可能太大。所以才会有下面两个 if 判断。如果1.5倍太小的话,则将我们所需的容量大小赋值给newCapacity(这就是ArrayList初始化容量为10的原因,一般可以自己估算一个合适的长度,可以增加效率),如果1.5倍太大或者我们需要的容量太大,那就直接拿 newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE
来扩容(集合长度最大值就是Integer.MAX_VALUE
)。然后将原数组中的数据复制到大小为 newCapacity 的新数组中,并将新数组赋值给 elementData。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
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;
}
有以上源码可知,add(int index, E element)
,addAll(Collection<? extends E> c)
,addAll(int index, Collection<? extends E> c)
操作是都是先对集合索引检查 ,以确保不会数组越界。然后判断是否需要扩容,最后通过 System.arraycopy()
方法将旧数组元素拷贝至一个新的数组中去。其实底层都是调用的System.arraycopy()
进行数组复制
。
/**
* src - 源数组。
* srcPos - 源数组中的起始位置。
* dest - 目标数组。
* destPos - 目标数据中的起始位置。
* length - 要复制的数组元素的数量。
*/
System.arraycopy(src,srcPos, dest, destPos ,length );
3.2 remov操作
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; // clear to let GC do its work
return oldValue;
}
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;
}
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
}
当我们调用remove(int index)
时:
- 首先会检查
index
是否合法,不合法则抛出IndexOutOfBoundsException
异常。 - 然后在判断要删除的元素是不是
数组最后一个位置
的元素,如果index是最后一个元素那么直接将最后一个位置为
空,然后
size-1即可。如果index
不是最后一个,,就再次调用System.arraycopy()
方法拷贝数组。说白了就是将从 index + 1 开始向后所有的元素都向前挪一个位置。然后将数组的最后一个位置空,size - 1。
当我们调用remove(Object o)
时: - 对o进行是否是null进行分别处理,然后对数组进行遍历,最后在数组中找到第一个与o对应的下标index。然后调用fastRemove(int index)进行删除。仔细观察 fastRemove(int index) 方法和 remove(int index) 方法基本全部相同。
3.3 set操作和get操作
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
修改操作比较简单,可以看此我们先判断index是否合法,然后取出旧值返回给调用者,再把新值赋值给原来的元素。由于 ArrayList 底层是基于数组实现的,所以获取元素就相当简单了,直接调用数组随机访问即可。
3.4 clear操作
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
删除操作就是把遍历数组,然后每一个元素置为null。
3.5 迭代器 iterator
有使用过集合的都知道,在用 for 遍历集合的时候是不可以对集合进行 remove操作的,因为 remove 操作会改变集合的大小。从而容易造成结果不准确甚至数组下标越界,更严重者还会抛出 ConcurrentModificationException。
public static void main(String[] args) {
List<String> list=new ArrayList();
list.add("A");
list.add("B");
list.add("C");
for (String s:list) {
if ("C".equals(s)){
list.remove(s);
}
}
//Foreach等效于迭代器
/*Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String s=iterator.next();
if ("C".equals(s)){
list.remove(s);
}
}*/
}
测试结果:
foreach 遍历等同于 iterator。为什么会出现这样的原因呢。思考一下。
public Iterator<E> iterator() {
return new Itr();
}
从源码中我们可以看到是直接返回一个Itr对象。
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
从源码可以看出,ArrayList
定义了一个内部类 Itr
实现了 Iterator
接口。在 Itr 内部有三个成员变量。 cursor
:代表下一个要访问的元素下标。 lastRet
:代表上一个要访问的元素下标。 expectedModCount
:代表对 ArrayList 修改次数的期望值,初始值为 modCount
。
下面看看 Itr 的三个主要函数:
hasNext()
:如果下一个元素的下标等于集合大小,就证明集合中没有遍历的元素了。next():
首先判断expectedModCount
和modCount
是否相等,不相等直接抛出ConcurrentModificationException
异常。然后对cursor
进行判断,看是否超过集合大小和数组长度,然后将cursor
赋值给lastRet
,并返回下标为lastRet
的元素。最后将cursor
自增 1。开始时,cursor
= 0,lastRet = -1;每调用一次 next 方法, cursor 和 lastRet 都会自增 1。remove ()
:方法首先会判断 lastRet 的值是否小于 0
,然后在检查expectedModCount
和modCount
是否相等。接下来是关键,直接调用ArrayList
的remove ()
方法删除下标为 lastRet 的元素。然后将 lastRet 赋值给 cursor ,将 lastRet 重新赋值为 -1,并将modCount
重新赋值给expectedModCount
。
下面我们一步一步来分析 Itr 的操作。如图一所示,开始时cursor
指向下标为 0 的元素,lastRet
指向下标为 -1 的元素,也就是 null。每调用一次 next,cursor 和lastRet 就分别会自增 1。当 next 返回 “C” 时,cursor 和 lastRet 分别为3和 2 [图二]。
此时调用remove()
,注意是ArrayList
的remove()
,而不是 Itr 的remove()
。最后一位置空,并且 modCount 会自增 1。从 remove 方法可以看出。[图三]。
此时 cursor = 3,size = 2,因为hasNext()
判断为true
,所以循环继续。来到 next ()方法,因为上一步的 remove ()
方法对modCount
做了修改 ,致使expectedModCount
与modCount
不相等,这就是ConcurrentModificationException
异常的原因所在。从例子中也可以看出异常出自 ArrayList 中的内部类 Itr 中的checkForComodification()
方法。
异常如何解决呢?
第一个使用自带的remove()方法解决。
for (int i = 0; i <list.size() ; i++) {
if ("C".equals(list.get(i))){
list.remove(i);
}
}
第二个使用迭代器的remove()方法解决。
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String s=iterator.next();
if ("C".equals(s)){
iterator.remove();
}
}
直接调用 iterator.remove()
即可。因为在该方法中增加了 expectedModCount = modCount
操作。但是这个 remove()
方法也有弊端。
1、只能进行remove()
操作,add()
、clear()
等 Itr 中没有。
2、调用 remove()
之前必须先调用 next()
。因为 remove()
开始就对 lastRet()
做了校验。而 lastRet 初始化时为 -1。
3、next ()之后只可以调用一次 remove()。因为 remove ()会将 lastRet 重新初始化为 -1.
4. 总结
ArrayList
底层基于数组实现容量大小动态可变
。 扩容机制为首先扩容为原始容量的 1.5 倍
。如果1.5倍太小的话,则将我们所需的容量大小赋值给 newCapacity,如果1.5倍太大或者我们需要的容量太大,那就直接拿 newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE
来扩容。 扩容之后是通过数组的拷贝来确保元素的准确性的,所以尽可能减少扩容操作。 ArrayList 的最大存储能力:Integer.MAX_VALUE
。 size 为集合中存储的元素的个数。elementData.length 为数组长度,表示最多可以存储多少个元素。 如果需要边遍历边 remove ,必须使用 iterator。且 remove 之前必须先 next,next 之后只能用一次 remove
。