ArrayList 源码深度解析

Feronia ·
更新时间:2024-11-15
· 976 次阅读

ArrayList 源码深度解析 一、重新认识ArrayList 什么是ArrayList?
ArrayList是基于数组实现的List类,封装了一个动态再分配的Object数组,以达到可以动态增长和缩减的索引序列。 长啥样?
ArrayList
如图,是一个长度为6,存储了6个6的数组,下标(index)从0开始,数组(elementData)表示存储的数据本身。 有哪些基本概念? index 表示数组下标; elementData 表示数组本身; DEFAULT_CAPACITY 表示数组初始大小,默认是10; size 表示当前数组的大小,int类型,未使用volatile修饰,非线程安全; modCount 表示当前数组被修改的版本的次数,也就是说当数组结构有变动,就会+1。 二、知其所以然----撸源码 1. 从整个ArrayList.java的类注释开始入手

简单翻译总结如下:
- ArrayList允许put null值,并且会自动扩容;
- size,isEmpty,get,set,Iterator和listIterator方法的时间复杂度是O(1),add方法的复杂度根据添加元素的多少而不同,添加n个元素,时间复杂度O(n);
- 非线程安全,因此在多线程情况下,为了线程安全推荐使用(List list = Collections.synchronizedList(new ArrayList(...)););
- 支持增强for循环,但需要注意的是,由于非线程安全,所以在使用迭代器迭代过程中,如果数组大小被改变,会快速失败,并抛出异常。

2. 初始化

三种初始化方式,源码分析

无参数初始化// 无参数构造器,默认是空数组 public ArrayList() { this.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("Illegal Capacity: "+ initialCapacity); } } 指定初始数据初始化 //指定初始数据初始化 public ArrayList(Collection c) { //elementData 是保存数组的容器,默认为 null elementData = c.toArray(); //如果给定的集合(c)数据有值,则进行拷贝赋值操作 if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) //如果集合元素类型不是 Object 类型,才开始拷贝,否则不执行 if (elementData.getClass() != Object[].class) { elementData = Arrays.copyOf(elementData, size, Object[].class); } } else { // 给定集合(c)无值,则默认空数组 this.elementData = EMPTY_ELEMENTDATA; } }

需要注意的地方

使用无参数构造器初始化时,默认容量时0,而不是传统印象里的10,只有接着对其进行add时才会变成10; 通过观察ArrayList的无参构造器,会发现当它进行初始化时,默认大小是空数组; 可以发现在指定初始数据初始化这种方式里,有一个see 6260652的注释,这是一个1.8版本的bug,在1.9版本的到了解决。解释是(toArray方法可能不会返回Object数组),也就是说当我们有一个ArrayList,里面的元素不是Object类型,比如是String,然后当我们调用toArray方法,得到一个Object数组后,再往这个String类型的Object数组赋值时,就会触发这个BUG,报错。官方查看文档地址:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652 3. 新增元素及扩容 先来看下新增元素核心源码 public boolean add(E e) { //确保数组大小足够,不够需要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! //直接赋值,非线程安全 elementData[size++] = e; return true; } 通过源码,我们可以知道新增就是往数组中添加元素,主要分为两步: 判断是否需要扩容,如需要就执行扩容操作 直接赋值(非线程安全) 接着看下扩容源码的实现 private void ensureCapacityInternal(int minCapacity) { //如果是空数组,就从最小容量和默认容量10之间取最大值 //反之如果初始化时给定了初始值,就按照给定的大小为准,不走if逻辑 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //确保容积足够 ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { //记录数组被修改 modCount++; // 如果我们希望的最小容量大于目前数组的长度,那么就扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); } //扩容,最后把现有数据拷贝到新的数组里面去 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // oldCapacity >> 1 右移运算符,是把 oldCapacity / 2 的意思 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容后的新的容量值 < 我们的期望容量值,那么就取我们的期望容量值 // 也就是扩充的容量至少要满足我们的期望,宁多勿少 if (newCapacity - minCapacity jvm 所能分配的数组的最大值,那么就取Integer 的最大值 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); } //获取Integer的最大值 private static int hugeCapacity(int minCapacity) { if (minCapacity MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } 扩容的本质就是数组的拷贝
我们通过源码可以发现,扩容的核心是一行代码:elementData = Arrays.copyOf(elementData, newCapacity);扩容会首先创建一个符合要求容量的新数组,然后把老数组的数据拷贝过去;我们一层层的点进去,会发现更底层的操作是通过System.arraycopy这个native方法进行拷贝的。/** * @param src 被拷贝的数组 * @param srcPos 从数组那里开始 * @param dest 目标数组 * @param destPos 从目标数组那个索引位置开始拷贝 * @param length 拷贝的长度 * 此方法是没有返回值的,通过 dest 的引用进行传值 */ public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 总结一波 根据扩容的算法,扩容后的大小是原始容量的1.5倍; ArrayList 中的数组容量的最大值是Integer 的最大值(Integer.MAX_VALUE); 新增过程中,没有对值进行严格校验,所以ArrayList允许null值。 新增元素操作很简单,elementData [size++] = e,因此非线程安全。 4. 删除 删除元素的方式有很多,不过代码和思路类似,这里选取删除值的方式来分析 // 根据值去删除 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++) // 注意这里是根据 equals 来判断值相等的 if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } 通过第一步,已经找到了要删除元素的索引,接下来就是根据索引位置来删除元素 private void fastRemove(int index) { // 记录数组的结构变化次数 modCount++; // 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起 // numMoved 用来表示,在删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去 int numMoved = size - index - 1; if (numMoved > 0) // 从 index +1 位置开始被拷贝,拷贝的起始位置是 index System.arraycopy(elementData, index+1, elementData, index, numMoved); //给最后一个元素重新赋值为null,然后让GC来对其进行清除 elementData[--size] = null; } 总结一波 由于新增元素时允许null值,因此删除的时候也允许删除null值; 在数组中找到目标值的索引位置过程是通过equals来判断的,因此使用非基本类型数组元素的时候需要注意; 当某一个元素被删除后,后面的元素会往前移动,以便于维护数组的结构。 5. 迭代器 由于ArrayList是通过实现java.util.Iterator 类来实现自己的迭代器,因此我们先来了解迭代器的源码

迭代器的几个主要参数及常用方法

3个重要参数:int cursor;// 迭代过程中,下一个元素的位置,默认从 0 开始。 int lastRet = -1; // 分场景意义不同,在查找场景下表示上一次迭代过程中,索引的位置;在删除场景下为 -1,避免重复删除。 int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;modCount 表示数组实际的版本号。 3个基本方法:
hasNext 还有没有值可以迭代
next 如果有值可以迭代,迭代的值是多少
remove 删除当前迭代的值

hashNext源码解析

public boolean hasNext() { return cursor != size;//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代 }

next源码解析

public E next() { //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常 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]; } // 版本号比较 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }

remove源码解析

public void remove() { // 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了 if (lastRet < 0) throw new IllegalStateException(); //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常 checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; // -1 表示元素已经被删除,这里也防止重复删除 lastRet = -1; // 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount // 这样下次迭代时,两者的值是一致的了 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }

Iterator源码小结一波

next方法主要完成两件事,首先检验能不能继续迭代,然后找到并返回迭代的值,并为下一次迭代做准备; lastRet在next场景下表示上一次迭代的元素下标,在remove场景下表示-1,用来防止重复删除元素; 如果删除成功,数组当前的modCount就会发生变化,这里会把expectedModCount重新赋值,下次迭代时两者的值就一致了。 ArrayList实现Iterator的源码解析 // 实现 Iterator 接口 private class Itr implements Iterator { // 迭代过程中,下一个元素的位置,从 0 开始,用来控制拿下一个元素 int cursor; // index of next element to return // 新增时表示上一次迭代过程中,索引的位置,删除成功时为 -1 int lastRet = -1; // index of last element returned; -1 if no such // 迭代过程中期望数组修改版本号 int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常 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() { // 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了 if (lastRet < 0) throw new IllegalStateException(); //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常 checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; // -1 表示元素已经被删除,这里也防止重复删除 lastRet = -1; // 删除元素时 modCount 的值已经发生变化,再此赋值给 expectedModCount expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } 三、线程是否安全

由于ArrayList自身的elementData、size、modCount在进行各种操作时,都没有加锁,并且这些变量都是非volatile类型的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。
所以,结合源码我们可以发现当ArrayList作为共享变量时,会有线程安全问题;当ArrayList是方法内的局部变量时,就不存在线程安全问题了。
最开始通过看类注释,我们知道他们推荐我们使用List list = Collections.synchronizedList(new ArrayList(...));来保证线程安全,源码解析如下:

public boolean add(E e) { synchronized (mutex) {// synchronized 是一种轻量锁,mutex 表示一个当前 SynchronizedList return c.add(e); } }

因此,SynchronizedList 其实是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低。


作者:且听_风吟



arraylist 源码

需要 登录 后方可回复, 如果你还没有账号请 注册新账号