☀️今天花了很久写了这篇关于 ArrayList 扩容机制源码解析的博客,在阅读源码的过程中发现了很多之前有误解的地方,也加深了对代码的理解,所以写下了这篇博客。
🎶本文附带了流程中所有的代码和附加解析,我有信心一定能帮大家完整的梳理和认识整个流程,如果博客对你有帮助的话别忘了留下你的点赞和关注💖💖💖
文章目录
- ArrayList 底层结构和源码分析
- 01.整体把握
- 02.无参构造方法
- 03.有参构造方法
- 04.底层扩容机制
ArrayList 底层结构和源码分析
01.整体把握
这里首先列出 ArrayList 扩容的几个特点,看完这些特点再去阅读体验会比较好
1)ArrayList 中维护了一个 Object 类型的数组,elementData。
2)当每次创建 ArrayList 对象的时候,如果使用的是无参构造器,则初始的 elementData 的容量为 0,第一次添加的时候则扩容 elementData 为 10,如果需要再次扩容,则扩容为原来的 1.5 倍
3)如果使用的是指定大小的构造器,则初始的 elementData 的容量就是指定的大小,如果需要扩容,也是直接扩容为 elementData 的 1.5 倍。
02.无参构造方法
下面来看具体的源码,首先就是 ArrayList 的无参构造方法:
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
解析:可以看到,它将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给了 elementData;ArrayList 的内部实现就是基于这个 elementData 数组,它是 ArrayList 存放元素的位置,之后的拿取、扩容之类的操作本质上都是在操纵它。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个常量,来看一下它的定义:
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
这是一个共享的(static)空数组实例,被用作使用 默认无参构造方法 时,elementData 的默认值;它的作用是与另一个共享的空数组实例来做区分,那另一个共享数组为 EMPTY_ELEMENTDATA,它在接下来要讲的 有参构造方法 中具有很重要的作用;简单来说 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是标识着通过无参构造形成的空 ArrayList,而 EMPTY_ELEMENTDATA 是通过有参构造形成的空 ArrayList,它们在后续的扩容策略中会有所不同。
除了这个标志作用以外,它们的作用都是相同的,都是在内存中开辟了一个 共享 空间用来存放空数组,可以避免过早的为新创建的 ArrayList 实例分配内存,达到节省内存的作用。
03.有参构造方法
1)先观察 ArrayList 的有参构造方法,ArrayList 其实提供了两种有参构造方法,通过 CTRL + P 快捷键,可以查看其中的参数:
可以看到有参构造的第一种方式就是提供一个 int 类型的 initialCapacity,也就是初始的容量;第二种方法是提供一个集合类,构造方法会将这个集合类转为 ArrayList 的类型。
先来看指定初始容量的构造方法,这里插嘴一句,如果大家用的 idea 版本是新版的,可以多去使用那个 SmartStepInto,是调试器提供的一个智能步入的方式,可以智能的跳过一些不必要的步骤;说回到这个有参构造方法
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);
}
}
如果指定的初始容量大于零,就直接构造一个容量为初始容量的 Object 数组,然后将其赋值给 elementData;否则,当初始的容量等于 0 的时候,就指定其为 EMPTY_ELEMENTDATA!这里就能看出与无参构造的区别了;如果是其他的数字,比如负数,就抛出一个异常。
2)再来看给定 Collection 的构造方法:
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
解析:首先通过 Collction 接口中定义的 toArray() 方法,将集合类转为一个数组,然后指定 size(当前 ArrayList 实例中存放元素的个数)赋值成数组的长度,然后判断这个长度是否为 0,如果不为 0 就将其赋值给 elementData。
但是关于第二个 else 我其实是有些疑惑的,我猜测写这段 else 逻辑
elementData = Arrays.copyOf(a, size, Object[].class);
是为了保证 ArrayList 中始终维护的是一个 Object 数组,但是其实第一个语句Object[] a = c.toArray();
已经确定了这是 Object 数组,所以单从这里看其实是有些冗余的,如果大家有什么见解可以在评论区指教一下。
如果长度为 0 的话,就将 elementData 赋值为 EMPTY_ELEMENTDATA,所以这个 static 属性其实就是标识有参构造方法形成的空 elementData 数组。
04.底层扩容机制
终于到了重中之重的扩容机制,也是面试题中经常会问到的部分,直接来追一下源代码:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
add 的代码其实比较简单,首先就是调用了 ensureCapacityInternal() 来保证存储空间(elementData)足够放下这个新的元素,然后再将元素放入其中:elementData[size++] = e;
,那接下来要看什么呢?不用多说,肯定是这个 ensureCapacityInternal() 方法。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
在这个方法中,首先是使用了 calculateCapacity(elementData, minCapacity) 方法去计算容量,这个 minCapacity 是上面传过来的,不管是上面的有参构造还是无参构造,最终形成的 ArrayList 实例的长度都是 0,所以此时的 minCapacity 就是 1。
下面的是 calculateCapacity() 方法
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
首先看第一个 if 语句 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
,先去判断了这个 elementData 是否为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那什么时候会是这个元素呢?答案就是 无参默认构造方法 创建的实例,这时候就发挥它的作用了,当发现 ArrayList 实例是通过无参构造形成的,就会去取 minCapacity 和 DEFAULT_CAPACITY 中的最小值,而 DEFAULT_CAPACITY 它的值就是 10,这也就是为什么很多面试题的答案说,首先创造空数组,然后第一次扩容的时候扩容成 10;但这并不是完全正确的,当不是无参构造的时候,其实此时的 minCapacity 仍然是 1。
OK,得到了 minCapacity,我们回到上一个方法:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
然后就是取执行 ensureExplicitCapacity() 方法了:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
首先是对这个 modCount 做了一个自增,这个变量记录了这个集合扩容的次数,然后去判断 minCapacity - elementData.length > 0
也就是最小需要的长度能否通过当前的 elementData 长度满足,如果不能就进入扩容方法 grow(),并且传入 minCapacity。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
首先记录下 oldCapacity,也就是扩容之前 elementData 的长度,然后执行这条语句 newCapacity = oldCapacity + (oldCapacity >> 1);
,使用了右移运算符,右移运算符其实就可以看作除以 2 的操作,然后再加上原本的 oldCapacity,最终就是原本长度的 1.5 倍,但是因为是最后将其转换为了 int,所以其扩容效果是 小于等于 1.5 倍的;然后后面就是将此时的 minCapacity 和这个由原本长度拓展 1.5 倍的长度做一个对比,取最大,后一个 if 语句是为了处理扩容过限的问题,代码比较容易,最后贴给大家看一下。
最终就是调用 Arrays.copyOf(elementData, newCapacity);
将原本 elementData 中的内容移动到新拓展的,长度为 newCapacity 的数组中,这就完成了一个完整的扩容。
- 此时如果是无参构造,它带进来的 minCapacity 就是 10,最终其会被拓展为 10
- 如果是有参构造的话,带进来的 minCapacity 其实就是 1,且计算得 int newCapacity = oldCapacity + (oldCapacity >> 1); 结果是 0,那最终 elementData 会被拓展成 1。
所以说第一次拓展均拓展成 10 其实是不准确的;其他长度的拓展大家顺着流程推导一下就很容易得到了。
最后贴上 hugeCapacity() 方法的源码:
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
这个方法是为了处理 newCapacity 的长度超过定义的数组的最大长度(MAX_ARRAY_SIZE,它被定义为 Integer.MAX_VALUE - 8),此时就使用 minCapcaity 进行初始化,如果发现 minCapacity < 0,就大概率是因为越界导致的了,因为当 int 超过 231 - 1 的时候,就会因为错位变成负数,所以此时抛出 OutOfMemoryError 超过内存限制错误,然后判断此时的 minCapacity 是否大于 MAX_ARRAY_SIZE,如果不大于就赋值成它,否则赋值成 Integer.MAX_VALUE,也就是 int 的最大值,如果还是不够会在后面因为越界抛出异常的。