前言:
作为一个名合格的程序员,不看看源码怎么行?ArrayList作为我们常用的一个集合类之一,它的源码不多并且不是很难,比较容易阅读理解。下面我就分析一下,我对ArrayList源码的理解。
一、ArrayList数据结构:
ArrayList的数据结构是一个数组(Object[] elementData),其中有size和capacity两个概念,size代表的数组中存储元素个数,capacity代表的是数组的长度。另外,ArrayList与Array的主要区别是:ArrayList是可变长度、Array是固定长度。那如何ArrayList可变长度的呢?这里就用到了ArrayList的扩容机制的知识,它的扩容机制底层是通过调用 Arrays.copyOf(T[] original, int newLength)实现的,即老数组复制成新数组。
二、ArrayList源码分析:
0.阅读方法:
我阅读的方法是在Idea中创建一个类(MyArrayList.java),把ArrayList类源码复制过来,然后在MyArrayList类中进行debug和做注释。细心的读者可能发现,我为什么又复制了AbstractList类,原因是ArrayList类继承了AbstractList类,AbstractList类中有的成员变量和方法是用protected访问修饰符修饰,protected访问修饰的权限是相同的包和其子类。我写的MyArrayList类继承AbstractList类,不能使用protected修饰的方法和成员变量。
1.成员变量:
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认初始容量,10
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
* 声明一个空实例数组,它是共享的空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 声明一个默认大小(10个)的空实例数组,它是共享的空数组实例。
* 我们要把它与 EMPTY_ELEMENTDATA 区分开,在添加第一个元素时,知道如何去扩张它。
* 即,它与 EMPTY_ELEMENTDATA 的区别在于当第一个元素被加入进来的时候它知道如何扩张
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 存储ArrayList元素的数组缓冲区。ArrayList的容量是此数组缓冲区的长度。
* 添加第一个元素时,elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA的任何
* 空ArrayList都将扩展为DEFAULT_CAPACITY
*/
// non-private to simplify nested class access 非私有以简化嵌套类访问
transient Object[] elementData;
/**
* 数组列表的大小
* @serial
*/
private int size;
2.构造函数:
/**
* 构造函数,指定容量大小
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public MyArrayList(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
*/
public MyArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造函数,构造一个包含指定集合元素的列表,顺序按照集合元素的顺序
*
* @param c 要将其元素放入此列表中的集合
* @throws NullPointerException
*/
public MyArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray 可能(不正确)返回 Object[] (see 6260652)
if (elementData.getClass() != Object[].class){
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
} else {
// 替换为空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
3.get方法:
/**
* 返回此列表中指定位置的元素
*
* @param index 要返回的元素的索引
* @return 此列表中指定位置的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
@Override
public E get(int index) {
// 检查给定索引是否在范围内
rangeCheck(index);
// 返回索引位置的元素
return elementData(index);
}
get方法比较简单, 首先调用rangeCheck(int index)方法检查给定索引是否在范围内,如果没有在范围内抛出IndexOutOfBoundsException异常,如果在范围内,则调用elementData(int index)方法获取元素。简单点说就是通过索引值(数组下标)获取数组中元素。
rangeCheck(int index):
/**
* 检查给定索引是否在范围内。如果不是,则抛出适当的运行时异常。此方法不检查索引是否为负:它总是在数组访问之前使用,
* 如果索引为负,则数组访问将抛出ArrayIndexOutOfBoundsException。
*
*/
private void rangeCheck(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
elementData(int index):
/**
* 获取索引位置的元素
*
* @param index
* @return
*/
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
4.set方法:
/**
* 将此列表中指定位置的元素替换为指定元素
*
* @param index 要替换的元素的索引
* @param element 要存储在指定位置的元素
* @return 返回该索引位置的老元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
@Override
public E set(int index, E element) {
// 检查给定索引是否在范围内
rangeCheck(index);
// 获取该索引位置的老元素
E oldValue = elementData(index);
// 替换该索引位置的元素
elementData[index] = element;
// 返回该索引位置的老元素
return oldValue;
}
set方法也比较简单, 首先也是检查给定索引是否在范围内,如果在范围内,则调用elementData(int index)方法获取老的元素,然后替换该索引位置的元素,最后返回老元素的值。
5.add方法:
/**
* 将指定的元素追加到此列表的末尾
*
* @param e 要添加到此列表的元素
* @return 只返回为true,这是实现Collection接口add()原因,只能是boolean类型
*/
@Override
public boolean add(E e) {
// 确保需要的容量(校验验添加元素后是否需要扩容),并且 Increments modCount
ensureCapacityInternal(size + 1);
// 添加元素,并size自增一(注意这里是i++,先赋值,后+1。原因:数组索引从0开始,数组长度从1开始)
elementData[size++] = e;
// 返回成功
return true;
}
add方法就相对复杂一些,这里会涉及到“数组容量的初始化”、“数组的扩容”、“fail-fast机制”等,在这里主要介绍说一下“数组容量的初始化”和“数组的扩容”,“fail-fast机制”在后边的章节会详细介绍一下。 add方法首先调用ensureCapacityInternal(int minCapacity),该方法会进行“数组容量的初始化”、“数组的扩容”使用“fail-fast机制”等的操作,然后再添加元素并size自增一(JDK源码中,比较喜欢一行代码干多件事情),最后返回true。
ensureCapacityInternal(int minCapacity):
/**
* 确保需要的容量,数据组为空时初始化数组的默认长度
*
* @param minCapacity
*/
private void ensureCapacityInternal(int minCapacity) {
// 判断elementData是不是为空,即判断ArrayList是否为空
// 如果是空的,minCapacity等于默认容量与minCapacity中的大值,即初始化数组的容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
该方法,首先判断elementData是不是为空(判断ArrayList是否为空),如果是空的,minCapacity等于默认容量与minCapacity中的大值,这里操作就是在初始化数组的容量。然后调用ensureExplicitCapacity(int minCapacity)方法。
ensureExplicitCapacity(int minCapacity):
/**
* 确保明确需要的容量,判断是否需要扩容
*
* @param minCapacity
*/
private void ensureExplicitCapacity(int minCapacity) {
//fail fast机制的维护的计数值
modCount++;
// 最小容量大于数组长度时,扩容
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
}
}
该方法, 首先会维护fail fast机制的计数值,然后判断最小容量是否大于数组长度,大于则需要扩容,调用 grow(int minCapacity)。
grow(int minCapacity):
/**
* 扩容方法
* 增加容量以确保它至少可以容纳由最小容量参数指定的个元素
*
* @param minCapacity 所需的最小容量
*/
private void grow(int minCapacity) {
//老容量
int oldCapacity = elementData.length;
//新容量=老容量+老容量向右移1位(缩小一倍,就是0.5倍),即1+0.5=1.5
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新容量小于所需的最小容量时,新容量赋值为所需的最小容量时
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
//新容量大于MAX_ARRAY_SIZE时,调用hugeCapacity(int minCapacity)方法
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// 扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
该方法, 前几行都是在确定扩容后数组的新容量大小,然后调用了Arrays.copyOf(T[] original, int newLength)进行扩容。
hugeCapacity(int minCapacity):
/**
* 计算最大容量
*
* @param minCapacity
* @return
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) {
throw new OutOfMemoryError();
}
// 最小容量大于MAX_ARRAY_SIZE,则使用Integer.MAX_VALUE,反之MAX_ARRAY_SIZE
// 注意:使用Integer.MAX_VALUE存在内存溢出的风险
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
6.remove方法:
/**
* 删除此列表中指定位置的元素,将任何后续元素向左移动(从其索引中减去一个)
*
* @param index 要删除的元素的索引
* @return 返回该索引位置的老元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
@Override
public E remove(int index) {
// 检查给定索引是否在范围内
rangeCheck(index);
// 修改次数自增
modCount++;
// 获取该索引位置的老元素
E oldValue = elementData(index);
// 计算需要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0) {
// 将index+1位置及之后的所有元素,向左移动一个位置
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
// size自减,并且原数组的最大索引位置设置为null,允许GC完成其工作
elementData[--size] = null;
// 返回该索引位置的老元素
return oldValue;
}
remove方法,首先检查给定索引是否在范围,如果在范围内,modCount会自增1;然后获取该索引位置的老元素;之后计算需要移动的元素个数,如果需要移动,会调用System.arraycopy()方法,将index+1位置及之后的所有元素,向左移动一个位置;最后,size自减,并且原数组的最大索引位置设置为null,允许GC完成其工作并返回老元素;
7.clear方法:
/**
* 从列表中删除所有元素
*/
@Override
public void clear() {
// 修改次数自增
modCount++;
// 允许GC完成其工作
for (int i = 0; i < size; i++) {
elementData[i] = null;
}
size = 0;
}
clear方法也比较简单, 首先修改次数自增,然后将数组中所有元素设置为null。
8.其他方法:
其他方法,我这里就不多分享了,大家可以看一看我的MyArrayList.java文件,其中添加和删除的另几个方法,还是值得看一看的。
三、错误机制fail-fast:
1.fail-fast机制介绍:
在ArrayList源码类注释上有对“fail-fast”机制的两段描述,内容如下:
* <p><a name="fail-fast">
* 错误机制fail-fast机制
* The iterators returned by this class's {@link #iterator() iterator} and
* {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
* if the list is structurally modified at any time after the iterator is
* created, in any way except through the iterator's own
* {@link ListIterator#remove() remove} or
* {@link ListIterator#add(Object) add} methods, the iterator will throw a
* {@link ConcurrentModificationException}. Thus, in the face of
* concurrent modification, the iterator fails quickly and cleanly, rather
* than risking arbitrary, non-deterministic behavior at an undetermined
* time in the future.
* 由iterator()和listIterator()返回的迭代器使用fail-fast机制,如果列表在迭代器创建后,任何时候进行结构上的修改,除了迭代器自身之外的任何方式,
* 即ListIterator#remove()或ListIterator#add(Object),迭代器将抛出ConcurrentModificationException异常。因此,在面对并发修改时,
* 迭代器会快速利索地失败,而不是在将来某个不确定的时间,冒着任意的、不确定的行为的风险。
*
*
* <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
* as it is, generally speaking, impossible to make any hard guarantees in the
* presence of unsynchronized concurrent modification. Fail-fast iterators
* throw {@code ConcurrentModificationException} on a best-effort basis.
* Therefore, it would be wrong to write a program that depended on this
* exception for its correctness: <i>the fail-fast behavior of iterators
* should be used only to detect bugs.</i>
*
* 注意,迭代器的fail-fast并不能得到保证,它不能够保证一定出现该错误。一般来说,fail-fast会尽最大努力
* 抛出ConcurrentModificationException异常。因此,为提高此类操作的正确性而编写一个依赖于此异常的程序
* 是错误的做法,正确做法是:ConcurrentModificationException应该仅用于检查bug。
简单来说就是:迭代器在遍历集合对象时,如果遍历过程中对集合对象的内容进行了修改(增删改),则会抛出Concurrent ModificationException。它的实现原理也很简单:迭代器在遍历时,首先将modCount变量赋值给expectedmodCount变量,如果在遍历集合期间,集合的内容发生变化,modCount的值就会改变。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
Tips:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出(ABA问题)。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
2.为什么在增强for循环中的删除会抛ConcurrentModificationException?
上面介绍了“fail-fast”机制,估计大家也能想到“增强for循环中的删除会抛ConcurrentModificationException”与“fail-fast”机制有关,那为什么有关系呢?聪明的人可能已经想到了“增强for就是通过迭代器实现的”,下面我们来证明一下吧,准备一个ForeachCode.java类:
package basicknowledgecode;
import java.util.List;
/**
* 〈一句话功能简述〉<br>
* 〈强for循环原理〉
* 通过查看字节码(.class文件),得知强for循环是通过Iterator(迭代器)实现的
*
* @author hanxinghua
* @create 2020/5/8
* @since 1.0.0
*/
public class ForeachCode {
List<Integer> list;
/**
* 查看foreach
*
*/
public void foreachSeeClass() {
for (Integer i : list) {
}
}
}
首先,我们把.java文件编译成.class文件,然后,通过 javap -verbose ForeachCode命令查看class字节码,相关class字节码如下:
通过字节码圈红框的地方,可以看出增强for使用的是迭代器。
四、MyArrayList类源码:
springboot_demo: springboot_demo 《 springboot-java-basic-note模块-sourcecode.list.arraylist包》
五、参考文件:
JDK1.8 ArrayList类源码;
java中快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?https://www.cnblogs.com/songanwei/p/9387745.html