ArrayList深度剖析
前言
系统性的细节性的去学习一个知识可能绝大多数同学的初衷都是为了应付面试。因为平时这些集合类都是封装好的API,我们只管调用就可以了。最差的情况可能就是会因为一些错误性的使用而出现IndexOutOfBoundsException或者IllegalArgumentException等异常问题。
那么我们如何去学会深度理解跟使用一个集合?在我的理解看来,主要掌握以下两点
1.了解这个集合底层的数据结构
2.了解这个集合常用的一些API,看看这些API是怎么操作底层的数据结构的
我认为掌握好以上两点,基本就可以对一个具体的集合的使用场景有了一些基本的概念。
话不多说,我们今天一起来学习一下ArrayList
一、ArrayList数据结构
要学习一个集合,我们通常都是从创建一个集合开始。那么就会出现如下的代码
List<Object> objects = new ArrayList<Object>();
也有可能会出现这样的代码
List<Object> objects = new ArrayList<Object>(2);
我们依次来看一下,这两种new 的方式都干了一些什么样的事情。先来看一下第一种。
//第一种
List<Object> objects = new ArrayList<Object>();
//new ArrayList的源码如下
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//空构造方法就是给当前对象的elementData变量指定了一个初始值。
//我们先看一下elementData是什么类型的元素
//点进去你会看到原来就是定义了一个Object数组
transient Object[] elementData;
//那我们再来看一下这个初始值是什么
//原来就是一个空的Object数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
看到这里我想你应该已经能够想到ArrayList里面用的到底是什么样的数据结构了,原来就是一个Object数组。然后我们再来看看第二种传入参数的构造方法到底干了什么。
//第二种
List<Object> objects = new ArrayList<Object>(2);
//new 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中EMPTY_ELEMENTDATA的概念
private static final Object[] EMPTY_ELEMENTDATA = {};
可以看到传入的整形进行了三次判断
1.如果大于0,那么直接创建一个传入容量大小的Object数组
2.如果等于0,那么使用的是默认的空数组EMPTY_ELEMENTDATA
3.其他则是非法传参校验
看完了ArrayList的两种创建过程,我想大家应该看明白了。ArrayList底层就是一个Object数组,我们可以在创建的时候选择是否要给他传递给定容量的数值,然后就可以创建一个相应容量的Object数组。
二、ArrayList的常用API
了解完ArrayList的底层数据结构之后,我们需要了解的就是ArrayList的一些常用的API,看他的底层是如何巧妙的去操作数组的。
add
add方法的整体流程分为以下两步
1.获取当前size+1之后的容量
2.判断当前容量是否大于当前elementData数组的长度,如果大于那么需要扩容再赋值,如果不大于那么直接赋值
public boolean add(E e) {
//这里主要是获取size+1之后的容量以及判断是否需要扩容
ensureCapacityInternal(size + 1);
//新加的元素放到当前Object数组最后一位的下一位
elementData[size++] = e;
return true;
}
//我们再来仔细看看ensureCapacityInternal做了什么
private void ensureCapacityInternal(int minCapacity) {
//calculateCapacity方法是为了获取合适的size+1之后的容量
//ensureExplicitCapacity判断新的容量是否需要扩容
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 这里需要注意的是ensureExplicitCapacity的扩容细节
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果当前size+1的容量已经大于当前数组的长度那么需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容方法
private void grow(int minCapacity) {
// 获取旧数组的长度
int oldCapacity = elementData.length;
// 用当前旧数组 + 旧数组容量的二分之一来当做扩容之后的新的数组长度
// 其实也就是原数组长度以1.5倍扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新数组容量小于minCapacity则取minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新数组容量已经取到比Integer最大值那么就取Interger最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 把原数组复制到一个新的数组,容量为原容量的1.5倍,然后再复制给elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
remove
remove整体逻辑
public E remove(int index) {
//判断索引是否超出size范围
rangeCheck(index);
//累加操作数
modCount++;
//获取要移除的索引的元素
E oldValue = elementData(index);
//需要复制的长度
int numMoved = size - index - 1;
//如果需要复制的长度大于0,那么需要从指定的源数组(从指定位置开始)复制数组到目标数组的指定位置。
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//置空最后一个元素后把size-1
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
set
public E set(int index, E element) {
//范围检查
rangeCheck(index);
//获取索引处旧元素
E oldValue = elementData(index);
//把新元素复制到相应的索引位置
elementData[index] = element;
return oldValue;
}
get
public E get(int index) {
//范围检查
rangeCheck(index);
//获取索引下标处的元素
return elementData(index);
}