以CURD角度手撕ArrayList源码

针对Java开发者而言,源码分析是必备的技能,有这么几种境界

1. 看的懂源码所实现的功能以及其逻辑

2. 看的懂源码如何设计,如何架构

3. 能在源码水平上进行扩展或者二次开发

鉴于笔者水平不足,经验不够,但还是希望能出一些源码分析系列的文档,希望借助此系列文档能够更好地帮助新手快速的理清或者学习源代码,达到第一个层次。因此笔者会尽量使用CURD操作的角度来分析JDK的源码。是因为多数程序员都是CURD阶段,当然最熟悉的还是CURD操作,因此以CURD的角度来看JDK的源码,相信会容易不少,毕竟万物皆可CURD,万物离不开CURD

注1:开发工具为IntelliJ IDEA 2020.1.2 x64 

注2:  JDK版本为JDK8  版本号为 jdk1.8.0_191


talk is cheap,show me the code ---undefined


代码段如下,有清晰明了的注释

import java.util.ArrayList;
import java.util.List;

public class ArrayListTest  {

    public static void main(String[] args) {

        //无参构造函数new一个ArrayList
        List arraylist=new ArrayList();

        //ArrayList的add操作
        arraylist.add("hello world");
        arraylist.add("world hello");
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

        //循环增长,查看动态grow函数
        for (int i=0;i<10;i++)
        {
            arraylist.add(i);
        }
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

        //Arraylist的getter setter操作
        System.out.println("getter"+arraylist.get(3));
        System.out.println("setter"+arraylist.set(3,"huhu"));
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

        //arraylist的addall操作
        arraylist.addAll(arraylist);
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

        //arraylist的index操作
        System.out.println("这是索引"+arraylist.indexOf("world hello"));
        System.out.println("这是最后索引"+arraylist.lastIndexOf("world"));
        System.out.println("当前ArrayList的容量大小"+arraylist.size());
        System.out.println("是否包含有"+arraylist.contains("huhu"));
        System.out.println("是否包含所有"+arraylist.containsAll(arraylist));
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

        //arraylist的remove操作
        System.out.println("删除元素"+arraylist.remove("world hello"));
        System.out.println("删除元素"+arraylist.remove(4));
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

        //arraylist的retain操作
        arraylist.retainAll(arraylist);
        arraylist.removeAll(arraylist);
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

    }


}

OK,下面我们试着断点分析来看看ArrayList的源码

到此步,请保留你的耐心,因为看源码是一个非常枯燥和非常花时间的过程,如果你觉得看不下去,可以休息一会儿,再继续。毕竟看源码,如同登山,当你在山顶时,相信看的风景会非常不一样的。

1. 看new ArrayList发生了什么

         //无参构造函数new一个ArrayList
        List arraylist=new ArrayList();

此行打断点(ctrl+F8),启用单步调试(F7)或者强制进入(Alt+Shift+F7)可以看到

        public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;         //无参构造函数初始化容量为10的空数组,目前代码只是初始化为空数组,并没有指定容量?什么时候指定的容量?
    }

可以看见new ArrayList()调用了无参构造函数,在此构造函数中ArrayList将之前初始化过后的静态变量  DEFAULTCAPACITY_EMPTY_ELEMENTDATA  (初始化构造了一个默认容量为10的空数组列表)并将其引用赋值给了ArrayList的本地真实的存储结构即elementData。

注3:无参仅仅初始化了一个空的数组列表,并没有指定数组的大小 (此为无参构造函数做的事)那么是什么时候指定了默认容量为10呐?是在calculateCapacity方法中,当elementData为默认容量的空数组列表时,返回一个10的值表示为列表的容量大小(后文会叙述)

Q1:ArrayList有几个构造函数?分别对应哪种情况?

Q2:为什么默认容量为10?

2. 看 ArrayList的add操作发生了什么

        //ArrayList的add操作
        arraylist.add("hello world");
        arraylist.add("world hello");
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

        //循环增长,查看动态grow函数
        for (int i=0;i<10;i++)
        {
            arraylist.add(i);
        }
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

在第一个add方法代码行出打一个断点,操作如同上文,在循环中的add又打一个断点(是为了方便查看ArrayList的动态扩容机制),启用单步调试(F7)或者强制进入(Alt+Shift+F7)可以看到


    public boolean add(E e) {                           //此函数将指定的元素放在列表末尾(以追加的方式)
        ensureCapacityInternal(size + 1);  // Increments modCount!!     //以当前大小+1为最小容量值为参数来判断所需要的容量大小
        elementData[size++] = e;
        return true;
    }


    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {         //当elementData是默认容量数组列表时,则返回默认容量大小10
            return Math.max(DEFAULT_CAPACITY, minCapacity);             //在这儿返回指定了elementData的默认容量大小10
        }
        return minCapacity;                                             //当elementData数组元素时候。则返回最小的容量大小
    }


    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;                                 //结构性变化+1

        // 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操作都发生了什么?

add->ensureCapacityInternal->ensureExplicitCapacity->calculateCapacity

1. calculateCapacity是计算ArrayList的最小容量的

如果最开始是初始化的空数组列表,则返回10的初始化容量,如果不是,则返回代入的参数即最小容量值

注4: 在看此源码的过程中,需要分清这么几个概念,通常概念不清,就看源码比较困难

长度指的是ArrayList的长度,表示是ArrayList可以容纳的元素多少个数

最小容量值指的是ArrayList在结构性变化的过程中所需要的容纳空间

大小指的是ArrayList实际上所拥有的元素个数的大小即size方法所返回的size值,也即ArrayList中的size属性

2. ensureExplicitCapacity是用来调用grow方法和针对modCount自增的

注5: modCount表示针对ArrayList的结构性变化的指针标识,如add,remove等

3. grow实现了ArrayList的动态扩容机制(重点)(面试常问)

在grow方法体内我们可以看到

旧容量为原来的ArrayList的长度大小

而新容量则是旧容量的1.5倍

若新容量都小于所需要的最小容量,则将最小容量赋值给新容量

若新容量比极限容量要大,那么就将极限容量和最大整型数当中的最大值赋值给新容量

完成以上的步骤后,就根据新容量来初始化一个新的数组,将原来的elementData的所有元素都等位的复制到新的数组当中,然后将新的数组引用赋值给elementData,这样就完成了ArrayList的动态扩容机制

Q3:ArrayList的动态扩容机制是怎么回事?

Q4:什么时候会触发ArrayList的扩容?

Q5:为什么新容量会设置成旧容量的1.5倍?为什么会采取位运算而不是简单的乘除运算?

Q6:若需要的最小容量都达到了极限容量值,是否还需要扩容?是否会报异常?

Q7: grow方法会被哪些方法给调用?哪些是显式调用?哪些是隐式调用?

Q8:ArrayList的扩容机制的时间复杂和空间复杂度是?频繁的动态扩容会导致什么样的问题?

4. 经过了以上的动态扩容之后

elementData就是新容量的数组列表了,然后执行了size++,并将这个位置上的元素填上所传入的参数值。

注6: add操作是在数组列表末尾直接追加元素,所以才使用了size++,并元素赋值

5. 成功之后,返回真值

3. 看ArrayList的getter setter操作发生了什么

           //Arraylist的getter setter操作
        System.out.println("getter"+arraylist.get(3));
        System.out.println("setter"+arraylist.set(3,"huhu"));
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

按照上面的方法同样的打断点进入调试,可以看见

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }


    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }



    // Positional Access Operations

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

此源码可以分3部分看,一部分是getter setter操作,一部分是rangeCheck,一部分是elementData

先看getter,可以看到

1. 先调用了rangecheck

步入rangeCheck,发现是针对传入的索引值进行合法性检查,若传入的索引值大于或者等ArrayList的大小,就抛出了 IndexOutOfBoundsException 说明了是索引溢出边界异常,在上面的注释可以发现,若索引值是负数的,就抛出 ArrayIndexOutOfBoundsException 说明了是数组索引溢出边界异常

2. 若传入索引值合法

则调用elementData方法,返回该索引位置上的元素

Q9:为什么会有两个索引异常?一个是 IndexOutOfBoundsException 另外一个是 ArrayIndexOutOfBoundsException? 是针对什么情况呢?针对索引值的检查?如此代码就足够了吗?

Q10:为什么要编写一个elementData方法?通过这个方法来返回元素?而不是直接使用 e=elementData(index)?

再看setter,可以看到

1. 调用rangeCheck检查索引合法性

2. 将索引值上的元素作为旧元素返回

3. 传入的新值赋值给索引值上的元素

此getter setter方法都比较简单,就不多说了

4. 看addAll操作发生了什么

           //arraylist的addall操作
        arraylist.addAll(arraylist);
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

同上一样的进入调试,可以发现


    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }


1. 将传入的集合通过toArray方法转为object的数组对象列表

2. 调用ensureCapacityInternal方法来计算当前size+集合中的元素大小是否能够有容纳空间?如果有,就继续,如果没有就需要调用grow方法进行扩容,直到了有容纳空间为止(grow的具体操作可以参考上文)

3. 调用System.arraycopy方法完成一次数组之间的复制,将集合中的元素追加复制到elementData中

4. size+numNew完成size大小的更新

5. 返回boolean值

5. 看index操作和contain操作都发生了什么

            //arraylist的index操作
        System.out.println("这是索引"+arraylist.indexOf("world hello"));
        System.out.println("这是最后索引"+arraylist.lastIndexOf("world"));
        System.out.println("当前ArrayList的容量大小"+arraylist.size());
        System.out.println("是否包含有"+arraylist.contains("huhu"));
        System.out.println("是否包含所有"+arraylist.containsAll(arraylist));
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

同上一样的进入调试,可以发现


    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;
    }



    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;
    }

先看indexOf,可以看出

1. 对传入的object对象引用判空

2. 如果是空,就for循环来获取整个ArrayList中第一次出现null的值,如果有,就返回该处索引值,如果循环完了,都没有,就返回-1

3. 如果object不是空,一样的for循环遍历整个ArrayList,查找第一次出现该值相同的位置,并返回该值所处的索引值,若遍历完了,都没有,就返回-1

再看lastindexOf,可以看出

实现的逻辑是跟indexOf是差不多的,都是遍历整个ArrayList来查找object,唯一不同是,遍历是倒序遍历,返回自然是最后一次出现该值的索引

再看contain,也可以发现

调用了indexOf,只不过是是通过indexOf来查找该元素的索引值,然后比对索引值是否大于等于0,若是就返回真,否则就是假,表示该元素在整个ArrayList中不存在

Q11:为什么需要对传入的object判空?空值和非空值对判断查找是否有区别?

6. 看remove操作发生了什么


        //arraylist的remove操作
        System.out.println("删除元素"+arraylist.remove("world hello"));
        System.out.println("删除元素"+arraylist.remove(4));
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

同上一样,调试进入源代码,可以看出


    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        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

        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
    }


remove这个操作有两个重载函数,一个是根据索引来remove,一个是根据对象来remove

先看根据索引来remove的代码

1. 检验索引是否合法

2. 结构性标识变化加1

3. 计算需要开始移动的索引位置

4. 如果索引位置大于0,就调用System.arraycopy从需要移动的索引位置开始从后往前覆盖复制

5. elementData的最后一个元素位置设置为null

6. 返回旧值

Q12:为什么要对elementData最后一个末尾元素设置为null?注释解释为了让gc能工作,那么仅仅是为了让gc工作么?gc又是什么时候发生gc的呢?若对不需要的元素引用设置为null,是否能马上gc?

再看根据object来remove的代码

1. 很显然首先需要判断object是否为null

2. 根据是否为null值,分两种方法,循环遍历查找对应的值

3. 调用fastremove方法

4. fastremove方法中的代码跟上文的remove(int index)步骤下2到5一致,也就是2到5封装成了一个fastremove方法

Q13:为什么步骤2到5封装成了一个fastremove方法?是否能快速删除?

7. 看retainAll和removeAll发生了什么

        //arraylist的retain操作
        arraylist.retainAll(arraylist);
        arraylist.removeAll(arraylist);
        System.out.println("当前ArrayList的容量大小"+arraylist.size());

同上一样,调试进入源代码,可以发现

  
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }



    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }


    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

1. 调用object.requireNonNull检查元素object是否为空

2. 不管是retainAll还是removeAll都调用了batchRemove方法,只不过传入参数一个正一个负

3. 来看看batchRemove究竟发生了什么

4. 可以看到batchRemove的大概逻辑是这样的

4.1 将自身的elementData做为一个缓冲区(可修改),设置r、w的读写指针,设置修改标志变量

4.2 循环遍历ArrayList列表元素,看集合c是否包含ArrayList集合元素

在retainall中,如果包含,就在elementData上w位置替换成r位置上的元素,然后w自增(实际上就是把包含的元素按照原顺序在elementData中保留下来)

在removeall中,如果不包含,就在elementData上w位置替换成r位置上的元素,然后w也自增(实际上把不包含的元素按照原顺序在elementData上保留下来)

注7:修改都是在elementData上修改的,任何改动都会修改到ArrayList实际上的数据元素

4.3 如果r不等于size,则通过System.arraycopy来复制完成elementData的元素修改

4.4 如果w不等于size,则将elementData中不需要的元素循环置空,方便gc回收

5. 返回成功与否标志

Q14:为什么会有r!=size,w!=size的判断?

Q15:为什么会直接在elementData上修改,而不另外单独设立一个可供修改的缓冲区数组?

8. 其他常见的功能函数分析

8.1 size函数


    public int size() {
        return size;
    }

非常简单,就是返回size的数值,size表示就是ArrayList的所拥有元素的大小

注8:size并非等于elementData的长度,也即size!=length,通常长度要比大小要大

8.2 isEmpty函数


    public boolean isEmpty() {
        return size == 0;
    }

也是非常简单,常用的函数,表示ArrayList是否是空列表,如果是返回真值,若不是,返回假值

8.3 toArray函数


    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

将集合元素转换成为数组对象的函数,一个是无参的转换函数,一个是有参的转换函数,其实质都是调用了Array.copyOf的复制函数

8.4 clear函数

 
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

将集合中所有元素置空,然后让gc回收的函数表达

9. ArrayList的迭代器分析

注9:针对以上提出的问题,笔者会单独开辟一个专栏,是针对现在面试官经常问的问题以及我个人在学习源码过程中所能想到一些思考问题,会进行一定的解答。码字不容易,请读者能点个赞,评论一下,便是足以了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章