Java进阶之----ArrayList源码分析

学习了一段时间,对于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;
    }


可以看到,逻辑还是很简单的, 只是对内部的数组进行了遍历,然后找出对应的对象,找到就返回true,没有的返回false。要注意的是遍历时对null和非null做了区分。避免抛出空指针异常。这个方法没有什么过多的需要分析的。


五、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));

这个例子说完之后,相信大家就可以理解了。


我这里只是分析了这几个常用的方法,对于其他没有分析到的方法,大家可以自行查看代码分析,其思路和大致原理都是一样的,都是对内部数组进行一系列操作。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值