学习了一段时间,对于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方法:
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方法中,都有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); }
看到这,我们可以明白ensureCapacityInternal这个方法实际的作用就是确定内部数组最小需要的容量。真正扩容的操作在后边。我们跟进ensureExplicitCapacity方法,首先这个方法进行了modCount++自增操作,跟进父类,发现注释特别长,我没仔细看,大概就是记录数组改变的次数,这里还没有涉及到,涉及到的话我们在重点分析。
之后
if (minCapacity - elementData.length > 0)
之后就进行了真正的扩容操作
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这个方法
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方法
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]; }
然后再来看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方法也是非常简单
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));
我这里只是分析了这几个常用的方法,对于其他没有分析到的方法,大家可以自行查看代码分析,其思路和大致原理都是一样的,都是对内部数组进行一系列操作。