Hello,小伙伴们,大家好:
今天是我们源码篇的第一天,在阅读源码之前我们应该清楚阅读源码的意义在哪儿。我认为阅读源码的意义有以下三点:
1、阅读源码能够学习前辈们许多优秀的代码设计方法。
2、阅读源码能够帮助我们提升编程能力。
3、阅读源码能够帮助我们快速的定位开发、运行时碰到的各种问题。
所以对于想提升技术的小伙伴们,我觉得阅读源码是一段必经的旅程。
好,我们进入正题,开篇一张图,了解一下ArrayList的继承关系。
Iterable接口:是java 集合框架的顶级接口,实现此接口使集合对象可以通过迭代器遍历自身元素,其核心的方法如下:
Iterator<T> iterator() 返回一个内部元素为T类型的迭代器
也就是说,集成该接口的类,应该实现iterator()方法来遍历自身元素,具体关于iterable的细节,我们在设计模式篇再进行讲解。
Collection接口:集合层次结构中的根界接口。 集合表示一组被称为其元素的对象。 一些集合允许重复元素,而其他集合不允许。 有些被命令和其他无序。 JDK不提供此接口的任何直接实现:它提供了更具体的子接口的实现,如Set和List 。 该接口定义了基本方法,仅仅列举一些方法,如下:
boolean add(E e) 确保此集合包含指定的元素。 如果此集合由于调用而更改,则返回true 。 (如果此集合不允许重复,并且已包含指定的元素,则返回false。 )
boolean remove() 从该集合中删除指定元素的单个实例。
boolean addAll(Collection<? extends E> c) 将指定集合中的所有元素添加到此集合。
AbstractCollection抽象类:为Collection中定义的方法提供了一些具体实现。
List接口:有序集合(也称为序列 ),又叫做列表。使用该接口的用户可以精确控制列表中每个元素的插入位置。 用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。 与集合不同,列表通常允许重复的元素。且如果它们允许空元素,它们通常允许多个空元素。 有人可能希望实现一个禁止重复的列表,当用户尝试插入时会抛出运行时异常,但是我们预计这种使用是罕见的。 List中定义的方法比Collecition多,但是核心方法相同。
聊到这儿,读者应该注意几点,1、该列表有序;2、该列表允许空元素;3、该列表允许重复元素。当我们阅读集合的另外一条分支-Set我们会发现Set的实现与List有很大的区别。
RandomAccess接口:是一个标志,表明该类支持快速随机访问。这个接口的目的是允许通用算法改变它们的行为,以提供良好的性能,当应用到随机或顺序访问列表。也就是说:是当要实现某些算法时,会判断当前类是否实现了RandomAccess接口,会选择不同的算法。来达到最佳性能。
好了,下面就是今天的主角,ArrayList,(掌声):
ArrayList类:可调整大小的数组,继承List接口,实现了List接口定义的所有方法,并允许存储所有元素,包括null 。 除了实现List 接口之外,该类还提供了一些方法来操纵内部使用的存储列表的数组的大小。 (这个类是大致相当于Vector,不同之处在于它是不同步的)。我们来看看ArrayList是如何实现添加、删除元素的。
我们先了解一下ArrayList中的核心属性:
/**
*存储数组列表元素的数组缓冲区。
*/
private transient Object[] elementData;
/**
* 默认的初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* ArrayList的大小
* @serial
*/
private int size;
/**
* 列表被结构修改的次数。结构修改是指改变列表的大小,或者以一种可能产生不正确结果的迭代方式扰乱它。继承自AbstractList
* @serial
*/
protected transient int modCount = 0;
/**
* Shared empty array instance used for empty instances.
*
* 用无参构造的共享空数组实例。
*/
private static final Object[] 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.
* 用于无参构造的实例大小共享空的数组实例。
* 我们将其与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时应该膨胀多少
*
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
可能大家对最后两个属性会很迷糊,到底是做什么的,我们通过源码来了解这两个属性的用途。先看ArrayList的构造方法,看看ArrayList是如何产生的:
//如果构造ArrayList没有任何参数,那么直接让elementData等于空数组即可。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//如果传入了容量,那么就创建一个指定容量大小的数组,如果容量等于0,那么就让elementData等于空数组。
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);
}
}
```java
为什么要定义DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA赋值给elementData,而不是每次都new 一个对象在赋值给它?大家可以想象一下如果每次都是new一个对象,如果系统中创建大量的ArrayList,那么整个系统会存在大量的空数组,无疑会占用部分内存,降低系统性能。如果将一个空数组定义成final的,让所有创建的ArrayList初始都指向它,那么会大大减少内存占用。我觉得我们阅读源码的意义也体现于此。
好了,接下来我们来看看向ArrayList添加元素的过程:
```java
public boolean add(E e) {
//确保有足够的容量,新容量为当前容量大小加1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//看一下ensureCapacityInternal()的具体实现:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//记录修改次数
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//如果最小容量大于当前数组的长度,那么就进行扩容
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;//记录旧容量,就容量等于数组长度
int newCapacity = oldCapacity + (oldCapacity >> 1);//新容量等于旧容量的1.5倍。例如如果旧的容量是10,那么新的容量等于10+5=15。
if (newCapacity - minCapacity < 0)//如果新容量小于期望的最小容量,就让新容量等于最小期望的容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//如果新容量等于最大的数组长度,那么调用hugeCapacity方法获取一个容量,该方法根据传入的容量和MAX_ARRAY_SIZE比较,如果minCapacity>MAX_ARRAY_SIZE,则返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE。
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//Arrays.copyOf()方法,需要把数组中的数据复制一份,到新数组中。而这个方法底层是System.arrayCopy是一个native方法,效率不高。
elementData = Arrays.copyOf(elementData, newCapacity);
}
通过阅读源码我们发现,如果我们可以事先估计出数据量,那么最好给ArrayList一个初始值,这样可以减少其扩容次数,从而省掉很多次内存申请和数据搬移操作。能够提升系统的性能。
到此,我们对ArrayList添加元素的过程已经了如指掌,我们在来看看它是如何删除元素的:
public E remove(int index) {
RangeCheck(index);//进行范围检查,如果index>size那么就抛出异常
modCount++;//记录被修改的次数
E oldValue = (E) elementData[index];//记录要被删除的元素
int numMoved = size - index - 1;//要拷贝的数组长度
if (numMoved > 0)
//移动数组元素位置
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
将--size的位置置为null。
elementData[--size] = null; // Let gc do its work
return oldValue;
}
//System.arraycopy源码如下:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
代码解释:
Object src : 原数组、int srcPos : 从元数据的起始位置开始、Object dest : 目标数组、int destPos : 目标数组的开始起始位置、int length : 要copy的数组的长度
删除的过程就是:检查要删除索引的范围,向前移动数组元素,将size-1位置的元素置为空让GC回收。此外对于add()方法和remove()方法中涉及到modCount属性的修改,这是在使用迭代器遍历元素时,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了ArrayList,细节之后我们讲解迭代器模式的时候再进行讲解。
ArrayList的大致工作流程到此咱们就了解清楚了。本文若有不足之处,还请大家多多指正。若有疑问,请小伙伴们私信我,或者评论区留言。更多IT技术资讯,请大家关注《炫酷的Java》公众号阅读,谢谢大家。