1、ArrayList概述
1)ArrayList是可以动态增长和缩减的索引序列,是基于数组实现的List类,即动态数组。它是线程不安全的,允许元素为null。
2)该类封装了一个动态再分配的Object[]数组,每个类对象都有一个capacity属性,表示Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加。
3)因其底层数据结构是数组,所以它是占据一块连续的内存空间(容量就是数组的length),可以根据下标以O1的时间复杂度读写(改查)元素,因此时间效率很高,但它也有数组的缺点,空间效率不高。
4)ArrayList中的扩容操作性能消耗比较大,若能提前知道数据规模,应通过public ArrayList(int initialCapacity) {}
构造方法,指定集合的大小,构建实例对象,以减少扩容次数,提高性能。
或扩容时,手动调用public void ensureCapacity(int minCapacity) {}
方法扩容。不过该方法是ArrayList的API,不在List接口里,所以使用时需要用ArrayList实例对象调用,若是List对象则需要强转:((ArrayList)list).ensureCapacity(30);
5)ArrayList和Collection的关系:
2、ArrayList的数据结构
分析一个类的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就理解了该类的实现思路,具体的实现细节再具体分析。
ArrayList的数据结构是:
说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。对ArrayList类的实例的所有的操作底层都是基于数组的。
3、ArrayList源码分析
3.1继承结构和层次关系
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
ArrayList的继承结构:
ArrayList extends AbstractList
AbstractList extends AbstractCollection
所有类都继承Object类,所以ArrayList的继承结构就是上图所示。
注意:
1)为什么要先继承AbstractList,让AbstractList先实现List,而不是让ArrayList直接实现List ?
接口中全都是抽象方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,先让抽象类AbstractList实现接口中一些通用的方法,再让具体的类,如ArrayList继承这个抽象类,拿到这些通用的方法,然后自己在实现一些特有的方法。这样让代码更简洁,将继承结构最底层的类中的通用方法都抽取出来,一起让抽象类实现,减少重复代码。
2)ArrayList实现了哪些接口?
①List接口:ArrayList的父类AbstractList也实现了List 接口,为什么子类ArrayList还去实现一遍呢?开发这个collection 的作者Josh说,这其实是一个mistake,因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。
②RandomAccess接口:是一个标记性接口,代表了其拥有随机快速访问的能力,可以以O(1)的时间复杂度去根据下标访问元素。
实现了该接口,使用普通的for循环来遍历性能更高,如ArrayList。
而没有实现该接口,使用Iterator迭代器性能更高,LinkedList。
所以它让我们知道用什么样的方式去获取数据性能更好。
③Cloneable接口:是一个标记性接口,实现了该接口,表示该对象能被克隆,能使用Object.clone()方法。没实现该接口调用Object.clone()方法就会抛出CloneNotSupportedException。
④Serializable接口:实现该序列化接口,表明该类可以被序列化。什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原来的类。
3.2类中的属性
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
{
// 版本号
private static final long serialVersionUID = 8683452581122892189L;
// 默认容量
private static final int DEFAULT_CAPACITY = 10;
// 空对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空对象数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素数组
transient Object[] elementData;
// 实际元素大小,默认为0
private int size;
// 最大数组容量:2147483647-8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//modCount 是继承自AbstractList的属性,
//记录了ArrayList结构性变化的次数,也就是集合长度变化次数。
protected transient int modCount = 0;
}
3.3构造方法
ArrayList有三个构造方法:
1)无参构造方法
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
/**
* Constructs an empty list with an initial capacity of ten.
* 说明了默认会给10的大小,所以说调用无参构造,初始容量是10.
*/
public ArrayList() {
//默认第一行加上super(),即调用父类中的无参构造方法,是个空的构造方法
/**DEFAULTCAPACITY_EMPTY_ELEMENTDATA:是个空的Object[]。
将elementData初始化,空的Object[]会给默认大小10,此时还未赋值。*/
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2)带初始容量的构造方法
//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果初始容量大于0,则新建一个长度为initialCapacity的Object数组.
//注意这里并没有修改size(对比第三个构造函数)
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果容量为0,直接赋为EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
3)以其他集合为参数的构造方法
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection extends E> c) {
//直接利用Collection.toArray()方法得到一个对象数组,并赋值给elementData
elementData = c.toArray();
//因为size代表的是集合元素数量,所以通过别的集合来构造ArrayList时,要给size赋值
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
//这里是当c.toArray出错,没有返回Object[]时,
//用Arrays.copyOf 来复制集合c中的元素到elementData数组中
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//如果集合c元素数量为0,则将空数组赋值给elementData
this.elementData = EMPTY_ELEMENTDATA;
}
}
小结:构造函数执行完毕,就是初始化一个数组elementData和数组实际元素个数size。
关于方法:Arrays.copyOf(elementData, size, Object[].class)
是根据class的类型来决定是new 还是反射去构造一个泛型数组,同时利用native函数System.arraycopy
,批量赋值元素至新数组中。
如下:
public static 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;
}
//src:源对象;srcPos:源对象对象的起始位置
//dest:目标对象;destPost:目标对象的起始位置
//length:从起始位置往后复制的长度。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
这些复制数组的方法都是浅拷贝,浅拷贝与深拷贝的区别可参考:
浅拷贝与深拷贝
3.4核心方法
3.4.1 增
1)boolean add(E e)
:默认直接在末尾添加元素
public boolean add(E e) {
//确定内部容量是否够,size是数组中数据的个数,因为要添加一个元素,所以size+1。
//先判断size+1个数据,数组能否放得下,即在此方法中去判断是否数组.length是否够
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;//在数组末尾追加一个元素,并修改size
return true;
}
//确定内部容量的方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//此方法判断elementData是否为空,为空则返回默认容量(10)
//不为空则将传来的参数minCapacity原值(size+1)返回
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//判断初始化的elementData是不是空的数组,也就是没有长度
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//若为空,则minCapacity=size+1,其实就是等于1。
//空的数组没有长度就存放不了,所以就将minCapacity变成默认容量10
//但是此时,还没有真正初始化这个elementData的大小。
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//与传来的容量minCapacity(10或size+1)对比,判断elementData实际的容量是否够用
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 判断是否需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
扩容核心方法grow:
private void grow(int minCapacity) {
//将扩充前的elementData大小给oldCapacity
int oldCapacity = elementData.length;
//newCapacity就是1.5倍的oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
//这句话适应于elementData为空数组时,length=0,则oldCapacity=0,newCapacity=0,minCapacity =10
//所以判断成立,在这里真正的初始化elementData的大小,就是为10
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
//如果newCapacity超过了最大的容量限制,就调用hugeCapacity(),将能给的最大值给newCapacity
newCapacity = hugeCapacity(minCapacity);
//新的容量大小已经确定好,就copy数组,进行扩容操作
elementData = Arrays.copyOf(elementData, newCapacity);
}
//将能给的最大值给newCapacity
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//若minCapacity 大于MAX_ARRAY_SIZE,那么返回Integer.MAX_VALUE,反之将MAX_ARRAY_SIZE返回。
//因为maxCapacity比minCapacity大很多,就用minCapacity来判断。
//Integer.MAX_VALUE:2147483647 MAX_ARRAY_SIZE:2147483639
//也就是说最大也就能给到int型最大值。若超过这个限制,就要溢出。
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
2)void add(int index, E element)
:在特定位置添加元素,即插入元素
public void add(int index, E element) {
//判断插入的位置index是否越界
rangeCheckForAdd(index);
//判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//在插入元素之后,要将index之后的元素都往后移一位
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)boolean addAll(Collection extends E> c)
:默认直接在末尾添加集合C中所有元素
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;
}
4)boolean addAll(int index, Collection extends E> c)
:在特定位置添加集合C中所有元素
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方法,先判断是否需要扩容(带索引index时还需要判断索引是否越界),都会进行数组的复制,比修改modCount即+1。
正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。
当调用add方法时,实际上还会进行一系列调用,可能会调用到grow,grow可能会调用hugeCapacity,实际上的函数调用如下:
举例说明一:
List lists = new ArrayList();
lists.add(8);
初始化lists大小为0,调用的ArrayList()型构造函数,那么在调用lists.add(8)方法时,会经过怎样的步骤呢?下图给出了该程序执行过程和最初与最后的elementData的大小。
可以看到,在add方法之前开始elementData = {};调用add方法时会继续调用,直至grow,最后elementData的大小变为10,之后再返回到add函数,把8放在elementData[0]中。
举例说明二:
List lists = new ArrayList(6);
lists.add(8);
调用的ArrayList(int)型构造函数,那么elementData被初始化为大小为6的Object数组,在调用add(8)方法时,具体的步骤如下:
可以知道,在调用add方法之前,elementData的大小已经初始化为6,之后再进行传递,不会进行扩容处理。
3.4.2 删
其中private void fastRemove(int index)
是private的,是提供给public boolean remove(Object o)
用的。
1)public E remove(int index)
:删除指定位置上的元素
public E remove(int index) {
//检查index的合理性,判断是否越界
rangeCheck(index);
//修改modeCount 因为结构将发生改变
modCount++;
//通过索引获取要删除的元素值
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
//复制原数组index之后的元素,来覆盖待删除元素及之后的元素
System.arraycopy(elementData, index+1, elementData, index,numMoved);
//尾部赋值为null,并修改size,让gc(垃圾回收机制)更快的回收。
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));
}
//根据下标从数组取值 并强转
E elementData(int index) {
return (E) elementData[index];
}
2)public boolean remove(Object o)
:删除指定元素
//删除该元素在数组中第一次出现的位置上的数据。 如果有该元素返回true,否则返回false。
//fastRemove(index)方法的内部跟remove(index)的实现几乎一样,知道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;
}
//不用判断是否越界 ,也不需要取出该元素。
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;
}
3)public boolean removeAll(Collection> c)
:批量删除集合C中的元素
public boolean removeAll(Collection> c) {
Objects.requireNonNull(c);//判空
return batchRemove(c, false);//批量删除
}
public static T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
//此方法用于两处,如果complement为false,则用于removeAll()如果为true,则给retainAll()用。
//retainAll()是用来检测两个集合是否有交集的,保留交集中的元素。
private boolean batchRemove(Collection> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0; //r用来控制循环,w记录剩余元素个数
boolean modified = false;
try {
for (; r < size; r++)
//遍历原数组,判断集合C中是否有数组中的元素
if (c.contains(elementData[r]) == complement)
//若没有包含,则代表是需要保留的元素,则将其覆盖数组头部元素
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
//出现异常会导致 r !=size , 则将出现异常处后面的数据全部复制到数组里。
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
//删除完毕之后,将数组中w位置之后的元素全部置为null
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
小结:
1)删除操作一定会修改modCount,涉及到数组的复制,相对低效。
2)remove函数删除元素都是将其置为null,为了之后整个数组不被使用时,会被GC更快回收。
3.4.3 去除null,trimToSize
trimToSize方法
(1)修改modCount次数加1
(2)将elementData尾部空余的空间(包括null值)去除,例如:数组长度为10,其中前五个元素有值,其他为空,调用该方法后数组的长度变为5。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
3.4.4 清空,clear
(1)修改modCount次数加1
(2)将数组中每个元素都赋值为null,等待垃圾回收将其回收
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
3.4.5 改
不会修改modCount,相对增删是高效的操作。
public E set(int index, E element) {
rangeCheck(index);// 检验索引是否越界
E oldValue = elementData(index);//取出旧值
elementData[index] = element;//赋新值
return oldValue;//返回旧值
}
3.4.6 查
不会修改modCount,相对增删是高效的操作。
public E get(int index) {
rangeCheck(index);// 检验索引是否合法
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];//返回的值都经过了向下转型(Object -> E)
}
3.4.7 包含,contain
//普通的for循环寻找值,根据目标对象是否为null分别循环查找。
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
// 从头部开始查找数组里面是否存在指定元素
//找到第一个和指定元素相等的元素,返回下标
//与此函数对应的lastIndexOf,表示从尾部开始查找。
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
3.4.8 迭代器,iterator
interator方法返回的是一个内部类,由于内部类的创建默认含有外部的this指针,所以这个内部类可以调用到外部类的属性。
public Iterator iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator {
// 下一个要返回元素的索引,默认是0
int cursor;
//最后一个要返回元素的索引,-1表示不存在
int lastRet = -1;
//记录期望的修改次数,判断集合在迭代中是否修改过结构
//用于保证迭代器在遍历过程中不会有对集合的修改结构操作,迭代器的自身的remove方法除外
int expectedModCount = modCount;
Itr() {}
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)
//再次判断是否越界,在这里的操作时,是否有异步线程修改了List
throw new ConcurrentModificationException();
cursor = i + 1;//游标+1
//返回元素 ,并设置上一次返回的元素的下标
return (E) elementData[lastRet = i];
}
//remove 掉 上一次next的元素
public void remove() {
if (lastRet < 0)//先判断是否next过
throw new IllegalStateException();
checkForComodification();//判断是否修改过结构
try {
//删除元素 remove方法内会修改 modCount ,所以后面要更新Iterator里的这个标志值
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();
}
}
快速报错机制(fail-fast)的实现原理:或为什么会发生并发修改异常?
1)ArrayList从其父类AbstractList继承了一个modCount属性,每当对ArrayList进行修改(add,remove,clear等)时,就会将modCount加1。
2)ArrayList中迭代器的实现类Itr也有一个expectedModCount属性。
3)一旦调用iterator()方法来使用迭代器时,Itr类也就被初始化,expectedModCount就会被赋予一个与modCount相等的值。
4)如果接下来进行遍历操作,则每次调用next()方法获取值时,都会先进行修改检查(checkForComodification),也就是检查modCount和expectedModCount两个值是否相等。
5)如果在遍历过程中进行了对集合的其它修改操作而使得modCount值发生变化,从而造成两者不等,就立即抛出ConcurrentModificationException。
注意:迭代器自身也提供了remove方法,但该方法会同步更新modCount的值赋给expectedModCount,保证该remove方法是安全的,而不希望在迭代时使用ArrayList容器自己提供的add、remove等方法。
如何正确的删除容器中的元素:
public void setUp(){
list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
}
//Demo1:使用for循环,删除元素【正确】
public void testFor(){
for(int i=0;i iterator = list.iterator();
while(iterator.hasNext()){
String s= iterator.next();
//删除3
if("3".equals(s)){
iterator.remove();//使用迭代器的remove
}
}
System.out.println(list);
}
//Demo4:使用Iterator,调用集合自身的remove()删除元素【错误】
public void testIterator2(){
Iterator iterator = list.iterator();
while(iterator.hasNext()){
String s= iterator.next();
//删除3
if("3".equals(s)){
list.remove(s);//使用list容器的remove
}
}
System.out.println(list);
}
//Demo5:获得iterator后进行了错误操作【错误】
public void testIterator3(){
Iterator iterator = list.iterator();
//这是错误的操作。获取迭代器后不能再调用容器的修改方法
list.add("这是错误的行为");
//这是允许的。
//iterator.remove(xx);
while(iterator.hasNext()){
String s= iterator.next();
//删除3
if("3".equals(s)){
iterator.remove();//使用迭代器的remove
}
}
System.out.println(list);
}
关于java容器中的快速报错机制可参考:
java容器中的快速报错机制
4、ArrayList总结
1)ArrayList本质上是一个可以存放null的elementData数组。
2)ArrayList区别于数组的地方在于能够自动扩展大小,其中关键方法就是gorw()方法。
3)扩容操作会导致数组复制,批量删除会找出两个集合的交集,以及数组复制操作,因此,增、删都相对低效。 而 改、查都是很高效的操作。
4)增删改查中, 增删一定会修改modCount, 改查一定不会修改modCount。
5)和Vector区别在于Vector在API上都加了synchronized所以它是线程安全的,以及Vector扩容时,若无指定扩容量则扩容为之前的2倍,而ArrayList是扩容为之前的1.5倍。
5、参考笔记
Java集合源码分析(一)ArrayList
面试必备:ArrayList源码解析(JDK8)