目录
一、ArrayList介绍
Java 集合框架拥有两大接口Collection和Map,其中,Collection下面有三个: List、Set和Queue。Arraylist就实现了 List接口,其实就是一个数组列表,不过作为 Java 的集合框架,它只能存储对象引用类型,也就是说当我们需要装载的数据是诸如int、float等基本数据类型的时候,必须把它们转换成对应的包装类。
二、ArrayList源码解析
1、ArrayList实现的接口
如下图所示:
观察上图,不难发现ArrayList继承了AbstractList<E>,并实现了三个接口,分别是:Serializable、Cloneable和RandomAccess。下面对ArrayList中继承的类和实现的接口进行简单的介绍:
- AbstractList类:该类实现了List接口里面的方法,并且为其提供了默认代码实现。而List接口中主要定义了集合常用的方法让ArrayList进行实现,如:add、addAll、contains、remove、size、indexOf等方法。
- Serializable接口:主要用于序列化,即:能够将对象写入磁盘。与之对应的还有反序列化操作,就是将对象从磁盘中读取出来。因此如果要进行序列化和反序列化,ArrayList的实例对象就必须实现这个接口,否则在实例化的时候程序会报错(java.io.NotSerializableException)。
- Cloneable接口:实现Cloneable接口的类能够调用clone方法,如果没有实现Cloneable接口就调用方法,就会抛出异常(java.lang.CloneNotSupportedException)。
- RandomAccess接口:该接口表示可以随机访问ArrayList当中的数据。随机访问是指我们可以在常量时间复杂度内进行数据的方法,因为ArrayList的底层实现是数组,而数组是可以随机访问的。
2、ArrayList中的变量
ArrayList就是一个以数组形式实现的集合,其元素的功能为:
- DEFAULT_CAPCITY:默认初始容量10。
- elementData:ArrayList是基于数组的一个实现,elementData就是底层的数组
- size:ArrayList里面元素的个数,size是按照调用add、remove方法的次数进行自增或者自减的,所以add了一个null进入ArrayList,size也会加1
- EMPTY_ELEMENTDATA:当没有指定ArrayList容量时返回该数组
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA:当没有指定ArrayList容量时返回该数组
3、ArrayList的构造方法
(1)带初始容量的构造方法
public ArrayList(int initialCapacity) {
//如果初始容量大于0
if (initialCapacity > 0) {
//创建一个指定容量大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { //如果初始容量为0
//将EMPTY_ELEMENTDATA赋值给elemanetData
this.elementData = EMPTY_ELEMENTDATA;
} else { //否则则抛出异常(容量小于0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
总结:
- 如果初始容量小于0, 则抛出异常;
- 如果初始容量等于0,则将EMPTY_ELEMENTDATA赋值给 elementData;
- 如果初始容量大于0,则闯将一个初始容量大小的数组。
(2)无参构造函数
public ArrayList() {
//构建一个初始容量为10的数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
总结:使用默认的构造方法,则创建一个初始容量为10的数组。
(3)集合型构造方法
public ArrayList(Collection<? extends E> c) {
//将参数中的集合转为数组,赋值给elementData
elementData = c.toArray();
//如果数组大小不等于0
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//如果c.toArray()返回的数组类型不是Object[]类型的,则利用Arrays.copyOf()创建一个大小为size的、类型为Object[]的数组, 并将elementData中的元素复制到新的数组中,最后让elementData 指向新的数组
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else { // 否则设置元素数组为空数组
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
总结:将参数中的集合转换为数组,赋值给 elementData,然后给size进行赋值,size代表集合元素数量。再判断参数是否为空,如果数组的大小不等于 0,则进一步判断是否转化为Object类型的数组,如果不是,则进行复制;否则,设置元素数组为空数组
4、ArrayList主要方法分析
假设我们使用无参构造方法创建一个ArrayList,那么它是如何操作的。
(1)ArrayList 如何指定底层数组大小的?
ArrayList中真正存储数据的地方是数组,因此在初始化ArrayList的时候要给数组分配一个大小,开辟一个内存空间。ArrayList的无参构造函数如下:
通过上述分析可以看到,ArrayList底层的Object数组,即:elementData 赋值了一个默认的空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也就是说,使用无参构造函数初始化ArrayList后,它当时的数组容量为 0 。
然而容量为 0 的数组没啥用,那么是什么时候给数组分配内存的呢?如果使用了无参构造函数来初始化Arraylist, 只有当我们真正对数据进行添加操作add时,才会给数组分配一个默认的初始容量DEFAULT_CAPACITY=10
(2)add方法的分析
add方法的源码如下:
public boolean add(E e) {
//保证容量,再添加元素之前要保证内部的数组大小足够容纳该元素
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以看到:add方法添加一个元素到列表的末尾。它首先通过ensureCapacityInternal(size+1)来保证Object[]数组有足够的空间存放添加的数据,然后再将添加的数据存放到数组对应位置上。
private void ensureCapacityInternal(int minCapacity) {
//判断集合存放数据的数组是否等于空容量的数组,即第一次加入元素时才会执行此代码块。
//假如制定了ArrayList的长度为1,那么加入元素时就会频繁的触发扩容
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//通过最小容量和默认容量,求出最大值,用于第一次扩容
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//将计算得到的容量传递给下一个方法,继续校验
ensureExplicitCapacity(minCapacity);
}
可以看到:当第一次添加元素时,最小容量为10。如果不是第一次添加元素,则传入参数就为最小容量,得到最小容量后,然后调用ensureExplicitCapacity()方法,判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
//实际修改集合次数加1
modCount++;
// 判断最小容量减去数组长度是否大于0.这里的长度指的是容积,而不是元素的个数。也就是说,当前数组长度小于最小容量时,进行扩容
if (minCapacity - elementData.length > 0)
//将第一次计算得到的容量传递给核心扩容方法
grow(minCapacity);
}
通过ensureExplicitCapacity()方法判断最小容量和当前数组大小,若所需的最小容量大于数组大小,则需要进行扩容,然后调用grow()方法实现扩容
private void grow(int minCapacity) {
//记录数组的实际长度,第一次添加元素时没有存储元素,则长度为0
int oldCapacity = elementData.length;
//将数组长度扩容为原容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断新容量减去最小容量是否小于0,如果时第一次调用add,则必然小于
if (newCapacity - minCapacity < 0)
//将最小容量赋给新容量
newCapacity = minCapacity;
//判断新容量减去最大数组大小是否大于0,如果时则计算出一个超大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//调用数组工具类方法,创建一个新数组,将新数组的地址赋值给elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
Step1:先将当前数组大小赋值给oldCapacity,然后 将oldCapacity + (oldCapacity >> 1)赋值给newCapacity(扩容后容量),该表达式相当于1.5*oldCapacity ,即扩容为原来的1.5倍。
Step2:利用newCapacity进行两次判断:
- 第一次判断 if (newCapacity - minCapacity < 0),判断扩容后容量是否大于minCapacity,若小于minCapacity,则直接将minCapacity赋值给newCapacity
- 第二次判断 if (newCapacity - MAX_ARRAY_SIZE > 0),判断newCapacity 是否超出了ArrayList所定义的最大容量,若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为 Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
Step3:最终得到newCapacity,然后调用Arrays.copyOf()方法进行扩容
private static int hugeCapacity(int minCapacity) {
//如果最小容量小于0,抛出异常;否则就比较并返回
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
综上所述,先得到最小所需容量,即当前数组大小+1,若为第一次添加则所需最小容量为10,然后使用最小所需容量与当前数组大小进行比较,判断是否需要扩容,若需要则调grow进行扩容,扩容实际是使用Arrays.copyOf()方法,创建一个新数组大小为扩容量,然后将当前数组进行赋值。
注意点如下:
- 使用无参构造创建一个ArrayList对象时,初始数组为0,当第一次添加元素时变为10
- 默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组
- 在jdk1.7之前创建实例时就创建了长度10的数组,而1.8是在第一次添加元素时。
分析完上述代码,可以看下如何在指定位置去添加元素的代码逻辑,其源码如下:
public void add(int index, E element) {
//检查给定位置是否在范围内。如果不是,则引发运行时异常,如果索引为负,则会引发ArrayIndexOutOfBoundsException
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
可以看到:在指定位置添加元素,首先进行了数组范围的检查,防止越界,然后调用方法检验是否要扩容,且增量++,之后完成数组拷贝即可。
(3)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);
//因为最后一个数据已经拷贝到前一个位置了,所以可以设置为 null ,进行垃圾回收
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
由上述分析:删除元素同样需要进行范围校验。然后计算删除需要移动的数据,再通过数组拷贝移动数组。其次还有一个小细节,可以发现remove()方法是有返回值的,而这个返回值就是我们删除的元素的值。
(4)set()方法
该方法用来设置指定下标的数据,进行元素数据的更新。
public E set(int index, E element) {
//范围校验
rangeCheck(index);
//更新元素并返回旧值
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
(5)get()方法
该方法用来获取对应下标的数据。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
(6)其他方法
- size() : 获取集合长度,通过定义在ArrayList中的私有变量size得到
- isEmpty():是否为空,通过定义在ArrayList中的私有变量size得到
- contains(Object o):是否包含某个元素,通过遍历底层数组elementData,通过equals或==进行判断
- clear():集合清空,通过遍历底层数组elementData,设置为null
5、迭代器
(1)Itr内部类
Itr类是ArrayList的内部类,其主要字段有以下几个:
- int cursor:下一个元素的下标 当我们 new 这个对象的时候这个值默认初始化为0 // 我们使用的时候也是0这个值,因此不用显示初始化
- int lastRet = -1: 上一个通过 next 方法返回的元素的下标
- int expectedModCount = modCount:modCount 表示数组当中数据改变的次数,modCount 是ArrayList 当中的类变量,expectedModCount 是 ArrayList内部类 Itr 中的类变量。然后将这个变量保存到 expectedModCount当中,使用 expectedModCount 主要用于 fast-fail 机制
当ArrayList当中发生一次结构修改(Structural modifications)时,modCount就++。所谓结构修改就是那些让ArrayList当中数组的数据个数size发生变化的操作,比如说add、remove方法,因为这两个方法一个是增加数据,一个是删除数据,都会导致容器当中数据个数发生变化。而set方法就不会是的modCount发生变化,因为没有改变容器当中数据的个数。
在迭代器中有两个重要的方法next()和hasNext(),其源码如下:
public boolean hasNext() {
return cursor != size;
}
//获取元素的方法
public E next() {
//每次获取元素,会先调用该方法校验 预期修改次数是否 == 实际修改次数
/*
tips:
if(s.equals("hello")) {
list.remove("hello");
}
当if表达式的结果为true,那么集合就会调用remove方法
*/
checkForComodification();
//把下一个元素的索引赋值给i
int i = cursor;
//判断是否有元素
if (i >= size)
throw new NoSuchElementException();
//将集合底层存储数据的数组赋值给迭代器的局部变量 elementData
Object[] elementData = ArrayList.this.elementData;
//再次判断,如果下一个元素的索引大于集合底层存储元素的长度 并发修改异常
//注意,尽管会产生并发修改异常,但是这里显示不是我们要的结果
if (i >= elementData.length)
throw new ConcurrentModificationException();
//每次成功获取到元素,下一个元素的索引都是当前索引+1
cursor = i + 1;
//返回元素
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
//如果预期修改次数 和 实际修改次数不相等 就产生并发修改异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
可以看到:
- haxNext():根据cursor和size来判断是否已经遍历到末尾了
- next():准备移动索引,先判断是否越界,如果没有越界,那么就可以移动cursor索引,并且返回cursor索引指向的值
(2) ListItr内部类
ListItr也是ArrayList的一个内部类,它继承了Itr类,新添加了hasPrevious、nextIndex、previousIndex、previous等方法,其源码如下:
private class ListItr extends Itr implements ListIterator<E> {//ListIterator<E> extends Iterator<E>
//new ListItr(n)代表从n开始遍历
ListItr(int index) {
super();
cursor = index;
}
/*判断是否有上一个元素*/
public boolean hasPrevious() {
return cursor != 0;
}
/*返回下一次越过的元素索引*/
public int nextIndex() {
return cursor;
}
/*返回上一次越过的元素索引*/
public int previousIndex() {
return cursor - 1;
}
/*向前遍历*/
public E previous() {
checkForComodification();//fail-fast
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;//向前遍历-->cursor-1
return (E) elementData[lastRet = i];//为lastRet赋值并返回越过的元素
}
/*设置元素*/
public void set(E e) {
if (lastRet < 0)//这里也说明了调用set之前要先调用next或previous
throw new IllegalStateException();
checkForComodification();//fail-fast
try {
ArrayList.this.set(lastRet, e);//调用ArrayList.set方法,这里没有修改modCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
/*添加元素*/
public void add(E e) {
checkForComodification();//fail-fast
try {
int i = cursor;
ArrayList.this.add(i, e);//调用ArrayList.add方法,modCount++
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;//重新设置expectedModCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
三、总结
- ArrayList 就是一个实现了List接口的课自动扩容的数组,当添加元素的时候它会尝试扩容,扩容的标准是变为原来的1.5倍,当删除元素的时候,它会左移元素,避免数组出现"空位"
- ArrayList 都有容量,容量就是ArrayList里面数组的大小
- ArrayList 是一个有序的集合,它的维持的顺序就是元素的插入顺序(可以对比HashMap)
- ArrayList 可以存储重复值和null值
- ArrayList 是快速失败的,在遍历的同时当集合被修改后会抛出ConcurrentModificationException,可以使用Iterator 的删除方法来避免这个问题、
- ArrayList 不是线程安全的,如果你想在多线程环境中使用,可以使用Vector 或者它的线程安全包装类
关于ArrayList的应用场景
- 对于需要快速随机访问元素,应该使用ArrayList。
- 对于单线程环境或者多线程环境,但List仅仅只会被单个线程操作,此时应该使用非同步的类ArrayList。