前言
同学们好,欢迎来到源码终结者系列,很多同学在编程的道路上会感觉读源码有很大的难度, 认为我们都是普普通通的开发人员, 没必要去研究那么"高大上"的底层源码, 实际则不然, 读源码有很多的好处, 并且可以解决我们在工作or学习中遇见的疑难杂症, 一个高级程序员是需要去深入研究一些技术的,那么读源码的能力则极其重要。那么就从今天起, 跟随我一起来撕开各种技术源码的面纱吧!
ArrayList介绍
ArrayList 是一个由数组构成的一个集合,并且具备可扩展性(可扩容),并且ArrayList也支持null元素, 并且支持泛型
ArrayList实现的接口
List接口
ArrayList是一个实现了List,RandomAccess,Cloneable,Serializable的类
List接口可以实现对元素的增删改查等功能
RandomAccess接口
RandomAccess则是对元素进行随机访问的支持(JDK作者提供的一个无方法的标志性接口,实现该接口则代表可以支持随机访问)
Cloneable接口
Cloneable也是一个标志性接口, 也是实现该接口则代表可以克隆一个不同地址的对象
Serializable接口
Serializable也是一个标志性接口,代表该类可以被序列化
核心参数
那么当我们了解完ArrayList实现了哪些接口后,我们就对ArrayList中的核心参数做一个说明吧!
// 代表数组的默认长度
private static final int DEFAULT_CAPACITY = 10;
// 如果使用有参构造函数创建集合时使用该常量创建
private static final Object[] EMPTY_ELEMENTDATA = {};
// 如果使用无参构造函数创建集合时使用该常量创建
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 该数组则是ArrayList真正用来存数据的地方, 实际上就是一个数组!
// transient 代表该对象不会被序列化
transient Object[] elementData;
// 数组的实际长度
private int size;
// 数组的最大长度, 我的内存顶不住了啊!
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
那么看到这里,恭喜你,对ArrayList中的各个参数应该都明白了,那么我们即将开始源码的旅途!
核心方法源码解读
从这里开始我们将要开始我们的方法源码解读啦,将会对ArrayList中的核心方法做一个细致的解读,感兴趣的同学们不要错过哦!
get方法
俗话说得好,从简至繁,那么我们首先先从较为简单的get方法来看一看
// 此方法代表根据传递的index索引值去获取该数组中对应位置的元素
public E get(int index) {
// 该方法则是判断索引是否越界, 如果越界则抛出索引越界异常
rangeCheck(index);
// 这里会发现elementData(index)实际上是一个方法, 其实等同于elementData[index]
return elementData(index);
}
// 该方法则是判断索引是否越界, 如果越界则抛出索引越界异常
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 根据索引获取数组对应索引位置的元素
E elementData(int index) {
return (E) elementData[index];
}
那这个时候相比同学们已经掌握了get方法的细节, 那么我们来看一下一个ArrayList对象被创建的时候会做什么呢?
ArrayList构造
首先来看最简单的无参构造
无参构造
public ArrayList() {
// 将数组赋值为默认的{}, 代表是一个空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
传int类型参数的构造
// initialCapacity为用户指定的容量大小
public ArrayList(int initialCapacity) {
// 当指定的容量大小>0的时候
if (initialCapacity > 0) {
// 新创建一个对应容量的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 当指定容量=0的时候, 则使用上面讲到的有参构造创建数组的常量对应的{}
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 当指定容量<0的时候, 则抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
传集合的构造
// c是一个集合
public ArrayList(Collection<? extends E> c) {
// 首先将传过来的集合转成数组
elementData = c.toArray();
// 将数组的长度赋值给size变量后判断是否不为0
if ((size = elementData.length) != 0) {
// 如果不为0的话判断数组的类型是否是Object类型, 如果不是的话需要对数组进行复制改为Object类型
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果数组长度为0则赋值为默认的{}
this.elementData = EMPTY_ELEMENTDATA;
}
}
那么到这里ArrayList的构造则就解读完了, 是不是没有什么难度呢?创建好集合后我们要往里面插入新的值呀,那么我们来看一下add的方法
add方法
// e代表要插入到数组中的参数
public boolean add(E e) {
// 该方法是为了判断是否需要扩容
ensureCapacityInternal(size + 1);
// 将数组当前长度对应的索引位置赋值为新插入的元素e
elementData[size++] = e;
// 返回true代表插入成功
return true;
}
// calculateCapacity返回新增数据后的最大长度, ensureExplicitCapacity则判断是否需要扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//返回新增数据后的最大长度, 传参elementData就是数组变量, minCapacity是size变量+1
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断数组是否是无参构造创建的并且是第一次初始化的
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 返回10或者minCapacity(谁大返回谁)
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 该方法判断是否需要进行扩容
private void ensureExplicitCapacity(int minCapacity) {
// 操作数加一
modCount++;
// 如果数组新增后的长度减去现在数组的长度大于0, 那么则进行扩容
if (minCapacity - elementData.length > 0)
// 扩容方法
grow(minCapacity);
}
// 扩容方法
private void grow(int minCapacity) {
// 先用oldCapacity 接收原来数组的长度
int oldCapacity = elementData.length;
// newCapacity 代表扩容后的长度, 等于原来长度加上原来的长度除以2, 相当于1.5倍(不懂左右移的同学可以去学习一下, 还是比较简单的
// >>代表二进制的数往右移动一位,那么就相当于除2。 <<则是往左移动一位, 相当于乘以二)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 判断新的最大长度减去新增后的数组长度是否小于0
if (newCapacity - minCapacity < 0)
// 将新增后的数组长度赋值给扩容后的最大长度
newCapacity = minCapacity;
// 如果扩容后的最大长度超过了默认的最大长度后
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 返回一个边界长度
newCapacity = hugeCapacity(minCapacity);
// 将原来的数组进行拷贝, 变成新数组后赋值给原来的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 返回一个边界长度
private static int hugeCapacity(int minCapacity) {
// 判断新增之后的长度是否小于0, 小于0则抛出异常
if (minCapacity < 0)
throw new OutOfMemoryError();
// 如果新增后的长度大于了默认的最大值, 则返回int类型最大值, 否则是默认最大值
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
那么add还支持通过索引位置去添加元素,我们一起来看一下
// index代表要插入的元素位置, element代表要插入的元素
public void add(int index, E element) {
// 检查索引是否越界
rangeCheckForAdd(index);
// 该方法是为了判断是否需要扩容(同上面一致)
ensureCapacityInternal(size + 1);
// 拷贝数组, 实际上就是将该index位置的元素和之后的元素都往后移动一位, 留出空间给新插入的元素。
// 参数概念:第一个是原数组,第二个是原数组中的开始位置,第三个是目标数组,第四个是目标数据中的开始位置,第五个是要复制数组元素的数量
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 插入新元素
elementData[index] = element;
// 数组长度++
size++;
}
// 检查索引是否越界
private void rangeCheckForAdd(int index) {
// 当index大于数组长度或者小于0时, 抛出异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
set方法
那add方法是往数组中新增一个元素, 我们能不能修改数组中的元素呢, 显然也是可以的, 提供了一个Set方法用来修改元素, 那么我们来一探究竟!
// index代表要修改的元素索引位置, element是修改后的元素
public E set(int index, E element) {
// 检查数组是否越界(get方法中已经讲过, 忘记的同学回顾一下)
rangeCheck(index);
// 获取旧元素数据并在下面返回
E oldValue = elementData(index);
// 修改对应的索引位置为新元素
elementData[index] = element;
// 返回旧元素
return oldValue;
}
remove方法删除元素
那么当我们添加完元素后, 不需要的元素肯定是需要删除方法的
// 根据索引位置删除元素
public E remove(int index) {
// 检查索引是否越界(参考上面已讲过的内容...)
rangeCheck(index);
// 修改次数加一
modCount++;
// 先取出将要被删除的元素
E oldValue = elementData(index);
// 计算删除需要移动的元素数量, 比如size=10, 我们要删第五个元素, 那么就是size - 4 - 1, 假设数组是1 2 3 4 5 6 7 8 9 0那么我们删除5这个元素的时候需要把后面的6 7 8 9 0往前移动一位
int numMoved = size - index - 1;
// 当要移动的元素数量大于0时, 就需要进行数组拷贝
if (numMoved > 0)
// 参数概念:第一个是原数组,第二个是原数组中的开始位置,第三个是目标数组,第四个是目标数据中的开始位置,第五个是要复制数组元素的数量
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后一个元素设置成null
elementData[--size] = null;
// 返回被删除的元素
return oldValue;
}
clear方法
清空数组中所有元素, 灰常简单!
public void clear() {
// 修改次数加1
modCount++;
// 循环将每个元素赋值为null.....
for (int i = 0; i < size; i++)
elementData[i] = null;
// 数组长度设置为0
size = 0;
}
到这里, 一些ArrayList对数组操作的核心方法就告一段落了, 下面我们再来讲几个极其常用的方法源码
indexOf方法
indexOf方法是判断一个元素是否存在于该数组中, 如果存在则返回对应的索引位置
// o是要检查是否存在的元素
public int indexOf(Object o) {
// 如果o==null的时候, 则判断元素中是否存在null的元素, 如果存在就返回对应的索引位置
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
// 如果o!=null, 则判断元素中是否存在相等的元素, 如果存在就返回对应的索引位置
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
// 没有找到则返回-1
return -1;
}
lastIndexOf方法
lastIndexOf和indexOf方法基本一致, 只不过是从后往前找而已
// 与indexOf基本一致, 参考上面的反向遍历即可
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
contains方法
实际也就是调用了一下indexOf方法
// 当indexOf方法查到的元素>=0的时候就返回true
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
isEmpty方法
判断数组是否为空
public boolean isEmpty() {
// 判断数组长度是否为0
return size == 0;
}
结语
到这里ArrayList的核心方法基本都讲解完了, 如果哪里有错误欢迎指出, 共同学习与进步。