ArrayList源码解析--值得深读

时间:2023-03-09 06:33:06
ArrayList源码解析--值得深读

ArrayList源码解析

基于jdk1.8

ArrayList的定义

类注释

  • 允许put null值,会自动扩容;
  • size isEmpty、get、set、add等方法时间复杂度是O(1);
  • 是非线程安全的,多线程情况下推荐使用CopyOnWriteArrayList或者Vector。
  • 增强for循环或使用迭代器过程中,如果数组大小被改变会抛出异常。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

从源码中我们可以看出,ArrayList继承了AbstractList,实现了List接口,RandomAccess,Cloneable,Serializable接口。

ArrayList源码解析--值得深读

实现List接口提供了元素的添加、删除。修改。遍历等功能。
实现RandomAccess接口,他是一个标志性接口,表明实现这个接口后,List集合支持随机访问。
实现Cloneable接口表明ArrayList可以被克隆
实现Serializable接口表示ArrayList支持序列化,能被传输。

ArrayList是一个动态数组,与普通数组相比他的容量可以动态的扩容。

ArrayList的有关属性

 private static final long serialVersionUID = 8683452581122892189L;

    /*默认容量大小是10*/
private static final int DEFAULT_CAPACITY = 10; /**
* 空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {}; /**
* 用来共享空数组实例
* 我们把它和EMPTY_ELEMENTDATA数组区分出来,当添加第一个元素的时候知道需要扩增多少容量
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /**
* 保存ArrayList数据的数组
*/
transient Object[] elementData; // non-private to simplify nested class access /**
* 集合包含元素的个数
*/
private int size;

ArrayList的构造函数

public ArrayList(int initialCapacity) {
// 如果创建的时候传入的有值,并且大于0,就直接创建大小是传入的值的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果创建的时候没有指定大小,就是一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//传入负数会抛出非法参数异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
} /**
* 构造一个空数组,当增加第一个元素的时候才确定数组容量是10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
} /** *构造函数的参数是一个集合,如果集合为null,会抛空指针异常
*
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
//如果集合的大小不为0 就通过Arrays.copy方法,把Object数组的元素一一复制到elementData数组中
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
// 注意这是java的一个bug,c.toArray()方法返回值可能不是Object数组,这里转成Object类型
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//如果集合里没有元素就是一个空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}

上面的那个// c.toArray might (incorrectly) not return Object[] (see 6260652)一般情况下都不会触发这个bug ,这里演示一下是怎么出现的。

public void testConstructor() {
List<String> arrayList = Arrays.asList("Hello");
//toArray方法返回Object数组类型
Object[] objects = arrayList.toArray();
log.info(objects.getClass().getSimpleName());
// 打印出来是 String[] //这样写是对的
//objects[0]="Java"; //这样就会报错因为数组元素的类型是String
objects[0] = new Object();
}

不过这个bug已经在Java9解决。

新增和扩容的实现

新增元素


/**
* Appends the specified element to the end of this list.
*增加元素到集合的末尾
*
*/
public boolean add(E e) {
//先扩容,使容量+1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
} /**
* 在指定位置增加元素
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
//检查增加的位置是否越界
rangeCheckForAdd(index);
//扩容,size+1
ensureCapacityInternal(size + 1); // Increments modCount!!
//元素的复制,将 index之后的元素往后 移动一位,size++;
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//最后在index位置上赋值
elementData[index] = element;
size++;
}

扩容


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) {
//记录数组被修改的次数加1
modCount++; // 如果我们期望的数组大小大于目前的数组长度,那么就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
} //扩容方法
private void grow(int minCapacity) {
// 先记录旧的数组大小
int oldCapacity = elementData.length;
//扩容后的容量=扩容前的+扩容前的大小/2,也就是说扩容后的值是原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容后的值,小于期望的大小,那扩容后的值就改为期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; //如果扩容后的值大于jvm所能分配的最大值,那么就用Integer.MAX_VALUE.,否则等于MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 最后数组复制,底层是System.arraycopy()方法
elementData = Arrays.copyOf(elementData, newCapacity);
} private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
  • 扩容后数组的容量是原先数组容量的1.5倍。
  • ArrayList中的数组长度最大是Integer.MAX_VALUE,超过这个值,JVM就不会给数组分配内存空间了。
  • 新增时,并没有对新增元素进行严格校验,所以可以新增null。

其实我们可以看出,大多数方法中都用到了Arrays.copyOf方法

public static <T,U> 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;
}

再看System.arraycopy

//src 是原数组,srcPos是原数组的位置,dest是目标数组,destPOS是目标数组的位置,length是拷贝的长度,这个方法是native方法,所以底层可能是C/C++编写的
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);

删除和迭代

ArrayList支持两种删除元素的方式

  1. remove(int index) 按照下标删除
  2. remove(Object o) 按照元素删除,删除第一个匹配到的元素。
public E remove(int index) {
//数组下标越界检查
rangeCheck(index);
//修改次数自增1
modCount++;
//记录被删除的值
E oldValue = elementData(index);
// 记录将要从index后移动的多少个位数到前面,因为size是从0开始,index是从1开始所以需要减一
int numMoved = size - index - 1;
if (numMoved > 0)
//从elementData的index+1处拷贝,拷贝到elementData数组中,从index处开始放置拷过来的元素,拷贝的长度是 numMoved
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 最后一个元素赋值为null,帮助GC return oldValue;
} /**
根据值删除元素
*/
public boolean remove(Object o) {
//y因为ArrayList允许元素值为null,所以删除的时候遍历一次找到第一个是null的值,然后移除。
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++)
//这里是根据equals方法判断值是否相等,相等再根据索引删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
} /*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
//记录修改数加1
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; //最后元素赋值成null,有助于GC
} /**
移除集合中所有元素
*/
public void clear() {
modCount++; // clear to let GC do its work
for (int i = 0; i < size; i++)
//将集合中的每一个元素赋值为null
elementData[i] = null;
//集合元素的个数设置成0
size = 0;
}
  • 新增时因为没有对null做校验所以可以新增null,删除的时候就需要对null值做判断了,按照值删除元素实际上还是根据值找到这个元素的索引,进行删除。比较的方法是通过equals方法。
  • 某一个元素被删除后,ArrayList采用的是直接将这个元素后面的所有元素复制到原数组中,最后的一个元素的值设置成null,让JVM进行GC操作。

接下来看迭代


public ListIterator<E> listIterator(int index) {
//如果索引越界直接抛异常IndexOutOfBoundsException,否则返回从指定下标开始的所有元素列表(按熟顺序),ListIterator接口继承了Iterator接口
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
} /**
* 返回列表中的列表迭代器按顺序返回,迭代器是fail-fast
*/
public ListIterator<E> listIterator() {
return new ListItr(0);
} /**
* 以正确顺序返回元素列表的迭代器
*/
public Iterator<E> iterator() {
return new Itr();
} //ltr是ArrayList的内部类,实现了 Iterator接口,这里只是Itr的部分代码
private class Itr implements Iterator<E> {
int cursor; // i迭代过程中,下一个元素的位置,默认从0开始
int lastRet = -1; // 新增时表示上次迭代过程中,索引的位置;删除场景下是-1
int expectedModCount = modCount;
//期望修改的版本号=实际的修改版本号
Itr() {} public boolean hasNext() {
//判断下一个元素存不存在,只需知道下一个索引的下标是不是数组大小,如果不是返回true,如果是返回false
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)
throw new ConcurrentModificationException();
//下一次迭代时元素的位置
cursor = i + 1;
return (E) elementData[lastRet = i];
} public void remove() {
//当lastRet小于0,也就是说在多个线程操作时,直接将迭代器中的列表删完了,里面没有元素了,就会抛出IllegalStateException
if (lastRet < 0)
throw new IllegalStateException();
//迭代过程中判断版本号有没有被修改,如果被修改抛出ConcurrentModificationException
checkForComodification(); try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
//lastRet=-1防止重复删除
lastRet = -1;
//下次迭代时期望的版本号和实际的版本号一致
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//版本号比较
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

小结

  • ArrayList底层是使用类似于动态数组实现的,默认构造方法初始化的容量是10,但是在没有指定容量大小时区add第一个元素时才会确定出数组的容量。
  • 扩容时,扩容后的长度是扩容前的1.5倍
  • 实现了RandomAccess接口,支持随机读写,get读取元素的性能较好
  • 线程时不安全的,是因为自身的elementData、size、modCount在进行各种操作时,都没有加锁
  • 新增(这里的新增默认是增加到尾部)和删除方法 的时间复杂度是O(1)

以上理解有误的地方欢迎指出,共同进步!