[JDK源码分析] ArrayList底层实现原理
ArrayList是List接口的动态数组实现,允许存放所有元素,包括null。除了实现List接口外,ArrayList类还提供了操作数组大小的方法。功能与Vector类似,但是不同步。
类定义
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList类继承1个抽象类,实现4个实现。
- Serializable接口:标记接口,表示可序列化。
- RandomAccess接口:标记接口,表示列表支持快速随机访问,LinkedList则不支持随机访问,没有该接口。
- Cloneable接口:标记接口,表示可复制。
- List接口:作为列表规范,规定需要实现的功能,LinkedList同样实现List接口。
- AbstractList抽象类:提供List接口骨架实现,尽可能较少实现随机访问数据所需的操作。对于连续访问数据(如链表),优先使用AbstractSequentialList,内部主要是迭代器实现。
【扩展】对于随机存储数据和连续访问数据,可以参考Collections.binarySearch()方法的源码。
// Collections public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) { if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD) // for循环遍历实现 return Collections.indexedBinarySearch(list, key); else // 迭代器实现 return Collections.iteratorBinarySearch(list, key); }
性能比较:ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快!!
类注释解读
ArrayList是List接口的动态数组实现,允许存放所有元素,包括null。除了实现List接口外,ArrayList类还提供了操作数组大小的方法。功能与Vector类似,但是不同步。
时间复杂度:get、set以及迭代器操作时间复杂度是O(1)。add操作以均摊常数时间运行,也就是说,添加n个元素需要O(n)时间。其他操作时间复杂度为O(n),以线性时间运行。与 LinkedList 实现相比,该实现的常数因子更低。
扩容:ArrayList实例的容量capacity是指存储元素的数组大小,总是大于等于列表大小。随着不断添加元素,容量会自动扩展。在添加大量元素之前,ArrayList采用ensureCapcity方法来增加实例容量,有效的减少递增式再分配的数量。
线程安全性:该实现不是同步的。多个线程同时访问一个ArrayList实例,并且存在至少一个线程对列表做结构性修改时,必须进行同步。针对于ArrayList,结构性修改是指任何添加或删除一个或者多个元素,或者显式调整底层数组大小的操作;修改元素值不是结构性修改。一般通过对自然封装该列表的对象进行同步操作来完成,如果不存在这样的对象,应该采用Collections.synchronizedList将当前列表封装起来(最好是在创建时完成,以防意外对列表的不同步访问)。
ArrayList的迭代器操作具有快速失败(Fast-Fail)机制,在创建迭代器后,除非是自身remove或add操作,任何其他对列表的结构性修改,迭代器都会抛出ConcurrentModificationException。
属性与常量
ArrayList架构比较简单,底层就是一个Object数组–elementData。
- DEFAULT_CAPACITY 表示数组初始容量,默认为10。
- size表示列表元素个数,没有volitile修饰,线程不安全。
- modCount是AbstractList抽象类中的字段,用于记录当前数组结构性修改次数。
【注】elementData是transient修饰的,不被序列化。
其他常量
// 可分配的最大数组大小,由于有些虚拟机会保留位置,分配更大数组会导致OutOfMemoryError private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 共享的空数组实例,用于空实例。 private static final Object[] EMPTY_ELEMENTDATA = {}; // 共享的空数组实例,用于默认大小的空实例。 // 与EMPTY_ELEMENTDATA做区分,以便了解在添加第一个元素时需要扩展多少数组容量。 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
类初始化
无参构造方法 – 默认容量
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("Illegal Capacity: "+ initialCapacity); } }
大于0,创建指定大小的Object数组;等于0,初始化共享空数组EMPTY_ELEMENTDATA;其他抛出参数不合法异常。
指定集合作为参数的构造方法
public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // 如果c.toArray返回的不是Object[]类型,转为Object if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 集合元素个数为0,使用空数组代替 this.elementData = EMPTY_ELEMENTDATA; } }
需要特别注意的是,ArrayList无参构造初始化时,默认创建空数组,在首次添加元素时才会初始为10。
新增与扩容实现
ArrayList类add(E e)方法,将指定元素添加到列表末尾,时间复杂度为O(1),但考虑到动态数组扩容的性质,添加操作为均摊O(1)。
public boolean add(E e) { // 判断底层数组容量是否足够,不够则进行扩容 ensureCapacityInternal(size + 1); // Increments modCount!! // 直接赋值--线程不安全 elementData[size++] = e; return true; }
在源码中,添加元素操作分为了两步:扩容和赋值。
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果底层数组仍为默认空数组,设定为初始值10 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; // 扩容后容量是当前数组容量的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容后容量仍然小于期望最小容量,设为期望容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果扩容后容量大于JVM所能分配数组容量的最大值Integer.MAX_VALUE - 8,则使用Integer的最大值Integer.MAX_VALUE if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 创建新数组,复制原数组所有元素到新数组后,最后指针指向新数组 elementData = Arrays.copyOf(elementData, newCapacity); }
在新增过程中,
- 没有对值做校验,因此ArrayList是允许空值的。
- 数组操作,需要特别注意边界值,例如数组下界不能小于0,上界不能大于Integer.MAX_VALUE。
- 扩容结束的赋值语句elementData[size++] = e;采用的是直接赋值,没有锁,此处线程不安全。
- 数组拷贝操作,内部调用System#arraycopy方法,是一个native方法。
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
源码问题:计算最小期望容量方法中,当elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,应该直接返回默认值,而不需要Math.max判断。因为只有ArrayList无参构造初始化时,elementData才是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果底层数组仍为默认空数组,设定为初始值10 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
删除操作
ArrayList删除元素,可以通过底层数组索引删除,通过值来删除以及其他批量删除。
通过底层数组索引删除
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); // 置空末尾元素,便于GC回收内存 elementData[--size] = null; return oldValue; }
通过元素值来删除
public boolean remove(Object o) { // 值为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++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
根据值删除,存在值为空和非空两种情况,在遍历时分别通过==和equals方法来匹配。需要特别注意的是,删除的是匹配到的第一个元素,而不是所有匹配元素。
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; // clear to let GC do its work }
除了边界校验,fastRemove方法与remove(int index)方法代码几乎一致。同时我们也发现了System.arraycopy的使用,这一操作在涉及到数组操作中有着广泛应用,例如String。
其他–查询和更新元素
获取索引位置元素
public E get(int index) { // 边界校验,index大于等于size,抛出IndexOutOfBoundsException rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; }
elementData数组中元素是Object类型,获取指定类型需要强转。
更新指定索引位置元素
public E set(int index, E element) { // 边界校验 rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
ArrayList迭代器
ArrayList中提供了多种迭代器实现,例如Itr迭代器、ListIterator迭代器。Itr迭代器是AbstractList.Itr的优化版本,而ListIterator迭代器继承Itr,并对其提供了一些扩展,例如add和set操作。
Itr迭代器
private class Itr implements Iterator<E> { // 下一个元素的索引位置 int cursor; // 最新被返回的元素索引,如果没有更多元素,设为-1 int lastRet = -1; // 期望结构修改次数 int expectedModCount = modCount; Itr() {} ... final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
迭代器变量中,expectedModCount是校验数组结构修改的关键变量。而lastRet变量用于标识是否还有元素可以删除,迭代开始值为-1,无元素;remove操作后,lastRet值也置为-1,表示不能连续删除。
Itr迭代器实现Iterator接口,主要包含了3个方法。
hasNext操作
// 是否还有未迭代的元素 public boolean hasNext() { // 如果下一个元素位置是size,表示没有元素可以迭代 return cursor != size; }
next操作
// 返回下一个元素值 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; // 返回元素值,并设置lastRet return (E) elementData[lastRet = i]; }
remove操作
public void remove() { // 如果数组中没有元素 if (lastRet < 0) throw new IllegalStateException(); // 校验迭代过程中,数组是否有结构性修改 checkForComodification(); try { // 调外部类remove,删除当前迭代元素 ArrayList.this.remove(lastRet); // 因为是删除操作,迭代索引位置不变 cursor = lastRet; // -1表示元素已经被删除 lastRet = -1; // 修改期望modCount值 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
我们可以关注到:
- 迭代器的删除操作调用了外部类ArrayList的remove(int index)方法,方法必然会导致数组结构性修改(modCount++)。迭代器的remove操作会对expectedModCount进行重新赋值,这也就是自身迭代器add和remove操作不会抛ConcurrentModificationException的原因。
- remove操作中,将lastRet置为1,避免重复删除。
- remove操作通常与next方法组合使用,不单独使用。
Fail-fast机制
迭代器有expectedModCount属性用于保存迭代器被调用时的modCount值。如果在迭代过程中,迭代器的增add、删remove、改set、查next/previous 4种操作方法会对modCount进行校验是否被修改。如果被修改,抛出ConcurrentModificationException。
所谓的Fail-Fast机制核心就是下面两句
int expectedModCount = modCount; 条件表达式判断 modCount == expectedModCount
modCount属性继承自AbstractList,标识当前List结构修改的次数。这是一种fail-fast行为,用于防止并发导致不一致的结果。
protected transient int modCount = 0;
【注】注意的是modCount属性并不是volatile的。查阅ConcurrentModificationException源码注释,我们可以发现该异常并不总是表示对象被不同线程并发修改。同一个线程也可能触发该异常。例如,假如集合具有fail-fast迭代器,如果在进行迭代操作时修改数据,迭代器会抛出该异常。
* Note that this exception does not always indicate that an object has * been concurrently modified by a <i>different</i> thread. If a single * thread issues a sequence of method invocations that violates the * contract of an object, the object may throw this exception. For * example, if a thread modifies a collection directly while it is * iterating over the collection with a fail-fast iterator, the iterator * will throw this exception.
- 点赞 5
- 收藏
- 分享
- 文章举报
- Java中HashMap底层实现原理(JDK1.8)源码分析
- Java面试绕不开的问题: Java中HashMap底层实现原理(JDK1.8)源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- 59_数组_模拟ArrayList容器的底层实现_JDK源码分析ArrayList
- Java中HashMap底层实现原理(JDK1.8)源码分析
- (转载)Java中HashMap底层实现原理(JDK1.8)源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- ConcurrentHashMap底层实现原理(JDK1.8)源码分析
- java学习之旅59--模拟ArrayList容器的底层实现_JDK源码分析ArrayList
- Java中HashMap底层实现原理(JDK1.8)源码分析
- 数组第二十四课,模拟ArrayList容器的底层实现,JDK源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- 顺序线性表 ---- ArrayList 源码解析及实现原理分析
- java_模拟实现ArrayList(底层原理分析并实现相关功能)
- Java集合ArrayList实现原理——源码分析
- 集合List和ArrayList等实现类的底层原理分析
- java并发容器CopyOnWriteArrayList实现原理及源码分析