本文所有代码来自JDK1.8
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList继承了AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable接口
java.lang.Object java.util.AbstractCollection<E> java.util.AbstractList<E> java.util.ArrayList<E>
List是动态数组,并允许操作所有元素,包括null。
每个ArrayList实例都有一个容量大小,此容量是列表数组中能够存储元素的个数,它始终是大于等于元素个数的。当ArrayList中添加元素时,其容量会自动增加。在通过ensureCapacity方法添加大量元素之前,预判需要的容量,因此可以手动设置ArrayList实例的容量,这样会减少重新分配的次数。
ArrayList实现是不同步。如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改了列表,则必须进行同步,结构修改是指添加或删除一个或多个元素的操作,或显式调整后备数组的大小;仅设置元素的值不是结构修改。
1、如何扩容的?
2、线程安全吗?如何确保线程安全
3、如何初始化?
4、查找、添加、删除元素时间复杂度?
5、ArrayList与LinkedList的比较
/** * 默认初始化容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 用于没有实例的共享空数组实例 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 用于默认大小空实例的共享空数组实例 * 区别于EMPTY_ELEMENTDATA, 当第一个元素添加进来可知该如何膨胀 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * ArrayList的底层数据结构 * 任何空ArrayList元素类型为DEFAULTCAPACITY_EMPTY_ELEMENTDATA * 当第一个元素添加进来将扩展为DEFAULT_CAPACITY * */ transient Object[] elementData; //非私有,以简化嵌套类访问
注意:transient关键字是指在采用Java默认的序列化机制的时候,被该关键字修饰的属性不会被序列化,说明此属性不采用默认序列化方法,因为自己实现了序列化和反序列化(writeObject
和readObject
)。
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
1、序列化中记录列表中的个数,且按顺序序列化
2、序列化过程中若出现modCount != expectedModCount
,也会抛出ConcurrentModificationException
/** * 包含的元素个数,与容量做区别 */ private int size; /** * 最大数组大小为Integer.MAX_VALUE - 8 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/** * 构建一个指定容量的空列表 */ public ArrayList(int initialCapacity) { //创建输入大小的数组 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; //初始容量为0,初始化一个空数组 } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; //当输入的初始化容量不符合要求,抛出IllegalArgumentException } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } /** * 构建一个容量为10的空列表 */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 构建一个包含指定集合的所有元素的列表,元素顺序按照集合迭代器返回的顺序 */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // 将原来集合中元素向上转换为Object类型 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 当集合元素为0,初始化一个空数组 this.elementData = EMPTY_ELEMENTDATA; } } /** * 把ArrayList的容量减小到元素数目大小,可最小化存储 */ public void trimToSize() { modCount++;//算结构性改变 if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
假设一群人在排队,每个人按顺序都代表一个号,第一个人正在办理业务代表0号,后面的人依次排开1,2,3,4号,当工作人员喊到3号的时候,这个人知道是自己就可以站出来,就相当于get(index)操作。
public E get(int index) { rangeCheck(index); return elementData(index); } private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
首先判断要查找的下标是否大于等于数组大小(从0开始存数据),若没有返回该位置元素,否则抛出下标越界异常
//通过indexOf(o)来判断是否存在对应元素 public boolean contains(Object o) { return indexOf(o) >= 0; } 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; } public int lastIndexOf(Object o) { if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData[i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData[i])) return i; } return -1; }
1、首先判断输入元素是否为null,避免空指针异常(ArrayList可以存null元素)
2、indexOf
方法从头到尾遍历,lastIndexOf
方法从尾到头遍历
3、找到则返回相应位置下标,为大于等于0的整数,没有找到相应元素则返回-1
下面用图举例:
假设在一群人在排队中,突然来了一个人,正常的话是排到队尾的,相当于add(E e)
;但是这个人可能有急事就会选择插队,那么他找个位置插进去,那么他插进去位置后面的人都需要往后走一步才会产生空间,这就相当于add(int index, E element)
//在列表末尾添加元素 public boolean add(E e) { ensureCapacityInternal(size + 1); 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); }
1、首先调用ensureCapacityInternal(size + 1)
,如果elementData
为空,则minCapacity
设置为10和size+1中最大值
2、发生结构性改变,modCount
会增加1
3、如果此时的容量还小于元素的个数,说明数组已经放满,则需要进行扩容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); }
1、获取当前数组长度oldCapacity
2、新的长度为oldCapacity + (oldCapacity >> 1),即将oldCapacity右移一位然后加1,新容量是旧容量的1.5倍+1
3、如果newCapacity <minCapacity
,则新容量设置为minCapacity
4、如果新容量比允许的最大数组容量都要大,则新容量设置为虚拟机允许的最大值
5、将elementData
放入新容量数组中
//在列表中任意位置添加元素 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++; }
1、不仅检查输入的索引是否大于数组大小,还判断是否小于0
2、发生结构性改变,其中modCount
增加1
3、向后移动一部分元素,为要添加的元素腾出空间
4、在所在下标添加元素
5、list中元素个数加1
下面用图举例展示:
//指定集合所有元素添加到list集合中,添加了返回true,否则返回false public boolean addAll(Collection<? extends E> c) {// E代表父类 ,?代表 子类 Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); System.arraycopy(a, 0, elementData, size, numNew); size += numNew; //若加入的是空集合则返回false return numNew != 0; }System.arraycopy(src, srcPos, dest, destpoS,length);
其中各个参数意义如下:
Object src : 从哪复制 -- 源数组.
int srcPos : 源数组从哪个下标开始复制.
Object dest : 复制到哪 -- 目的数组.
int destPos :粘贴到开始下标的位置.
int length :将要被复制的元素个数.
//删除指定位置上的元素 public E remove(int index) { rangeCheck(index);//1 modCount++;//2 E oldValue = elementData(index);//3 int numMoved = size - index - 1;//4 if (numMoved > 0)//5 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; //6 return oldValue;//7 }
1、判断输入的索引是否小于数组个数,否则抛异常结束
2、发生结构性改变,modCount
需要增加1
3、旧值需要返回,记录旧值
4、计算移动的元素个数,下图举例5-2-1=2个
5、数组进行左移操作,即删除了要删除的元素
6、最后位置设置为null,下次gc就会回收这些元素所占的内存空间
7、返回旧值
下面用图举例:
//删除指定的元素 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; }
1、首先判断是否是null,避免空指针异常,然后从头到尾遍历找对应元素,找到删除返回true,否则返回false。
2、fastRemove(int index)
与上面的remove(int index)
类似,但少了边界检测,因为说明已经在边界内找到了相应元素。
3、关键modCount++;
,下面细节扩展分析中会用到
public Object[] toArray() { return Arrays.copyOf(elementData, size); } public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); }
toArray()
作为数组对象和集合对象转化的桥梁,将返回包含list中所有元素的一个新分配的数组。
时间复杂度分析
1、ArrayList基于索引结构,在中间插入或删除一个元素会导致列表中一部分元素移动,适合查找操作,不适合中间插入、删除操作;LinkedList基于链表结构,在中间插入或删除元素的开销是固定的,因此适合频繁插入、删除操作。
2、在ArrayList的添加元素,当达到阈值会导致数组进行重新分配的容量。
3、ArrayList的空间浪费主要体现在list列表的结尾预留一定的容量空间;而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间,包括指向指针
1、ArrayList内部封装了一个Object类型的数组,本质与数组没有差别,一些方法内部都是调用array方法实现的。
2、可以将ArrayList看成一种会自动扩增容量的Array,而Array一旦分配容量则不能动态改变。
3、Array既可以存储基本类型元素,也可以存储对象类型元素;而ArrayList只能存储对象类型元素
fail-fast为快速失败机制,为Java集合的一种错误检测机制
此类的iterator
和listIterator
方法返回的迭代器是fail-fast的:如果在创建迭代器之后的任何时候对列表进行结构修改,比如通过迭代器自己的remove或add方法,迭代器将抛出ConcurrentModificationException
。因此,在并发修改的情况下,迭代器会快速停止操作,而不是在未来的未确定时间发生非确定性行为的风险,而且即使不是在多线程环境,如果单线程违反了规则,同样也会抛出异常。其中阿里巴巴开发手册中规定如下:
在foreach循环中进行元素删除,为什么会引起fail-fast机制呢?因为foreach循环中集合遍历底层是通过iterator进行的。
通过iterator直接运行,当然抛出同样的异常结果。
通过堆栈信息可以得到异常发生在调用链第14行,String next = iterator.next()
;中调用了iterator().checkForComodification()
方法,而异常就是这个方法中抛出的,看下抛出异常的原因。
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
说明发生了modCount != expectedModCount
,才抛出的异常。接下来我们来分析modCount
和expectedModCount
这两个变量
插入图expectedMod
modCount
是AbstractList中一个成员变量,被ArrayList继承,表示该集合中实际被修改的次数,用于在遍历集合iterator时,检测是否发生了add、remove等结构性改变的操作。
expectedModCount
是ArrayList中内部类Itr中的成员变量,表示这个迭代器期望该集合被修改的次数,只有通过迭代器对集合进行操作,该值才会改变。
Itr
是一个Iterator的实现,ArrayList.iterator方法获得的迭代器就是Itr的实例。
private class Itr implements Iterator<E> { int expectedModCount = modCount; }
他们之间的关系如下:
class ArrayList{ private int modCount; public void add(); public void remove(); private class Itr implements Iterator<E> { int expectedModCount = modCount; } public Iterator<E> iterator() { return new Itr(); } }
是什么导致modCount != expectedModCount
,通过之前分析源码可知fastRemove(int index)
中modCount
加1,但此时expectedModCount
并没有增加1,才导致两个数不相等。
简单来说集合遍历是通过iterator进行的,但是元素的add/remove却是使用类中自己的方法吗,因此混淆在了一起,导致iterator在遍历的时候,当一个元素通过自己方法添加或删除时,就会抛出一个异常。
知道了以后我们可以选择正确的方法在遍历的时候删除一些元素
1、使用普通for循环,此时没有使用iterator
2、迭代器iterator的remove()方法
参考:
Java Standard Ed. 8 https://docs.oracle.com/javase/8/docs/api
Holis https://mp.weixin.qq.com/s/e9ITxUmsMFhfjeHhOgTtfA