ArrayList源码解析
基础概念
【1】ArrayList是动态增长或者缩减的索引序列,底层基于数组实现,但是会自动进行扩容。即动态数组
【2】继承关系:
-
public class ArrayList<T> extends AbstractList<T> implements List<T>, RandomAccess, Cloneable, Serializable<T>{}
-
继承类和实现的接口如下
- RandomAccess接口:标识接口,标识实现该接口的类支持快速随机访问
- Serializable接口:标识接口,标识该类可以被序列化,即开启序列化标识
- Cloneable接口:标识接口,实现该接口后,然后在类之中重写Object的clone方法,然后调用clone方法才能克隆成功,Cloneable接口是合法调用clone方法的标识,注意深克隆和浅克隆
- List接口和抽象类AbstractList抽象类定义了需要实现的方法和对通用方法做实现
源码分析
【1】成员变量
-
private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size; private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
-
成员变量的含义解析:
- DEFAULT_CAPACITY:定义缺省容量值,即默认的初始化常量
- EMPTY_ELEMENTDATA:ArrayList空实例共享的一个空数组
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA:用于默认大小的空实例的共享数组实例。将EMPTY_ELEMENTDATA和this(DEFAULTCAPACITY_EMPTY_ELEMENTDATA);区分开来,以便在添加第一个元素的时候的得到扩容量
- 详细区别:即有参构造方法且如果创建的长度为0的话,那么elementData = EMPTY_ELEMENTDATA
- 无参构造方法创建ArrayList的时候,elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA
- 这样做的原因是为了计算后续的扩容问题
- size:elementData之中存放元素的个数
- elementData:底层存放元素的Object类型数组。此变量用了transient修饰,表示被序列化时会忽略
- MAX_ARRAY_SIZE:指定数组的最大长度,因为通过索引访问,索引为int类型
【2】构造方法
-
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
-
无参构造方法默认创建的空数组为成员变量之中的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
-
public ArrayList(int initialCapacity) { if(initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if(initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(""); } } public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if((size == elementData.length) != 0) { if(elementData.getClass != Object[].class) { elementData = Arrays.copyOf(elementData, size, Object[].class); } } else { this.elementData = EMPTY_ELEMENTDATA; } }
-
有参构造方法如果长度为0那么创建的空数组为成员变量之中的EMPTY_ELEMENTDATA
-
注意点:
- 参数为集合类型,长度非0的时候进行了类型判断。原因:集合转换为数组的时候,不一定会转换为Obejct类型即elementData.getClass != Object[].class。如果通过Arrays.asList等等方式创建出来的集合。利用Arrays的toArray方法转换为数组的时候会保留原本的类型。elementData.getClass!=Object[].class的返回值为true。所以需要对这种情况进行处理。通过Arrays.copyOf方法将其转换为Object类型的数组
【3】add添加系列方法源码
添加元素数组扩容方法解析
【1】扩容算法源码
-
public boolean add(E e) { ensureCapacityInternal(size + 1); // 当前数组元素长度加1得到添加成功时的需要的长度,即最小扩容长度 elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 如果通过无参构造方法创建ArrayList minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 扩容到默认长度10 } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { if(minCapacity - elementData.length > 0) { // 如果当前数组长度比最小扩容长度要小,那么进行扩容 grow(minCapacity); } } private void grow(int minCapacity) { int oldCapacity = elementData.length(); // 原来数组的长度 int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容到原来的1.5倍,通过位运算实现 if(newCapacity - oldCapacity < 0) { // 如果初始长度为0,导致位运算后扩容后长度newCapacity仍然为0 newCapacity = minCapacity; // 将最少扩容长度赋值给扩容后的长度 } if(newCapacity - MAX_ARRAY_SIZE > 0) { // 索引为int类型,避免扩容后长度超过int的上界 newCapacity = hugeCapacity(minCapacity); } elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if(minCapacity < 0) { throw new OutofMemoryError(); } return (minCapacity > MAX_ARRAY_SIZE)?Integer.MAX_VALUE:MAX_ARRAY_SIZE; }
【2】数组扩容方法流程解析:
- a.通过集合长度变量[size+(添加的数量)]获取到数据需要的最小容量
- b.判断是否通过无参构造方法创建的空数组,如果是那么首次直接扩容到默认长度DEFAULT_CAPACITY
- c.如果不是,那么用最小扩容长度-去数组的长度。如果最小扩容长度大于数组长度,那么进行扩容
- d.扩容运算通过位运算实现。即扩容到原来的1.5倍
- 扩容算法:原长度 + 原长度右移1位 ===> 原长度 + 原长度/2
- 可能原来的长度为0,位运算之后,长度仍然为0,此时将最小扩容长度赋值给新长度
- 可能扩容之后长度超过int的界限,边界处理,最大长度为int的上限值Integer.MAX_VALUE
- e.扩容之后需要保证,扩容后的长度大于原数组的长度,且不超过int的上限
- f.扩容之后,数组元素拷贝通过System.arrayCopy方法实现
- g.然后进行添加
添加元素算法解析
【1】源码
-
public boolean add(E e) { ensureCapacityInternal(size + 1); // 当前数组元素长度加1得到添加成功时的需要的长度,即最小扩容长度 elementData[size++] = e; return true; } 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++; // 维护数组长度 } private void rangeCheckForAdd(int index) { if(index < 0 || index > size) { throw new IndexOutOfBoundsException(); } } public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); // 转换为数组 int numNew = a.length; // 获取长度,后续的拷贝元素方法需要使用 ensureCapacityInternal(size + numNew); // 扩容 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); int numMoved = size - index; if(numMoved > 0) { System.arraycopy(elememtData, index, elementData, index + numNew, numMoved); } System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }
【2】思路和索引插入单个相同,扩容加上移动元素。核心是通过System.arraycopy方法实现移动数组元素。addAll的插入写的很秀。非常厉害。一次扩容两次拷贝,第一个拷贝实现插入位置后面元素的移位,第二次拷贝直接覆盖需要插入的位置元素
【4】删除系列方法源码解析
【1】删除算法实现
-
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; return oldMoved; } /** * 关于System.arraycopy方法,ArrayList底层数组操作大量使用了这个方法完成数据拷贝到新数组之中 * 数组一旦创建长度便不可更改,本质上每次都是创建一个新数组,然后拷贝元素 */ Syste.arraycopy(Object src, int srcPos, Object dest, int destPos, int length); /** * src: 源数组 * srcPos: 源数组的起始位置 * dest: 目标数组 * destPos: 目标数组的起始位置 * length:拷贝元素的数量 */ public boolean remove(Object o) { if(o == null) { // 如果删除元素为null,遍历找到值为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 fasle; } // 和删除单个元素的算法一样(快速删除,不检查索引越界问题) public void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if(numMoved > 0) { System.arraycopy(elementData, index, elementData, index + 1, numMoved); } elementData[--size] = null; } public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, false); } public 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++) { // 遍历 if(c.contains(elementData[r]) == complement) { //如果不存在 elementData[w++] = elementData[r]; //保存该元素 } } } finally { if(r != size) { //循环中断的处理,后面未遍历的元素默认保留,拷贝到elementData要保留的最后一个元素后面 System.arraycopy(elementData, r, elementData, w, size - r); w = w + size - r; } if(w != size) { //elementData保留个数不等于实际数组大小 for(int i = w; i < size; i++) { elementData[i] = null; } modCount = modCount + size - w; modified = true; } } return modified; }
[2]方法解析
-
单个删除其实非常简单,找到删除位置,后面的位置依次前移一个位置,覆盖。最后置空最后一个索引位置。
-
批量删除removeAll的核心代码在于batchRemove的try块的for循环这里
for(; r < size; r++) { // 遍历 if(c.contains(elementData[r]) == complement) { //如果不存在 elementData[w++] = elementData[r]; //保存该元素 } }
- 解析:删除的时候complement为false,表示如果elementData[r]如果不在集合之类,那么把这个元素保留到elementData[w]位置上,重置到数组本身
- 如果存在,那么不执行保存操作
- 批量删除实际上是求差集,即complement为false时求的是差集,如果为true则是求并集
- r!=size:表示判断r是否会等于size,如果批量删除操作成功,那么表示elementData遍历完毕,此时r的值一定等于size的长度。如果r!=size,那么表明出现异常导致数组没有遍历完毕,此时r(即遍历位置)把剩下没遍历完的当作不需要删除的数组元素,放到保留的数组元素的数组后面。
- r=size:继续执行判断w!=size,原数组elementData已经遍历完,w不等于size表示集合c存在要删除的数组元素 则从w开始遍历,在w往后的元素都置空elementData[i] = null,此时数组保存的就是批量删除之后的结果
【5】其余源码解析
-
获取元素
-
public E get(int index) { rangeCheck(index); // 检查索引是否越界 return elementData[index]; // 如果索引没有越界,那么直接返回index索引位置的元素 }
-
设置元素set
-
public E set(int index, E element) { rangeCheck(index); // 检查索引是否越界 E oldValue = elementData[index]; // 返回原来该索引位置的元素 elementData[index] = element; // 重新设置该索引位置元素的值 return oldValue; // 返回原来的值 }
-
置空集合clear
-
public void clear() { for(int i = 0; i < size; i++) { elementData[i] = null; // 遍历集合,将集合每一个元素都置为null } size = 0; // 将长度置为0 }
-
获取长度size
-
public int size() { return size; // 返回长度变量size }
-
判断集合是否为空isEmpty
-
public boolean isEmpty() { return size == 0; // 通过长度进行判断,如果长度为0,那么则为空 }
-
判断是否包含指定的元素contains
-
public boolean contains(Object o) { return indexOf(o) >= 0; // 查询指定元素,如果返回的索引位置大于等于0那么则存在该元素 } public int indexOf(Object o) { if(o == null) { // 如果o为null for(int i = 0; i < size; i++) { // 遍历如果有元素为null,返回索引位置 if(elementData[i] == null) { return i; } } } else { for(int i = 0; i < size; i++) { if(o.equals(elementData[i])) { return i; } } } return -1; // 如果不存在则返回-1 }
-
从后向前判断是否存在该元素lastIndexOf
-
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; }
-
转换为数组toArray
-
public Object[] toArray() { return Arrays.capyOf(elementData, size); } // 最底层仍然是通过调用System.arraycopy方法实现,继续看底层实现,以下是Arrays类之中 public static <T> copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { // 创建数组,长度和传入的original类型相同,长度相同 T[] copy = ((Object)newType == (Object)Object.class) ? (T[]) new Object[newLength] : Array.newInstance(newType.getComponenetType(), newLenght); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
-
((Object)newType == (Object)Object.class):判断newType是不是Object类型的数组。将其强转为Object类型的原因是:要利用==进行判断内存地址,从而判断它们是不是同一种类型。因此需要向上强转为Object,否则编译错误
-
newType.getComponentType():方法作用是返回数组内元素的类型,不是数组的时候返回null
-
Array.newInstance(newType.getComponenetType(), newLenght):创建一个类型和newType类型一样,长度为newLength的数组。Array.newInstance调用本地方法Array.newArray
-
Array.newInstance返回为Object实际上是数组
-
(T[])new Object[newLength];此处可以强转的原因是:如果传入的数组是Object类型,那么三元表达式一定为true此时,创建Object数组,T也是Object类型,毫无问题。如果条件为false,那么获取到元素类型,通过反射创建数组,T的类型就是传入数组的类型,通过反射也解决了类型转换的问题,即通过反射创建出和原数组类型相同的数组
ArrayList的线程安全问题
ArrayList为什么线程不安全
以添加方法为例子
-
添加方法源码如下
-
public void add(E e) { ensureCapacityInternal(size + 1); // 当前数组元素长度加1得到添加成功时的需要的长度,即最小扩容长度 elementData[size++] = e; return true; }
-
线程不安全地方代码就是elementData[size++] = e;此行代码严格来说执行步骤为:
- elementData[size] = e; 设置size索引处的值
- size++; 然后长度自增
-
因为elementData[size++] = e; 设置和长度增加是非原子性操作。如果多线程同时添加操作的时候,那么则会导致线程安全问题。