ArrayList实现
ArrayList集合继承AbstractList类,实现List接口
要注意的是AbstractList 抽象类中的modCount表示List数据结构被修改的次数.
由于ArrayList在实现过程中JDK8和JDK11基本上一样,所以就以JDK8来说。
先来看看ArrayList类声明的参数.
// 初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享数组实例 不保存数据
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于空实例的共享数组实例 用于默认大小空实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存放数据的数组 ArrayList存数据的真正数组.
transient Object[] elementData;
// Object中所能存放数据的个数. 也就是说当前ArrayList所能支持的大小是Integer.MAX_VALUE-8.
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// size 即表示当前有多少元素,也表示元素的下标
// size -1 就是第size个元素在Object数组中的下标.
private int size ;
要注意的是ArrayList没有和HashMap一样的扩容因子,但是ArrayList也会进行动态扩容,ArrayList进行扩容的是以当前数组大小的一半进行扩容,也就是扩容当前一半. 看不懂,没关系,我们向下慢慢看。
我们看ArrayList初始化的空的构造函数可知,其构建的是一个空的elementData的Object数组,我们来看具体的代码.
我们以add(E) 为例子来进行详细说明
// 我们在初始化的时候,基本上都是用的该构造函数进行初始化的,而我们知道DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 的含义就是一个空的Object数组,所以在初始化的时候并没有初始化一个10个大小的Object数组elementData
// 我们只是初始化了一个空的Object数组.
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
既然我们知道了ArrayList进行初始化为一个空的Object数组,那么为什么网上都说ArrayList的初始大小为10呢?
当我们看过ArrayList的add源码之后就一目了然了。
ArrayList在进行第一次add的时候,会进行判断当前elementData是否能装下本次插入的数据,如果不能装入则用移位的方式进行动态扩容,如果能装入,那么就让如到elementData[size]位置上将当前要出入的数据.
好了,只说理论应该看的不太明白,那我们就来看看ArrayList的源码.
// 该方法是我们初始化ArrayList直接调用的add方法.
// 具体的扩容是在grow()方法中进行扩容的,而grow()扩容方法是通过ensureCapacityInternal方法内调用.
public boolean add(E e) {
// 该方法的作用就是判断当前elementData数组上是否有位置插入当前要增加的e元素,如果不能确保当前位置上一定能插入e元素,
// 那么就会自动调用grow()方法进行扩容.
ensureCapacityInternal(size + 1); // Increments modCount!!
// 当JVM执行到了这里的时候就说明此时elementData一定能在size位置上插入e元素
elementData[size++] = e;
// size++ 表示的是 先在elementData[size]位置上插入元素e,然后才对size++.
return true;
}
那么你们有没有想知道ensureCapacityInternal()
方法到底做了什么,就能触发grow()方法进行动态扩容?
我们直接上代码 以下是扩容函数的整个调用链.
// 扩容第一步
private void ensureCapacityInternal(int minCapacity) {
// 如果是第一次调用add方法增加一个元素,那么calculateCapacity方法返回的是DEFAULT_CAPACITY值10
// 也就是说ArrayList真正的初始化时在第一次调用的时候进行初始化的.
// 如果ArrayList不是第一次进行调用add方法,那么将会返回minCapacity
// 所以也就是说,该方法的目的就是判断当前ArrayList是否为空的.
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 该方法对要出入的位置进行检测.
// 该方法在初始化ArrayList进行第一次调用的时候将会返回DEFAULT_CAPACITY. 也就是返回10.
// 该方法的根本目的就是判断当前ArrayList实例是否为空的,
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前elementData为空的元素,那么就返回原始自定义的DEFAULT_CAPACITY
// 默认的DEFAULT_CAPACITY 为10.
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 初始化ArrayList之后,在第一次向ArrayList中增加第一个元素的时候,将会触发该判断语句.
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 该方法会修改ArrayList的数据结构
// 该方法会判断是否进行扩容.
private void ensureExplicitCapacity(int minCapacity) {
// 表示修改ArrayList数据结构的次数+1
// 不管是否进行扩容的都先增加modCount的值是因为add方法会修改ArrayList的数据结构
// 而在该方法中进行修改modCount的值是最合适不过的了
// 不管add方法需不需要进行扩容,都能对modCount进行+1, 因为add方法的修改了ArrayList的数据结构.
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 如果当前elementData中存放的数量+1 要比elementData.length大,那么就进行扩容.
grow(minCapacity);
}
那么我们就来看看JDK8中ArrayList具体是怎么进行扩容的. 在看扩容源代码之前我们需要稍微了解下Java中的移位操作。
我们写的Java代码在底层都是01运算,因为代码都是依赖计算机进行运算的,那么使用01运算也就是移位来运算,将会很大的提升性能.
在Java中左移是乘法,右移是除法
例如:1<<2 就是12的2次幂 1<<4 就是12的四次幂
1>>2 就是1/2的2次幂 1>>4 就是1/2的四次幂
如果了解了Java中移位运算了解了,那么我们就来看ArrayList中的扩容方法具体是怎么实现的.
// 扩容的具体方法. 传进来的是 size+1 也就是要保存e的位置.
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 右移一位 也就是要右移的一半. newCapacity是扩容之后的容量.
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 如果右移运算的结果要比size+1小,那么扩容后的Object数组大小为size+1
// 如果友谊运算的结果要比size+1小,那么就说明newCapacity没有起到扩容的关键性作用,那么此时就使用size+1来进行扩容一个位置.
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果右移之后的数值大小要比当前数据结构所能允许的大,那么就执行该方法
// 该方法返回值分为三种情况,第一种是size+1<0则抛出OutOfMemoryError错误
// 如果size+1 比Integer.Max_VALUE-8还要大,那么就返回Integer的最大值
// 否则返回Integer的最大值-1
newCapacity = hugeCapacity(minCapacity); // 具体的hugeCapacity待会将介绍,这个方法键返回真正的能存入数据的大小.
// minCapacity is usually close to size, so this is a win:
// 利用Arrays.copyOf进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
// Arrays.copyOf(arg1,arg2) 参数含义:
// 第一个参数: 要复制的具体的数组
// 要复制的具体的数组的长度 假如说 (elementData,100) 那么就新建一个Object数组,只是将这elementData数组里面已经存在的值存入
// 新数组里面相同的位置,只不过是将长度变为100.
// 扩容完毕.
}
当扩容后的值大于当前数据结构(Object数组)所能允许的最大值的时候,将会调用hugeCapacity方法
// 只要调用该方法就说明,扩容之后的newCapacity要大于MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果size+1 要比Integer.MAX_VALUE要大(此时是不能以MAX_ARRAY_SIZE进行扩容的)时,就返回Integer的最大值,
// 否则就返回Integer的最大值减去8.
// 从方法调用角度来看, minCapacity传进来的值一定是size+1 而在调用该方法的时候就已经对newCapacity和minCapacity判断过了.
// 如果newCapacity要大于MAX_ARRAY_SIZE 那么才进行调用的.
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
到这里扩容ArrayList增加就已经说完了,总结一下:
- ArrayList在new的时候内部elementData是不会进行初始化的,而是在第一次调用add方法的时候才进行初始化为10个大小的elementData的.
- ArrayList在进行扩容的时候是以当前elementData的长度的1/2进行扩容的,如果非要说一个ArrayList的扩容因子的话,那么0.5就是ArrayList的扩容因子.
- ArrayList有最大长度限制,也就是说ArrayList最大能存储Integer.MAX_VALUE个值.
我们对扩容流程进行下总结:
ArrayList在调用add方法的时候,会先判断当前位置也就是size位置上是否能存储数据,如果不能则存储数据,否则就进行扩容。但是这个add方法应该分开来看,首先我们来看第一次进行调用add方法的时候将会在calculateCapacity()方法判断是不是一个空的数组,如果是一个空的数组那么就返回默认的大小进行初始化.初始化不是调用的nwe而是调用的Arrays.copyOf()方法. 如果不是第一次添加数据,那么将会执行grow(size+1)方法进行扩容.
上面我们看了ArrayList的add()方法,那么我们现在来看看ArrayList是怎么进行元素删除的?
在看源码之前,不如我们先猜猜,ArrayList是怎么进行删除的。如果不看JDK集合源码你是否会觉得ArrayList是通过将指定索引出的元素设置为null来进行删除的 ? 那好,我们就带着这个疑问去探索ArrayList的源码.
// 移除指定位置上的元素, 然后将整个数组向左移动,索引数-1.
// index传进来的值就是索引值不需要-1.
public E remove(int index) {
// 判断数组下标是否越界.
rangeCheck(index);
// 修改 修改表结构的次数 自增
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 将指定源数组中的数组从指定位置复制到目标数组的指定位置
// index+1 表示将要删除的那个元素后一个元素的个数不是下标 如果要表示下标应该是index
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 这行代码一定会执行, 先将size-- 然后再见这个位置上的元素赋值为null
// 将目标数组中的第index个位置上的元素赋值为null,因为通过System.arraycopy方法已经,将目标数组中的第index个
// 个位置上的元素赋值到了目标数组中的第index个位置上
// 也就是说在目标数组中,第index个位置上的元素和第index+1个位置上的元素是相同的
// 此时要通过将第index个位置上的元素赋值为null ????? 这个不就表示要删除刚刚被复制过来的元素吗?
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
让我们来看看是怎么检查数组下标越界的
// 如果当前位置大于或者等于Object数组所存的数据,那么就抛出数组下标越界异常.
private void rangeCheck(int index) {
if (index >= size)
// size 就是当前要存入数据的索引.
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
举个删除的例子
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
// 此时size为1
list.add(1);
// 移除第一个元素
list.remove(0);
// 如果remove参数是在的位置而不是索引的话那么就会抛出数组下标异常异常.
// IndexOutOfBoundsException
}
我们来看看System.arraycopy(arg1,arg2,arg3,arg4,arg5)四个参数的意思
第一个参数是:第一个参数是源数组
第二个参数是:第二个参数是源数组中的起始位置 不是下标
第三个参数是:第三个参数是目标数组
第四个参数是:目标数组中的起始位置也就是要接受被覆盖的位置,不是下标.
第五个参数是:要复制的数组元素的个数.
总结下ArrayList的删除:
- remove参数传进来的是索引.
- remove操作会修改ArrayList的数据结构修改的次数.
- remove操作是通过使用System.arraycopy()函数来进行数组的删除,这样会把target目标数组中的第size-1个位置设置为null,因为将中间的删除了之后,尾部是占用空间的,所以我们要将尾部所占的内存空间释放掉.
到此ArrayList的增加和移除就已经讲述清楚了!
既然已经了解了ArrayList的基本增加和修改了,那么我们再来修改下ArrayList是否线程安全? 如果不是线程安全的,是为什么?
ArrayList是线程不安全的集合框架,在并发情况下不建议使用ArrayList.
我们来看个例子先:
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
// 此时size为1
list.add(1);
Iterator<Integer> iterator = list.iterator();
list.add(2);
while (iterator.hasNext()) {
// 这里将会抛出异常.
Integer next = iterator.next();
}
}
当我们不去运行的时候,可能会发现这没有什么错误,但是当我们运行该例子的时候将会发现,这里有个异常发生了。
java.util.ConcurrentModificationException
这里我把list.add(2) 放在了list.iterator之后,只是为了模拟出现错误,但是在多线程的情况下是很有可能出现这种错误的。
我们看下ArrayList中迭代器源码
public E next() {
// 在调用next的时候,会调用方法检查expectedModCount值是否与modCount相同
// expectedModCount是在调用iterator方法初始化一个Ltr对象的时候将ArrayList当前实例的modCount赋值给expectedModCount
// 当我们在外部继续修改ArrayList的时候将会修改modCount值,这个时候expectedModCount将和modCount不同步
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];
}
// 调用这个方法进行检查,并抛出ConcurrentModificationException异常.
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
所以我们说ArrayList是线程不安全的集合框架,在多线程下modCount不是多个线程共享的变量,所以抛出ConcurrentModificationException异常.
到了这里我们已经明白了ArrayList底层的实现和ArrayList为什么不是线程安全的集合,并且给出了一个模拟不安全的例子。
如果觉的上述文章有哪里写的不对的地方还请在留言区指出,当看到了一定进行修改.