ArrayList实现原理
ArrayList本质上为一个动态数组,会根据元素的动态增删改查进行数组大小的变化。以下就常用的方法进行源码的一些个人理解。
目录
构造函数 ArrayList()
- 构建一个长度为0的动态数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
构造函数 ArrayList(int initialCapacity)
- 构建一个长度为
initialCapacity
的数组
this.elementData = new Object[initialCapacity];
add(E e)
Add(E e)
中就不把详细源码copy过来了,只是大概的说下其添加元素思路。在add(E e)
方法中首先会调用一个叫ensureCapacityInternal(int minCapcaity)
的方法进行检测数组长度,如果所需要的数组长度大于当前数组长度(即数组长度不够),那么会调用grow(int minCapacity)
方法进行数组扩容,扩容的时候先计算出扩容后的数组长度,具体计算代码如下:
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
oldCapacity >> 1
是将oldCapacity
转化为二进制然后再将其右移一位,即实际效果为oldCapacity
除2向下取值。也即动态数组每次扩容都是讲长度扩大为原来的1.5倍。
此时,数组已有足够的长度添加元素e
,即将e
依次添加至数组中。
elementData[size++] = e;
return true;
add(int index, E element)
Add(int index, E element)
先通过方法rangeCheckForAdd(index)
进行边界检测,若index
小于0或者大于数组的长度那么抛出异常IndexOutOfBoundsException
。在边界检测之后,同样调用Add(E e)
中的ensureCapacityInternal(int minCapcaity)
方法进行数组扩容操作。但与Add(E e)
不同的是,Add(E e)
中扩容之后只需要在数组最后添加元素e
,而Add(int index, E element)
需要在指定位置index
添加元素e
。插入逻辑如下:
// System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
index++;
可能有的小伙伴对System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
这个方法不是很了解,这个函数是JNI实现的Native方法。我刚开始看的时候也不太了解,但是当看到该函数的参数注释大家可能就会明白大半。
@param src the source array
@param srcPos starting position in the source array.
@param dest the destination array.
@param destPos starting position in the destionation data.
@param length the number of array elements to be copied.
因为是JNI实现的Native方法,所以我也没有再去通过其他途径看里面的源码(我也看不懂JNI代码…),但是通过注释也差不多能猜出大半了。该函数将原数组Object src
从第int srcPos
位开始,复制length
长度到Object dest
数组中,并且是从int destPos
位开始。
在将数组复制之后,然后再把元素element
插入到数组的第index
位中去。同时,将数组长度加1。
remove(Object o)
remove(Object o)
方法其实也比较简单,当调用它时首先会对Object o
进行判断,如果o
为null
的话则对数组进行正序遍历,找到数组中第一个为null
的元素的索引值index
,然后调用私有函数fastRemove(int index)
进行快速移除,重要代码如下:
int numMoved = size - index - 1;
if(numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved); //将数组从index+1位处全部前移一位
elementData[--size] = null;
fastRemove(int index
函数中忽略边界检测,直接根据index
计算出要移动的元素数量numMoved
,然后将index+1
及其之后的元素均向左移动一位到index
开始处的一共numMoved
个元素。因为移除了一个元素,所以最后将数组最后一位赋值null
让GC回收。因为找到了需要移除的元素并将其成功移除,所以return true
。
同理,如果o
不为null
,那么正序遍历找出第一个与Object o
相同的元素的索引值index
,之后逻辑同上,调用私有函数fastRemove(int index)
去进行快速移除。然后return true
。
若在数组中没有找到需要移除的元素object o
,则返回return false
。
remove(int index)
该方法先进行边界检测,看是否index
超出数组长度size
,若index
在合理范围内则进行与上述方法remove(Object o)
一样的逻辑。计算出需要移动元素个数,然后进行元素的移动,最后将最后一位赋值为null
。
set(int index, E element)
这个方法也很简单,先进行边界检测,确保index
不超出数组长度size
,然后将元素element
赋值给数组第index
项。
rangeCheck(index); // 边界检测
E oldValue = elementData(index);
elementData[index] = element; //赋值操作
return oldValue;
以上就是ArrayList的几种很基础的函数,归根到底是基于对动态数组的操作。也即ArrayList的底层其实就是一个动态数组,同时有一点需要稍微提及的是,ArrayList是非线程安全的(线程不安全)。那为什么这么说呢,其实在之前的几种方法源码中对每次对动态数组操作的时候都会维护更新一个变量modCount
,因为不影响我们理解上述方法,所以我在之前的代码中就省略了对modCount
维护更新的那一步。我把源码中的其中一段该变量的注释截取下来可能会帮助大家理解写这个变量的作用是什么。
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
* ...(以下省略)
protected transient int modCount = 0; // 变量modCouont
总体意思大概是说这个数是随着list的结构化修改的时候变化。那什么是结构化修改的时候呢?就是当list的size
变化的时候就是结构化修改的时候。并且在迭代过程中打断可能会导致不正确的结果。
在写的过程中会将modCount
赋值给一个叫expectedModCount
的变量,如果在多线程中进行操作,那么会出现所期望的expectedModCount
变量和modCount
不同,这样就会抛出ConcurrentModificationException
异常。那么要解决这个问题,就需要使用到Java.util.concurrent
包中的类了。
同时,由上面所知,ArrayList
实质为一个动态数组,对其查找十分迅速,但是如果如要添加和删除就要数组元素相对应的左移或右移,效率低。因此,如果查多改少建议使用ArrayList
,如果改多查少那么就可以使用LinkedList
了。有关LinkedList
的讲解请看下一章的介绍。
以上为个人对源码的理解,如果有不对之处,请大家批评指出。十分感谢~