Java集合–ArrayList知识梳理
概况介绍
ArrayList
作为Java集合框架下常用的数据结构,其类的声明如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
作为List
接口的大小可变数组的实现。其底层是基于数组实现,相当于容量可变的动态数组,它实现了所有可选列表操作,并允许包括 null 在内的所有元素。
ArrayList
还实现了RandomAccess
接口,使其支持快速随机访问,即可通过下标序号进行快速访问;实现了Cloneable
接口,支持被克隆;实现了Serializable
接口,因此支持序列化,能通过序列化进行传输。
ArrayList
除了不同步,大致上等同于Vector
类。如果出现多个线程同时访问一ArrayList
实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。
成员变量
// 默认初始化容量10.
private static final int DEFAULT_CAPACITY = 10;
// 用于有参构造器的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 无参构造器默认的空数组,用来和上面的空数组进行区分,确定实例的创建方式,以便在添加第一个元素时,确定集合扩展的容量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 储存ArrayList元素的数组缓冲区
transient Object[] elementData;
// ArrayList的大小,即其中包含的元素个数,和容量不是一个概念,每添加一个元素就size++.
private int size;
构造方法
ArrayList
类有三个构造方法:
// 1.构造一个初始容量为 10 的空列表。
public ArrayList() {
/*
* 因为DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
* 所以此时ArrayList的容量为0,只有当向容器中添加元素时,
* 即调用add(E e) 时,通过Arrays.copyOf(elementData, newCapacity),
* 才实现容量为10的数组的创建
*/
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 2.构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
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;
}
}
// 3.构造一个具有指定初始容量的空列表。
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);
}
}
成员方法
- add操作
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// 该变量继承自AbstractList,用于记录操作次数.
modCount++;
// elementData.length就是集合的容量,当size+1大于容量时,集合就需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
每次向集合中添加元素前都会先确认size+1
和capacity
的大小,然后size
自增并添加该元素到数组缓冲区。第一次向集合中添加元素时,通过calculateCapacity
方法判断elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是否成立,这里就体现了DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和EMPTY_ELEMENTDATA
的区别。如果成立,则返回DEFAULT_CAPACITY
和minCapacity
的较大值,然后通过grow(minCapacity)
将集合中的数组缓冲区elementData
扩容为DEFAULT_CAPACITY
。
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);
}
可以看出ArrayList
的扩容是将容量扩大至原来容量的1.5倍,但扩容之后的集合容量也不一定合适,所以才用了下面两个if判断,继续将容量扩展至合适的大小(ArrayList
最大的容量为:Integer.MAX_VALUE
),然后通过Arrays.copyOf()
将elementData
扩展至newCapacity
的大小。到这里也就实现了前面无参构造器默认容量为10的说法。
- remove操作
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
进行校验,然后对判断该位置是否为数组的末尾,如果是数组末尾,就直接将该位置的值设为null
;如果不是数组末尾,则调用System.arraycopy()
方法,将index
位置后面的元素向前移动一个位置,然后将数组末尾的值设为null
。
调用remove(Object o)
时,通过遍历获取首次出现的指定元素的位置,如果不存在直接返回false
;如果存在则调用fastRemove(int index)
(和remove(int index)
方法对比一下,你会发现两个方法基本一致),执行完毕后返回true
。
- set操作
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
- get操作
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
从上面的源码可以看出,获取和修改ArrayList
中的元素比较简单,所以放在一起介绍。
在根据下标进行数据修改和访问之前,同样会对index
进行校验,判断其是否超出了底层数组的边界,然后再进行set和get操作。因为ArrayList
的底层是基于数组实现的,实现了RandomAccess
接口,所以直接调用数组随机访问即可。
遍历集合
- 普通for循环
for(int i = 0 ; i < list.size() ; i++){
System.out.println(list.get(i));
}
- 增强for循环
for(Object obj : list){
System.out.println(obj);
}
- Iterator迭代器遍历
Iterator it = list.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
以上三种方式都均能实现对AarryList
的遍历,其中第二种方法其底层实现也是采用的迭代器模式。
我们在遍历ArrayList
时,不能直接调用集合本身的remove
方法来删除其中的元素,因为这会改变集合的大小,从而容易造成程序执行的结果不准确或者数组下标越界,甚至抛出ConcurrentModificationException
异常。
当我们使用普通for循环时,出现的结果可能和我们预想的不一样,通过代码进行分析:
public class IteratorDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
for (int i = 0; i < list.size(); i++) {
if(2==list.get(i)) {
list.remove(i);
}
}
}
}
程序运行完毕后,我们预期list
中只剩下一个元素,即{1,3}
,通过遍历将list
中的元素打印到控制台,却发现还存在两个元素,即{1,2,3}
。
之所以这样,是因为当我们第一次遍历到2==list.get(i)
时,通过list.remove()
将该元素删除时,将集合元素依次向前移动了一个位置,将最后的位置赋值为null
,执行--size
语句,即改变了集合的大小,此时再去执行for语句,i = 2,list.size() = 2
,而此时第二个“2”的下标变成了“1”,从而导致第二“2”就没有执行到remove
操作。所以我们需要对遍历代码进行稍微修改。
for(int i = 0 ; i < list.size() ; i++){
if(2 == list.get(i)){
list.remove(i);
i--;
}
}
这样修改后,当执行了remove()操作后,通过i–,继续将下标指向当前位置,再次循环还是从当前位置执行,这样就能确保集合中的重复元素也能给删除。
对于ConcurrentModificationException
异常,一般出现在增强for和迭代器中,通过源码来分析异常的原因。
// 该方法直接返回一个Iterator的对象实例
public Iterator<E> iterator() {
return new Itr();
}
// ArrayList的内部类
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; //
Itr() {}
public boolean hasNext() {
return cursor != size;
}
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
接口,通过list.iterator()
就返回了一个Itr的实例对象。
它的三个成员变量的初始值为:cursor = 0
,记录下一个将要遍历的元素的下标;lastRet = -1
,记录当前元素的下标;excpectedModCount = modCount
,表示预期的集合修改次数。
每次遍历数组,即调用next()
方法,都会将cursor+1
,并将自增前的cursor
赋值给lastRet
,然后返回lastRet
下标位置的元素。这过程如果顺利进行,直到cursor == size
,此时会抛出NoSuchElementException
异常,所以我们在遍历之前,增加一个校验:while(it.hasNext())
,判断cursor
是否指向了集合的末尾,如果是,就跳出循环,结束遍历;如果不是,就继续遍历。
如果我们在通过迭代器对集合进行遍历时,同时采用集合本身的remove()
方法,就会出现ConcurrentModificationException
异常,代码如下:
public class IteratorDemo{
public static void main(String[] args){
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
// 用于记录当前遍历元素的下标
int index = 0;
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
if(2 == it.next()){
list.remove(index);
}
index++:
}
}
}
执行上面的程序就会出现该异常。
从上面可以看出异常发生在checkForComodification()
方法中,查看该方法,发现当modCount !=expectedModCount
时,就会抛出该异常。
以上述代码为例,在遍历list
前,我们调用了四次add()
方法,我们看代码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
注意旁边的注释:Increments modCount!!
,每次向集合中添加元素,都会执行一次++modCount
,当我们调用四次add()方法后,此时modCount = 4
。然后开始遍历集合,首先调用iterator()
,返回一个new Itr()
实例,其中expectedModCount = 4
。在遍历的过程中,当2 == i
时,调用了集合的remove()
方法,执行++modCount
,从而导致modCount = 5
;继续执行next()
,就会调用checkForComodification()
方法,校验到expectedModCount != modCount
,从而抛出ConcurrentModificationException
异常。
如何解决该异常呢?我们可以通过迭代器本身的remove()方法。
while(it.hasNext()){
if(2 == it.next())
it.remove();
}
从代码中可知该方法的内部其实也是调用了集合本身的remove()
方法,只是它在执行删除操作的同时,将新的modCount
赋值给了expectedModCount
,同时重新设置了cursor
和lastRet
两个变量,避免了普通for循环的问题。
在使用迭代器的remove()方法时,需要注意如下两点:
1、调用remove()之前,必须先调用next(),因为remove()首先就会对lastRet进行校验,而lastRet的初始值为-1。
2、next()方法之后只能执行一次remove(),因为remove()会将lastRet重新设置为-1,连续调用会触发IllegalStateException异常。
总结
ArrayList的底层是基于数组的实现,其大小可变,相当于动态数组,默认的容量为10。当容量不足时,会首先扩容为原容量大小的1.5倍,如果1.5倍还是太小,则将我们需要的容量赋值给newCapacity,作为新集合的容量,如果1.5倍容量太大或者我们需要的容量太大,就按照newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE
。扩容之后通过数组的复制来确保集合内元素的准确性,所以在数据量较大且容量范围大概可知的情况下,应直接给定合适的initialCapacity
,避免不断扩容影响程序的效率。