带你走进Java集合_ArrayList源码深入分析

  ArrayList是List的接口中一个非常重要的实现类,也是项目中用的最频繁的集合,要了解为什么是最频繁的,就需要我们走进ArrayList内部,进行剖析它。

一、ArrayList内部的数据结构

  从ArrayList源码中我们可以很清楚的看到,ArrayList底层的数据结构是数组,所有ArrayList集合的增删改查无非就是对数组的增删改查,但是我们又知道数组的长度是不可变的,那么当数组满时,我们怎样在向ArrayList中添加呢?听我慢慢道来

二、ArrayList几个重要的属性

第一个属性:数组的默认大小

 /**
  * 底层默认的数组大小
 */
private static final int DEFAULT_CAPACITY = 10;

  第二个属性:当构造函数为ArrayList(0)

/**
*  当构造函数是ArrayList(0)时,默认的数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

 第三个属性:当构造函数为ArrayList()

/**
* 当构造函数是ArrayList()时,默认的数组 .
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

第四个属性:ArrayList底层的数据结构

/**
* ArrayList存储数据的数组
*/
 transient Object[] elementData;

 有的同学不了解transient关键字的作用,这个关键字最主要的作用就是当序列化时,被transient修饰的内容将不会被序列化,如果想深入了解transient,请自行搜索。这里不做深入研究,你就记着ArrayList的数据在序列化时底层数据将不会被序列化。

第五个属性:集合数组修改次数的标识

protected transient int modCount = 0;

第六个属性:ArrayList实际的长度

/**
* 集合的长度
*/
 private int size;

 这里有必要解释一个知识点,size是否等于elementData.length呢?刚接触ArrayList集合的也许认为就是一样的,其实这种说法是不正确的,size和elementData的长度并没有必然的联系,例如,如果我们ArrayList()时,ArrayList只是给我们默认的elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此时只是空数组,只有第一次add时才默认数组的大小为DEFAULT_CAPACITY=10,此时elementData.length=10,而size=0,所有说两者并没有必然的联系,要说有联系,也是elementData.length>=size.

总结:上面六个属性是ArrayList重要属性,前五个是ArrayList内部使用的,我们在用ArrayList的时候不会用到它们,但是size是我们使用非常频繁的。

三、ArrayList构造方法

第一个构造方法:无参构造方法

 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

第二个构造方法:有参构造方法

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

第三个构造方法:参数是一个集合

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

第一个和第二个构造方法我们有必要说一下,当ArrayList()和ArrayList(0)时,为什么默认的是不同呢?我们可以仔细看出,无参构造函数只是将底层的数据默认成DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,数组并没有长度,当我们第一次调用add时要进行初始化数组的长度为:DEFAULT_CAPACITY =10,而有参构造函数的是要初始化数组的长度的,即使是0,在接下来无参构造函数要初始化数组长度的,而ArrayList(0)则已经初始化了,ArrayList()和ArrayList(0)用不同的默认,则就是为了下面第一次扩容时进行区别。

四、ArrayList重要的方法解读

第一个重要的方法:增加

public boolean add(E e) { ensureCapacityInternal(size + 1); -------------------------❶ elementData[size++] = e;------------------------------------❷ return true; }

add方法非常的简单,总共两行代码,

判断是否扩容,

将加入的放到数组的末尾,
至于怎样扩容的

并且怎样把默认的无参构造初始化为10的,当数组满后,怎样扩容的,这些我们等会在讲,我们先了解一个非常重要的内容,就是当多线程并发时,ArrayList为什么是不安全的。

第一个多线程情况:底层数据还有一个空位置,数组越界

线程1执行❶,因为有一个空位置,则不扩容.此时线程1让出CPU  
线程2执行❶时因为有一个空位置,则不进行扩容,当执行完❷后,数据已经满了。
然后线程1获取CPU,开始执行❷,此时数据已经满了,在进行添加,就会数组下标越界

第二个多线程情况:扩容,出现脏数据

线程1执行后,进行了扩容,然后执行,我们知道size++是复合操作读-改-写,当size=10.
线程2执行后,因为线程1已经扩容了,则无需扩容了,执行,因为线程1还没有执行++,此时size=10.
所以两个线程同时向size=10下标写数据,导致脏数据的出现

从上面的两个出现的情况可以看出,ArrayList在多线程下是不安全的,如果在多线程下不进行安全处理,则不能把ArrayList创建成共享的数据,就是全局变量。

好了,我们回到add方法的看看是怎样扩容的,先上代码

 private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {--------------------❸
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);-----------------❹
        }

        ensureExplicitCapacity(minCapacity);---------------------------------------❺
    }

当看到❸时,是不是想到上面我们讲到的,当进行无参数构造函数时,在第一次调用add时会进行对数组初始化大小,❹获取两者的最大值,minCapacity=0,DEFAULT_CAPACITY =10,所以是10,接下来看❺

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 此时:minCapacity=10,elementData.length=0 所以会执行grow 
        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);
    }

最终扩容的真正逻辑在grow中出现了,newCapacity是新数组的长度,newCapacity=oldCapacity+(oldCapacity>>1),这句话的意思就是老数组的长度+老数组长度右移一位,newCapacity=1.5oldCapacity.

就是如果ArrayList集合扩容,则扩容为以前的1.5倍。下面的代码就很容易理解了,大家自行观看,然后把老数组的数据复制到新数组中。其他的add(index,E e),set方法类似,这里就不在阐述了

第二个重要的方法:删除remove

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

❻是判断用户给出的index是否合法,就是判断index>=size,就是不合法,将会抛出异常,❼是获取将要删除的值,❽获取将要移动的下标,❾把删除位置后面的数据进行向前移动

从add(index,e)和remove两个方法可以看出,ArrayList集合的插入和删除需要对数组进行复制和移动,所以效率相对较低,所以ArrayList不适合那些插入和删除较多的逻辑。

第三个重要的方法:查

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

        return elementData(index);
    }

查是非常的简单的,首先判断index的合法性,然后通过给定的下标获取值。这个方法虽然简单,但是我们可以看出ArrayList对于查效率是非常的高的,时间复杂度(o1)

这篇文章就先写到这里,我们接下来总结一下:

1:如果调用的是无参构造函数,则默认的是空数组,并没有对数组的长度赋值

2:调用add(e)时,首先判断是否进行扩容,然后把e添加到最后,这个方法只是在扩容时进行数组的复制,不扩容时,没有数组的复制。

3:调用add(index,e)和remove时都有数组的复制和移动,所以效率相对较低,而查询get效率高,所以我们可以认定ArrayList适合于读取多,插入和移除少的情况

4:ArrayList并不适合多线程下作为共享变量,ArrayList是线程不安全的。

下一篇文章我将逐渐深入ArrayList其他方法的源码分析和用途,请持续关注

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