学习了一段时间,对于Java已经算是比较熟悉了。但是仅仅还是停留在熟悉的阶段,最近不是很忙,所以抽空来深入的学习一下Java一些类的具体实现。集合在我工作中用的比较多,所以,我就先从集合入手,一步步分析其中的最常用的方法。今天就先来分析ArrayList的具体实现。
关于如何获取Java的源代码,我们可以在Java的安装目录下的src.zip里得到,解压之后导入eclipse里即可。
言归正传,我们首先来分析ArrayList里的几个主要的方法:add、contains、get、set、remove这几个方法。
在分析之前,我们要先搞明白一个问题,就是ArrayList的实质是什么?有一些经验的朋友都知道,ArrayList的实质其实就是封装了对数组的一些操作,通过这些操作,从而达到我们需要的目的。有了这个认知,我们在后边的分析就容易的多了。
一、类中的几个变量
<span style="font-size:18px;">private static final int DEFAULT_CAPACITY = 10; //默认的初始化数组大小
private static final Object[] EMPTY_ELEMENTDATA = {};
private transient Object[] elementData; //ArrayList内部使用数组来进行实现。其实质上就是对数组的操作。
private int size;</span>
要注意的是size变量表示的是当前内部数组里有几个元素,而不是当前内部数组的长度。
二、构造方法
ArrayList构造方法有3个:
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0) //传入参数不能小于0,否则抛异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
PS:ArrayList在构造时,
(1)可以不传参数,那么就会使用默认的数组大小,也就是10个,进行对内部数组的初始化。
(2)如果传入int型变量,则用给定的int数字来对内部数组初始化。
(3)传入一个Collection对象,构造方法可以将Collection转换为数组,然后将转换的数组拷贝到内部数组中。
总结以上:ArrayList的构造方法实质上就是对ArrayList内部的Object数组进行初始化。这三个构造方法不同的在于初始化数组的大小,以及初始化后是否对其进行赋值等操作的区别。
三、Add方法分析
我们下面来分析add方法:
public boolean add(E e) {
// 确保当前的数组大小可以装的下传入的对象e
ensureCapacityInternal(size + 1);
// 在内部数组中存放对象,并将索引值+1
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
// 确保传入的数值没有越界
rangeCheckForAdd(index);
// 确保当前数组可以存放传入的对象e
ensureCapacityInternal(size + 1);
// 移动数组,确保内部数组在当前位置有地方存储对象e
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 在内部数组中存放对象
elementData[index] = element;
size++;
}
通过代码我们可以发现,在对元素进行插入的时候,使用第二种重载方法时,涉及到了对数组的移动,所以相对于第一种add方法来说效率不是很高。而且在add进数组的时候,也并没有对传入的对象进行判空,所以ArrayList中是可以存储null值的。
我们可以发现,两个add方法中,都有ensureCapacityInternal这个方法,我们按F3跟进去,看看里边是干嘛的。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) { // 若当前内部数组为空,则让<span style="font-family: Arial, Helvetica, sans-serif;">DEFAULT_CAPACITY,也就是10,与当前数组大小进行比较,取最大值</span>
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(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);
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);
}
好,跟到头了,反正我第一次看完之后是一头雾水,不过不着急,我们来慢慢分析。
我们从头开始看,在add方法里,在真正的对内部数组赋值之前调用ensureCapacityInternal,意义何在?很明显,怕这个内部数组不够用了,所以在进行真正的赋值操作之前,一定要确认这个内部数组有足够的空间来存放我们给的对象,so,ensureCapacityInternal这个方法的使命就是,确认当前数组大小是不是能装下,如果不能,就要对这个内部数组进行扩容。而我们知道,在Java里,数组一旦被初始化完成,是不能改变其大小的,ArrayList是如何实现的?我们慢慢来看。
我们看到ensureCapacityInternal里有这样一句,
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
这句话只针对new ArrayList();这种初始化方法有效,因为上边三个构造方法中,只有无参构造方法让内部数组等于empty。我们来看EMPTY_ELEMENTDATA是{}。很显然,我们用这个构造方法时,是不能直接对这个内部数组赋值的,所以需要进行”扩容“。既然要进行扩容,那么必然要给出一个最小值,即,这个数组要扩大多少?所以minCapacity这个变量就代表了需要扩充的最小的容量,否则就不能进行对内部数组的赋值。之后,ensureCapacityInternal调用了ensureExplicitCapacity方法,传入了最小需要的容量。
看到这,我们可以明白ensureCapacityInternal这个方法实际的作用就是确定内部数组最小需要的容量。真正扩容的操作在后边。我们跟进ensureExplicitCapacity方法,首先这个方法进行了modCount++自增操作,跟进父类,发现注释特别长,我没仔细看,大概就是记录数组改变的次数,这里还没有涉及到,涉及到的话我们在重点分析。
之后
if (minCapacity - elementData.length > 0)
判断是不是真的需要扩容,怎么说呢,比如上边我们用 new ArrayList()这个方式的话,根据上边的代码minCapacity=10,而现在内部类真正的大小时0,所以需要扩容。如果我们用new ArrayList(20)这个方式的话,内部数组在构造方法内已经初始化了,所以内部数组长度为20,而在add方法的时候,第一个操作传入的是size+1,即0+1。所以到达这个判断的时候,最小需要的容量为1,而长度为20 ,必然不需要扩容。
之后就进行了真正的扩容操作
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 新长度为原数组长度的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
最后将扩容好的好的数组返回。完成流程。
听我说了这么多,相信很多人还是不理解,这里我把以上内容总结成一句话:
原内部数组中的对象数目加1,如果大于原内部数组长度,则以原长度的1.5倍新建一个对于原数组的拷贝,并修改原数组,指向这个新建数组。原数组自动抛弃,size则自增1,向数组中添加对象。
看完了是不是觉得明白一点了呢?
add方法的另一个重载,与这个类似,不过其首先判断给定的位置是否发生数组越界,之后的操作大致相同。不过最后的赋值时,发生了数组移动,所以对于效率要求较高的,且List里数据很多的情况下,个人建议少用这个方法。
四、Contains方法分析
我们再来看contains这个方法
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
五、Get方法分析
接下来是get方法
public E get(int index) {
rangeCheck(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方法实质上也很简单,首先要 对取的位置进行范围判断,避免数组越界。之后就直接返回内部数组里的元素即可。从效率上来讲也是很高的。
六、remove方法分析
然后再来看remove方法,remove方法有2个,传入的参数分别是int和Object。我们来看看他们的具体实现。
public E remove(int index) {
rangeCheck(index); //判断传入的数字是否在合理范围内,即是否小于数组内真实的数据个数
modCount++; // 更改次数自增
E oldValue = elementData(index); // 将要remove的索引位置的元素取出
// 将内部数组中空出来的那个位置之后的元素移动到前边去
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 将最后一位置空,size自减
return oldValue; // 返回移除的那个数据
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
可以看出来,这个重载的方法实质上是先根据给定的对象,在内部数组中遍历出对应的索引值,之后根据索引值去删除,与上边那个方法大同小异。
总结起来就是:根据给定索引值,判断合理性,之后取出对应这个索引位置的对象,之后将这个位置之后的所有对象向前移动一位即可。
七、Set方法分析
set方法也是非常简单
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
相信大家不需要分析也能看出来, 判断索引值合理性,在给定的位置用给定对象覆盖掉原有对象,并返回原来的对象。
所以当需要进行覆盖操作时,要尽量使用set方法而非add方法,因为set方法中不涉及到对数组的移动,效率上自然就高了些许。
-----------------------------------------------------------------------------------------------------
有些朋友可能对System.arraycopy这个方法感到陌生,我特地查了一下文档,来给大家解释一下。System.arraycopy有5个参数,他们的含义分别是:
src -- 源数组.
srcPos -- 源数组中的起始位置。
dest -- 目标数组。
destPos -- 目标数据中的起始位置。
length -- 要复制的数组元素的数目。
这么说可能还是不明白,那么我们用一个例子来说明一下
int[] arr1 = { 0, 1, 2, 3, 4, 5 };
int[] arr2 = { 6, 7, 8, 9, 10, 11};
// 将arr1的元素复制到arr2中,从arr1的索引位置为3开始,复制长度为1个,到arr2中,arr2从索引为0的位置开始接受复制
System.arraycopy(arr1, 3, arr2, 0, 1);
// 所以最后结果是-- arr1:{ 0, 1, 2, 3, 4, 5 } arr2:{ 3, 7, 8, 9, 10, 11}
System.out.println(Arrays.toString(arr1));
System.out.println(Arrays.toString(arr2));
这个例子说完之后,相信大家就可以理解了。
我这里只是分析了这几个常用的方法,对于其他没有分析到的方法,大家可以自行查看代码分析,其思路和大致原理都是一样的,都是对内部数组进行一系列操作。