ArrayList全面解析
参考学习
- 此处膜拜大佬,五体投地
- https://www.pdai.tech/md/java/collection/java-collection-ArrayList.html (写的太好了,网站很nice,太佩服了!!!)
- https://blog.csdn.net/zxt0601/article/details/77281231 (两个字,清晰!!!适合我这种菜鸡学习)
底层实现
ArrayList本质上是一个数组,这一点在源码中可以很清晰的看到。
既然是个数组,对于查询操作它的效率较高,但是如果是插入删除这类操作,那么它的效率会较低。
普通的数组一旦定义,长度无法改变,这显然是不能满足我们的需求的,所以ArrayList实现了动态扩容,下面会有详细的介绍。
同时,ArrayList利用泛型语法糖限制了类型的定义,但是在编译后都是以Object类型进行存储,这一点在源码中也能很好地看到。
transient Object[] elementData; // 本质是一个数组,类型为Object
为了对ArrayList有一个全面深入的了解,我们从构造器开始,一步一步的进行学习 :)
构造器
ArrayList为我们提供了三个不同的构造器,以及四个成员变量。
下面一步一步对其进行分析,跟着数字顺序进行学习:
// 对于空参构造创建的数组,在容量确认的过程中会使用此参数进行初始化构建
// 所以空参构造创建的数组一旦add,就是直接扩容为容量10
private static final int DEFAULT_CAPACITY = 10;
// 一个空的数组,当调用new ArrayList<XXX>(0)时使用
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认情况下调用的空参,创建的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// ArrayList的真面目
transient Object[] elementData;
// size代表当前数组中元素的个数,它并不等于数组长度,因为数组长度可能是10但是里面只有1个元素,那么size=1
private int size;
// 0.先看空参构造函数
// 这部分非常简单,默认情况下,new ArrayList<>();将调用此空参构造
// 创建一个空数组,长度为0。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 1. 继续看有参构造函数
// 此构造函数提供一个int类型的形参initialCapacity
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 当传入值大于0时
// 创建一个长度等于传入值大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果传入值等于0
// 直接使用初始化好的EMPTY_ELEMENTDATA,一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 如果传入值小于0,就抛出一个错误
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/*
2. 参数类型为Collection的有参构造函数
此构造函数提供一个Collection<? extends E>类型的形参
*/
public ArrayList(Collection<? extends E> c) {
// Collection接口提供一个方法 toArray返回数组类型
// 所有实现类均重写了这个方法,将自身转化为一个数组进行返回
Object[] a = c.toArray();
// 将转换后的数组的长度赋值给size,并判断不等于0
if ((size = a.length) != 0) {
// 如果传入的参数就是ArrayList,则直接将引用指向传入的参数的地址
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 否则调用Arrays.copyOf,
// 这个方法本质就是创建一个新的长度为size的同类型的数组
// 并将传入数组中的所有值进行1:1的复制,并返回新创建的数组
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 如果传入的参数,就是一个空的集合,那就直接成员变量的空数组
elementData = EMPTY_ELEMENTDATA;
}
}
OK,构造方法就只有3个,其实看下来并没有很困难,做一个简单的小结:
- ArrayList类会初始化4个成员变量,其中size=0
- 空参构造,就是创建一个空数组
- int类型的参数的构造,如果传入值大于0就创建一个大小为传入值的数组,但是不修改size,size还是0!!!,否则就是空数组,如果传入值小于0就报错
- Collection类型参数的构造,就是将传入的集合类型转化为一个数组,将此数组中的所有值复制进一个新数组,并返回,这个过程中会直接修改size的值
这里我们第一次看到了 Collection.toArray() 和 Arrays.copyOf(elementData, size, Object[].class) ,Arrays.copyOf(elementData, size, Object[].class)的底层使用的是C++代码编写的native方法 System.arraycopy ,敲黑板了,这个几个方法都是在学习Collection相关代码中非常非常常见的方法,我们需要暗暗的记下,继续上路,继续学习。
看完了构造函数,我们已经对于ArrayList有一个初步的了解了,下面我们从最常用的API入手,深入研究 :)
常用API分析
添加可以说是ArrayList最常用的api之一了,ArrayList为我们提供了两个add方法和两个addAll方法,和上面一样,我们来一步一步学习源码
add(E e)
// 0. 单参数的add方法,用于向arraylist数组中插入一个对象
public boolean add(E e) {
// 剩余空间检查
// arraylist本质是数组,所以要做一个检查,以防超出容量
// 当容量满时会进行自动扩容的操作
// 下面对于自动扩容进行了详细的解释
ensureCapacityInternal(size + 1);
// 进行自动扩容之后,就是在当前数组末尾元素后一位将对象放进去
// 同时size + 1 表示当前数组元素数据+1
elementData[size++] = e;
return true;
}
自动扩容
// 剩余空间检查
private void ensureCapacityInternal(int minCapacity) {
// 这里要拆解为两步,1是calculateCapacity 2是ensureExplicitCapacity
// 先看calculateCapacity,传入当前数组和当前数组的size+1
// 再看ensureExplicitCapacity
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 这一步是判断list对象是由空参构造创建还是由其他有参函数构造
// 回忆一下空参构造,它创建的是一个空的数组,长度是0
// 但是ArrayList很贴心的为我们设定好了一个默认的初始容量 DEFAULT_CAPACITY = 10
// 所以一旦空参的list首次使用add方法,就会触发此处,通过扩容对空数组进行初始化
// 那么这个方法的主要存在意义其实也就是为了初始化空参构造函数创建出的空数组
// 对于其他两个方法创建的数组意义不大
// (需要注意的是使用new ArrayList(0)构建一个长度为0的数组,不享受DEFAULT_CAPACITY这个的创建)
// (所以new ArrayList(0)就比较蠢)
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 这里就是判断了如果是空参构造函数构造的数组
// 就对与默认的DEFAULT_CAPACITY = 10进行比较
// 返回较大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 这里就是判断当前数组是否真的需要扩容
private void ensureExplicitCapacity(int minCapacity) {
// modCount类似于一个修改计数器,记录对于数组的修改次数
modCount++;
// minCapacity要么是初始的10要么就是当前数组元素个数+1
// minCapacity和数组长度比较就是判断当前数组长度是否满足我的需求
if (minCapacity - elementData.length > 0)
// 不满足就grow
grow(minCapacity);
}
// 扩容!
private void grow(int minCapacity) {
// 先去的当前的数组的长度
int oldCapacity = elementData.length;
// oldCapacity >> 1 位运算右移一位 = 当前长度除2
// 所以扩大1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 扩大1.5倍的容量与我计算出的最低需求比较
if (newCapacity - minCapacity < 0)
// 扩大后的还是不满足我的需求那么就直接使用最低需求进行创建
newCapacity = minCapacity;
// 执行到这里的时候已经基本确定了grow后数组的大小
// 再做一个判断,当grow后的数组大小比MAX_ARRAY_SIZE还大
// 就要再去判断一个超大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 最后进行扩容
// 其实就是创建一个新的长度等于我们一步一步判断出来的待扩容的长度的数组
// 再把原来的数组的值一个一个复制进去
elementData = Arrays.copyOf(elementData, newCapacity);
}
add(int index, E e)
和普通add不同,add(int index, E e)用于向指定脚标插入元素
// 1. 用于向对应的脚标进行数据插入操作
public void add(int index, E element) {
// 判断一下需要插入的位置有没有超出数组范围,如果超出就抛错
rangeCheckForAdd(index);
// 和上面的add一样,进行剩余空间检查,容量不足就扩容
ensureCapacityInternal(size + 1);
// System.arraycopy的意思其实理解起来很简单
// 就是源数组的某位复制一定长度的数据到目标数组的某位
// 下面这段的含义就是将当前数组的第index位开始到结尾,整体后移一位,把第index位空出来
System.arraycopy(elementData, index, elementData, index + 1,size - index);
// 再在空出来的index位放上add进来的值
elementData[index] = element;
// 最后size + 1
size++;
}
// 数组越界的检查
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
addAll(Collection<? extends E> c)
其实通过上面两个add方法的学习,我们看到这个addAll方法的时候猜也能猜出来啥意思,就是将Collection的元素转换为数组,在全部加到ArrayList的数组中去
废话不多说,直接上源码解析
public boolean addAll(Collection<? extends E> c) {
// 先转为数组
Object[] a = c.toArray();
// 获取数组的长度,主要用于容量检查,进行扩容,复制的时候也可以知道需要复制的长度是多少
int numNew = a.length;
// 老样子,容量检测,容量不足就扩容
ensureCapacityInternal(size + numNew);
// 还是老样子,复制复制复制。。。
System.arraycopy(a, 0, elementData, size, numNew);
// 还是老样子,size+n
size += numNew;
// 当数组长度大于0的时候才会返回true
return numNew != 0;
}
addAll(int index, Collection<? extends E> c)
还有最后一个add相关的方法,通过上三个方法的学习,这个方法已经无需过多介绍了,看到名字看到参数马上就秒懂了
上代码
public boolean addAll(int index, Collection<? extends E> c) {
// 和add(int index, E element)都进行了数组越界的检查
// 因为index是外面传进来的无法保证它的安全性
rangeCheckForAdd(index);
// 下面这部分和addAll(Collection<? extends E> c)没有差别
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
// 这里做了一个小小的判断
int numMoved = size - index;
if (numMoved > 0)
// 当插入的位置不是末尾的时候,而是在原数组中间的某个位置的时候
// 将index位的所有元素后移n位,n等于插入数组的长度
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 新数组复制进原数组的index位
System.arraycopy(a, 0, elementData, index, numNew);
// 下面的步骤和之前一样,不多说了
size += numNew;
return numNew != 0;
}
add小结
add操作就是上述的4个方法,整体难度不大,核心就是两个:1、自动扩容 2、System.arraycopy,其实所谓的扩容也就是新建一个长一点的数组把老的放进去而已,这就和很多技术一样,看起来名字很牛逼,实际就那样 (-。-)
ArrayList提供了一个用于元素修改的方法set,功能十分简单
set(int index, E element)
set方法非常简单,就是常规的获取目标位置的元素进行置换,并返回原始值
public E set(int index, E element) {
// 有index的地方就有rangeCheck,数组越界检查
rangeCheck(index);
// 获取目标位置的元素
E oldValue = elementData(index);
// 就一个简单的复制
elementData[index] = element;
// 注意这里返回的原始值
return oldValue;
}
// 获取目标位置的元素
E elementData(int index) {
return (E) elementData[index];
}
对于删除,ArrayList提供了若干不同的方法,用于应对不同场景的使用,坚持一下,继续学习!
remove(int index)
删除目标索引位置的元素
public E remove(int index) {
// 有index的地方就有rangeCheck,数组越界检查
rangeCheck(index);
// 修改计数器+1
modCount++;
// 获取目标位置的元素
E oldValue = elementData(index);
// 所谓的删除其实就是将目标位置后面所有的元素整体前移一位,然后将最后一位设为空
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 直接数组最后一位设置为null,等待gc回收空间
elementData[--size] = null;
return oldValue;
}
remove(Object o)
删除数组中的某个对象,注意仅删除第一个出现的对象
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
// 找到第一个为null的位置,进行移除
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 找到第一个和o equals 的元素,注意这里是equals
// 所以在引用数据类型的数组使用remove时候需要注意
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// fastRemove中的删除过程和remove(int index)中的代码基本一致,不过多介绍
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;
}
removeAll(Collection<?> c)
这个方法用于删除ArrayList中与集合c共有的元素
public boolean removeAll(Collection<?> c) {
// 这里做了一步空指针检查
Objects.requireNonNull(c);
// 批量删除
return batchRemove(c, false);
}
// 这个方法是Objects类的静态方法
public static <T> T requireNonNull(T obj) {
if (obj == null)
// 如果obj为空就抛一个空指针异常
throw new NullPointerException();
return obj;
}
// 这一步做了具体的批量删除操作
private boolean batchRemove(Collection<?> c, boolean complement) {
// 获取arraylist的数组
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
// 循环遍历整个原数组
// 这里complement传入值是false
// 意思就是当传入的集合中不包含原数组中的元素
// 就保留这个元素,并按照顺序赋值进原数组,同时用w来记录保留下来的元素的个数
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
// 举个例子: 原数组是1 2 3 4 5,c是3 4,那么这一轮操作之后elementData数组就变成了 1 2 5 4 5,前三个被保留下来了,w=3
} finally {
// 这一步有点特殊,一般情况下在经历了上面的一轮循环之后,r = size
// 但是这里要注意的一下上述的循环如果抛出了异常,那么此时将出现r!=size的情况
// 不对此种情况进行处理,必然会出现错误的移除元素
if (r != size) {
// 这一步的目的就是将出现异常后的所有数据全部复制回来
// 如果数组elementData没有遍历完就跳出了循环,把剩下没遍历完的当作不需要删除的数组元素,放到保留的数组元素的数组后面。
// 举例说明:原数组是1 2 3 4 5,c是3 4,在遍历的过程中,遍历到第4个数的时候出现了异常,那么此时r=3,w=2。数组中4 5没有被遍历
// 此时1 2已经被保留,那么出现异常就要保留没被遍历的值,所以下面这一步结束 数组 = 1 2 4 5 5,w = 4
System.arraycopy(elementData, r, elementData, w, size - r);
w += size - r;
}
// 一般的c不是空数组或者出现异常的情况,那么w不会等于size
if (w != size) {
// 删除共有元素,回到上面的例子
// 原数组是1 2 3 4 5,c是3 4,那么这一轮操作之后elementData数组就变成了 1 2 5 4 5,前三个被保留下来了,w=3
// 此时删掉的就是4 5,最后元素就变成了1 2 5 null null
for (int i = w; i < size; i++)
elementData[i] = null;
// 记录下有几个元素被修改了
modCount += size - w;
// 同时将size只为w,w就是当前剩余元素的个数
size = w;
// 返回修改成功
modified = true;
}
}
return modified;
}
remove小结
可以看到无论是add还是remove本质上都是对于数组的复制,理解了这一点整个ArrayList的源码理解起来就很简单
get(int index)
获取方法太简单了,就是越界检查加数组脚标获取,此处就不做过多的介绍了,都是上面已经介绍过得方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
clear()
这个方法用于清空ArrayList
public void clear() {
// 修改计数器+1
modCount++;
// 遍历数组所有元素,全部置空
for (int i = 0; i < size; i++)
elementData[i] = null;
// 元素数 = 0
size = 0;
}
contains(Object o)
判断ArrayList是否包含目标元素,注意判断用的是equals
public boolean contains(Object o) {
// 获取第一个元素的脚标,如果没获取到的话会返回-1,以此来判断是否包含元素
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
// 遍历第一个null元素
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
// 遍历第一个equals相同的元素
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
// 没有就返回-1
return -1;
}
isEmpty()
用于判断ArrayList是否为空
public boolean isEmpty() {
// 这里可不是判断数组内容哦,直接用size判断即可
return size == 0;
}
trimToSize()
这个方法很不错,上面的各个方法看完一遍相信大家对于ArrayList的尿性也有所了解,无非就是数组的复制各种变式
这就会导致数组长度和实际的size不等,trimToSize就是强行压缩长度=size
public void trimToSize() {
modCount++;
if (size < elementData.length) {
// 这一步就是强行复制出一个新的长度等于size的数组出来
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
“鬼影重重” modCount
经过上面的一轮学习,我们已经对于ArrayList的底层实现有了一定的认识,但是如果仔细学习过了的话你一定和我一样有一个疑问,这modCount他娘的是个什么玩意?很多操作中都有它的身影,但是各个操作中却也只是对他进行了修改的操作,目前看来没有任何实质性的作用。带着这个疑问,我们继续向下学习 :)
迭代器Iterator
想要知道modCount的作用,那就不得不说迭代器
private class Itr implements Iterator<E> {
// 游标,初始化时从0开始
int cursor;
int lastRet = -1;
// 类初始化时,将modCount赋值给expectedModCount,做好铺垫,在checkForComodification时有用处!
int expectedModCount = modCount;
Itr() {}
// hasNext用来判断当前是否还有
public boolean hasNext() {
return cursor != size;
}
public E next() {
// 共修改检测!!!
checkForComodification();
// next主体部分的代码逻辑十分的简单,所以不过多介绍,我们重点去学习快失败机制
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() {
if (lastRet < 0)
throw new IllegalStateException();
// 共修改检测!!!
checkForComodification();
// remove主体部分的代码逻辑十分的简单,所以不过多介绍,我们重点去学习快失败机制
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
public void forEachRemaining(Consumer<? super E> 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++]);
}
cursor = i;
lastRet = i - 1;
checkForComodification();
}
// 看仔细咯,checkForComodification 检查是否存在共修改
final void checkForComodification() {
// 当modCount!=expectedModCount ??? why 明明我的expectedModCount就是由modCount赋值而来,为什么还不等于呢
// 答案就是并发环境!细心的朋友早就发现了这个ArrayList的缺陷,那就是不支持并发
// 那么这个modCount从哪来的呢,它是ArrayList的父类AbstractList的一个成员变量,ArrayList继承而来
// 举一个场景的例子,当一个线程使用迭代器遍历ArrayList进行一些操作,由于ArrayList不是线程安全的,那么此时其他线程也可以操作这个ArrayList对象
// 此时如果其他线程对ArrayList内的对象进行了修改,如果不做处理必然会引起我们正在遍历ArrayList的那个线程的错误
// 所以引入了这个modCount机制,当其他线程修改时将modCount进行修改,迭代器的expectedModCount检查到modCount被修改过了,就抛出异常,立刻停止
// 这也就是所谓的Fail-Fast机制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
Fail-Fast机制
在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
总结
以上就是对于ArrayList底层的一次解析,当然ArrayList的源码远不止这些,我只是挑出了我觉得比较重要的一些部分进行了学习梳理。
看完之后问自己一个问题,难吗?难却也不难,难在思想难在设计,不难是很多地方的代码其实也很普通
所以要建立一个思维,那就是:源码其实并没有那么遥不可及,并没有难得看不懂,我们需要的是明白脉络,学习思想,特别喜欢马士兵老师曾在视频中说的一句话 "不识庐山真面目,只缘身在此山中。想要明白整个庐山的真面目,我们要看的是他的脉络,而不是蹲在庐山中的一棵大树下研究那一根根的小草"
对于源码的学习也是如此,脉络才是关键,不要上来就去纠结某个方法是如何实现的,要是这样,路走不远,至少我个人是这么认为的:)共勉,加油!