java集合框架源码解读之集合框架解读之ArrayList
源码解读
源码总体情况
- ArrayList的底层维护了一个Object[] elementData动态(可进行扩容)数组
- 实例一个ArrayList对象时,底层会根据ArrayList构造函数是否有参,而对elementData数组进行初始化不同的长度。无参时,数组elementData默认长度为10;有参时数组elementData长度为传入的参数。无论实例ArrayList有参还是无参,今后每当elementData数组要进行扩容时,都会按照当前数组的长扩大1.5倍。如默认情况下(长度为10)每次扩容扩容情况10–>15–>22–>33…;当构造器传入参数如2时,每次扩容情况为2–>3–>4–>6…。接下来就按照有参和无参两种情况进行讲解源码(其实两种情况大同小异)。
- 无参情况下
1.首先让我们先看看实例化一个无参ArrayList发生了什么
- 当我们实例化一个无参ArrayList对象时
public class ArrayList_ {
public static void main(String[] args) {
//当构造器没有传入参数时,追进源码
ArrayList<Integer> list = new ArrayList<>();
}
}
- 现在我们追进源码的第一层
/*------------------源码进入部分-----------------------*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/*------------------相关字段解读--------------------------*/
transient Object[] elementData;//字段一
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//字段二
```
源码解读:
1.当我们进入ArrayList无参构造器时发现,ArrayList底层维护的elementData数组(也就是字段一)
指向了一个默认初始化长度为0的数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA(也就是字段二)
```
2.现在我们往实例化的ArrayList添加一个元素看看又发生了什么
-
追加一个元素
public class ArrayList_ { public static void main(String[] args) { //当构造器没有传入参数时 ArrayList<Integer> list = new ArrayList<>(); list.add(1); } }
- 源码第一层
public boolean add(E e) {
ensureCapacityInternal(size + 1);
//size是当前数组中有效数据的长度,size+1这里是用来假设当size长度加一时会不会发生扩容
elementData[size++] = e;//在不发生数组越界的情况下将数据e添加到elementData数组中
return true;
}
```
```
源码解读:
1.当我们进入add()函数时,会首先进入ensureCapacityInternal函数,该函数的主要作用是判断当加入当前数据e时,会不会发生数组越界,越界时就会将这个数组进行扩容之后再加入数据。
```
2.进入ensureCapacityInternal函数
```java
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/*------------------相关函数(方法)解读--------------------------*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);//DEFAULT_CAPACITY是elementData默认容量,长度为10
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//数组有效数据长度
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
源码解读:
1.当我们进入ensureCapacityInternal函数时,会进入一个需要传入一个整型参数的函数ensureExplicitCapacity
2.ensureExplicitCapacity需要的参数由calculateCapacity提供
3.下面就calculateCapacity和ensureExplicitCapacity进行解读
3.1 calculateCapacity函数的作用是如果elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA(换言之就是在实例 ArrayList时是否传参),当为true时,会将数组长度设为默认值DEFAULT_CAPACITY=10,并返回。反之,返minCapacity,
也就是上一层传入的size+1;
3.2 ensureExplicitCapacity是决定是否扩容的关键函数,当minCapacity - elementData.length > 0(即添加当前元素 后,有效数据长度大于数组的长度)结果为真时,就会进行扩容。
3.数组进行扩容
-
由于我们实例ArrayList时没有将没有传入参数,底层将elementData指向了一个空数组(也就是上文提到的DEFAULTCAPACITY_EMPTY_ELEMENTDATA),由于是第一次添加元素,而elementData此时又是一个空数组,所以会第一次会触发扩容机制,下面进入grow扩容函数。
private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); //这里是指扩容后的数组的长度,大小为原数组长度的1.5倍,这里oldCapacity >> 1等价于oldCapacity / 2 if (newCapacity - minCapacity=0的情况, < 0) //这里是防止newCapacity=minCapacity=0的情况,因为当实例化ArrayList构造器没有传参,从而导致elementData指向空数组,elementData数组长度为0 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) //这里是指扩容后的数组长度达到最大长度MAX_ARRAY_SIZE=2147483639时(通常用不到),会进行更大的扩容机制hugeCapacity, //这里不再赘述 newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity);//最后将原数组的内容拷贝到一个长度为newCapacity的数组中去,并将elementData指向这个数组。至此ArrayList首次扩容结束,结果逐层返回。 }
2.有参情况下
无参和有参差别不大,主要区别在于两者实例时和进行扩容时有些许不同,这里讲述一下两者有区别的地方,其它的两者大致相同,这里不再赘述
1.首先让我们先看看实例化一个有参ArrayList发生了什么
public class ArrayList_ {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>(2);
//这里会将ArrayList维护的数组elementData初始化为一个数组长度为2的数组,以后扩容的起始点便从2开始,往后增加数组长度的1.5倍
}
}
/*------------------进入ArrayList有参构造函数中--------------------------*/
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);
}
}
源码解读:
可以看到当进入ArrayList有参构造函数时,会根据传入的参数值initialCapacity分为三种情况
当initialCapacity > 0时,elementData会被初始化一个长度为initialCapacity的数组;
当initialCapacity = 0时,elementData会指向一个空object数组,也就是相当于上文无参时指向的那个空数组
其他情况,则会抛出IllegalArgumentException异常。
2.有参ArrayList扩容
public class ArrayList_ {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>(2);
list.add(1);
list.add(2);
list.add(3);//在此处将会发生扩容,从此处追进源码
}
}
/*-----------------进入扩容函数grow-----------------------*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
源码解读:
从此处看出有参和无参的使用的同一个扩容函数,唯一不同的时扩容的量的问题
无参情况下,数组以默认值10为起始值进行扩容,以后每次扩容时,会按照当前数组长度的1.5倍进行扩容;
有参情况下,数组会以传入的参数initialCapacity为起始值进行扩容,以后每次扩容时,也会按照当前数组长度的1.5倍进行扩容.
可以看出两者的主要差别在于起始值的不同。
例如现在的elementData.length为2。
通过ArrayList底层源码分析该实现类优缺点及其注意事项
优点:ArrayList底层维护的是一个动态数组,可以很方便的通过数组下标进行查找操作,便于对中小型且经常查找的数据进行存储。
缺点:由于数组的局限性,所以在对ArrayList进行删除时,会进行大量的移动操作,大大消耗了时间,因此在对数据进行经常性的删除操作时,不推荐使用ArrayList。
使用细节:由上述ArrayList构造方法及其底层原理可知,我们可以对ArrayList起始扩容量进行控制,这就需要我们结合自身数据量进行传入合适的参数。如假设我们想要每次增加1000个数据项(假设使用无参构造函数),可我们每次增加10个就要进行扩容,非常的占用时间和内存,这时我们就可以使用有参函数并传入>1000的参数,这样我们就没必要每次增加数据时都要扩容,大大节约了内存和时间。
写在最后:初学java,文章用来记录学习笔记,如有错误或不妥之处,欢迎大佬赐教,不喜轻喷。