JAVA常用集合源码分析:ArrayList

ArrayList简介

ArrayList 是一个动态数组,所谓动态,是相对数组来说的,我们知道当我们在使用数组的时候必须指定大小,而且大小只能是固定的,有时候就很不方便,让人不爽。而我们的ArrayList恰恰解决了这一痛点,让我们可以不受束缚地使用数组。

阅读方法

  • 看继承结构与实现接口。 看这个类的层次结构,处于一个什么位置,可以在自己心里有个大概的了解。
  • 看构造方法 。在构造方法中,看做了哪些事情,跟踪方法中里面的方法
  • 看常用的方法。跟构造方法一样,这个方法实现功能是如何实现

实现了哪些接口

《JAVA常用集合源码分析:ArrayList》

1、List<E>接口:….
2、RandomAccess接口:这个是一个标记性接口,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如arrayList。而没有实现该接口的话,使用Iterator来迭代,这样性能更高,例如linkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好
3、Cloneable接口:实现了该接口,就可以使用Object.Clone()方法了。
4、Serializable接口:实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原来的类。

成员变量

     //默认大小
    private static final int DEFAULT_CAPACITY = 10;

    //暂时不知道有什么用,只知道是个空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //暂时不知道有什么用,只知道是个空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //存储数据的数组,从这里可以看出,arrayList底层是由数组实现的,transient表示不能被序列化
    transient Object[] elementData; // non-private to simplify nested class access

    //ArrayList元素的个数
    private int size;

从这几个属性中,我们可以知道一下两点

默认容量是10,也就是我们new 一个ArrayList时不指定大小的话,默认就是10

底层是由一个数组来负责存储数据的

至于EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个类成员,我们暂时不知道它们的作用

构造函数

    //指定大小 
    public ArrayList(int initialCapacity) 
 
    //不传指定大小的话,就用默认大小
    public ArrayList()

    //传一个集合对象
    public ArrayList(Collection<? extends E> c)
        
  

这三个构造函数都没什么特别的,都是我们平时用的比较多的,值得一提的是,前面我们提到的EMPTY_ELEMENTDATA 在构造函数的实现中出现了

《JAVA常用集合源码分析:ArrayList》

《JAVA常用集合源码分析:ArrayList》《JAVA常用集合源码分析:ArrayList》

根据代码,我们可以发现当容量为0(指定的大小为0或者传入的集合大小为0),就把我们存放数据的底层数组指向EMPTY_ELEMENTDATA这么一个空数组,当时无参构造的时候,底层数组指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA由此可见,这两个空数组的作用是标记底层数组的初始化方式(个人见解,勿喷~)

接下来我们再来看我们常用方法的源码

1.add(Object) 在末尾增加元素

//把一个对象加入ArrayList末尾
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}

这里我们看见了一个会让我们产生疑问的代码,就是 ensureCapacityInternal(size + 1) 这行代码,按理说,我们底层是数组实现,只要执行 elementData[size++] = e 就应该是完事了的,那么 ensureCapacityInternal()的作用是什么呢?根据方法名字,我们也能猜到,该方法是用来进行进行扩容相关的。我们在一开始也已经提到,虽然ArrayList号称动态数组,但是它底层依旧是数组实现的,数组是有固定大小的,所以我们在使用的时候底层必须实现一系列的扩容机制。

我们来看看该方法的具体实现

 private void ensureCapacityInternal(int minCapacity) { //minCapacity究竟是啥?
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //如果数据为空的话
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //此处相当于把minCapacity 设置为默认值10
        }

        //确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否够用
        ensureExplicitCapacity(minCapacity);
    }

在这个方法中,我们首先要明确minCapacity是个什么样的变量,有何意义。根据字面意思,它表示的是最小容量,回到我们调用该方法的时候,给它传的是 size+1 ,也就是说 minCapacity = size + 1,也表示完我们执行完add后预期的数组大小,如果数组的最大容量比这个小,说明无法装下,就要进行扩容操作。

一开始判断elementData是否为空,如果为空的话,显然空的数组无法添加对象,所以就把默认扩容大小设为默认大小(10)

在该方法中,又调用了一个ensureExplicitCapacity(minCapacity);方法,该方法传入minCapacity,我们接着看这个方法的源代码

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//应该是一个修改标志位?

        //当底层数组的大小比预期最小容量还小的时候,就必须进入扩容操作啦
        //这里又包含了两种情况
        //1.底层数组为空,将要添加的元素为第一个元素,此时minCapacity为1,经过一轮判断(上一个方法中),被设为10
        //2.底层数组不为空,此时minCapacity为size+1, 将之与底层数据大小进行比较,如果底层数组大小比他小,进行扩容

        if (minCapacity - elementData.length > 0)
            //执行扩容操作
            grow(minCapacity);
    }

在该方法中,调用了我们整个ArrayList的核心扩容代码 grow(),我们来看看它的源代码

private void grow(int minCapacity) {
        //把当前底层数组大小赋给旧值
        int oldCapacity = elementData.length;
        //扩容为原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果扩容后的值依然达不到预期值,就把新值设为预期值(add后的容量)
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //扩容后的值超过了允许的最大值,就把能给的最大值给newCapacity
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //复制一个新的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

 

由此,我们的add,操作便完成啦,核心便是扩容机制的实现。

2.add(int , E) 在指定位置增加元素

  public void add(int index, E element) {
        // 判断是否越界
        rangeCheckForAdd(index);
        
        //容量判断以及扩容处理
        ensureCapacityInternal(size + 1);  // Increments modCount!!

        //这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位,
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

rangeCheckForAdd的具体实现如下

private void rangeCheckForAdd(int index) {
        //索引大于数组大小,或者小于0,即越界,抛出越界异常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

至于扩容,在上头已经分析过了,这里不做赘述。

3.remove(int index) 按下标删除

public E remove(int index) {
        //检查下标是否合法
        rangeCheck(index);

        modCount++;
        //保存旧值
        E oldValue = elementData(index);
        
        //移动的位数
        int numMoved = size - index - 1;

        //把后面的往前移一位,这里用了个arraycopy方法,把 index+1后面的numMoved位复制到从index位开始的..
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);

        //置空,让GC处理它
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
}

这里我们碰到了System.arraycopy()这个方法,在前面我们也碰到过,这是方法主要用于数组复制,参数如下

public static void arraycopy(Object src, //src:源对象
             int srcPos, //源对象对象的起始位置
             Object dest, //目标对象
             int destPos,  //目标对象的起始位置
             int length)  // 从起始位置往后复制的长度

4.remove(Object)   //删除该元素在数组中第一次出现的位置上的数据。 如果有该元素返回true,如果false。

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

其实逻辑都不难,值得注意的是这个方法是可以接收null作为参数的,至于fastRemove(),内部实现几乎和remove(index)一样..唯一的不同应该是不用判断越界

5.removeAll() 批量删除

 public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c); // 判断是否为空,空的话扔出异常
        return batchRemove(c, false);
    }

这里简要介绍下 Objects.requireNonNull() ,首先Objects是java7新增的一个工具类,注意要和Object进行区别。该方法的具体实现如下,主要作用就是判空。

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;

接下来我们就要继续研究batchRemove()这个方法了

//这个方法,用于两处地方,如果complement为false,则用于removeAll如果为true,则给retainAll()用,retainAll()是用来检测两个集合是否有交集的。
private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData; //拷贝下数据部分
        int r = 0, w = 0; //r用来控制循环,w是记录有多少个交集 或者说 代表批量删除后 数组还剩多少元素
        boolean modified = false; //标志位,未修改
        try {
            //高效的保存两个集合公有元素的算法
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement) //如果c不包含
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.

            if (r != size) { //出现异常会导致 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;
    }

这段代码有点长,蛮复杂的。。得多琢磨琢磨

6.还有些clear,cotians,size等就不一一看源码啦,也没啥难度,主要就是要掌握扩容机制啦,还有最后的那个批量删除,也有一丢丢难度,其他的不多研究哦。

总结

arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是gorw()方法。

arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全是删除集合中的元素。

增删改查中, 增导致扩容,则会修改modCount,删一定会修改。 改和查一定不会修改modCount。

想说的话

(⊙o⊙)…这是我第一次研究源码….一开始我是拒绝的.密密麻麻的代码,还有令人头大的英文注释,啊!我要晕啦,不过想到自己还这么菜,于是就硬着头皮啃啦,好在ArrayList的实现并不是很复杂,所以第一次源码阅读虽然比较慢,但最后还是搞的差不多啦。同时,这也是我写的最久的一篇博文….所以增加了这么写废话,哈哈

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/qq_37410328/article/details/82883132
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞