ArrayList源码&扩容机制分析

ArrayList源码&扩容机制分析

ArrayList 简介

image-20230401195229720

image-20230401212826654

ArrayList继承了AbstractList,实现了RandomAcess,Cloneable,Serialiazable接口

  • RandomAccess:是一个标志接口,表明实现这个接口的 List 集合是支持快速随机访问的。可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • Cloneable,覆盖了函数clone(),能被克隆
  • Serialiazable:可以被序列化

ArrayList和Vector的区别

  1. ArrayList是List的主要实现类,底层使用Object[]存储,适用于频繁的查找工作,线程不安全。ArrayList扩容时候扩容到原来的1.5倍。
  2. Vector是List的古老实现类,底层使用Object[]存储,线程安全。Vector扩容时,如果没有指定capacityIncrement的话,就是扩容到到原来的两倍。如果指定了capacityIncrement的话,就是扩容到原来的容量+capacityIncrement的大小。

ArrayList和LinkedList的区别

  1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构:Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。【ArrayList实现了RandomAcces接口给,而LinkedList没有实现】
  5. 内存空间占用:ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList 扩容机制分析

先从 ArrayList 的构造函数说起

(JDK8)ArrayList 有三种方式来初始化,构造方法源码如下:

/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

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) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}

细心的同学一定会发现 :以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。 下面在我们分析 ArrayList 扩容时会讲到这一点内容!

modCount值的含义

modCount是在ArrayList继承的父类AbstractList中定义的。用protected修饰,不能序列化

protected transient int modCount = 0;

jdk官方对其的定义

/**
 * The number of times this list has been structurally modified.
 * Structural modifications are those that change the size of the
 * list, or otherwise perturb it in such a fashion that iterations in
 * progress may yield incorrect results.
 
 modCount表示list机构化修改的次数。
 在list中的什么操作叫做结构话修改呢?
 那些改变了list大小的操作
 或者是否以迭代的方式扰乱,可能会产生不正确的结果。
 */

这里以无参构造函数创建的 ArrayList 为例分析

add 方法

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!,所以会增加modCount的值
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal() 方法

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

calculateCapacity() 方法

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 这个比较,如果是new ArrayList()的方式的话,elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA就成立,所以,就会返回DEFAULT_CAPACITY也就是10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

当 要 add 进第 1 个元素时,minCapacity 为 size + 1 = 1,在 Math.max()方法比较后,minCapacity 为 10。DEFAULT_CAPACITY = 10

ensureExplicitCapacity() 方法

如果调用 ensureCapacityInternal() 方法就一定会进入(执行)这个方法,下面我们来研究一下这个方法的源码!

// minCapacity = 10
private void ensureExplicitCapacity(int minCapacity) {
    // 记录结构改变次数
    modCount++;

    // overflow-conscious code
    // 10 - 0 = 10   也就是需要的容量10大于现在的elementData数组的值,所以要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

我们来仔细分析一下:

  • 当我们要add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list)因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法。

  • 当 add 第 2 个元素时,minCapacity 为 2,此时 elementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。

  • public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
  • 添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。

直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容。

grow() 方法

private void grow(int minCapacity) {  // minCapacity = 10
    // overflow-conscious code
    int oldCapacity = elementData.length;   // 0
    int newCapacity = oldCapacity + (oldCapacity >> 1);  // 0
    if (newCapacity - minCapacity < 0)   // 
        newCapacity = minCapacity;  // newCapacity = 10
    // 新的容量不大于Integer的最大值,不用管
    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);
}

int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)! 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数.

我们再来通过例子探究一下grow() 方法 :

  • 当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 hugeCapacity 方法。数组容量为 10,add 方法中 return true,size 增为 1。
  • 当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。

这里补充一点比较重要,但是容易被忽视掉的知识点:

  • java 中的 length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.

  • java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法.

  • java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

hugeCapacity() 方法。

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8

ensureCapacity方法

ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?

/**
如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
 *
 * @param   minCapacity   所需的最小容量
 */
public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;

    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

理论上来说,最好在向 ArrayList 添加大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

总结:在用无参的构造器创建ArrayList的时候,如果执行了add方法

先执行ensureCapacityInternal(size + 1)方法,初始时,size = 0;再调用一个ensureExplicitCapacity(calculateCapacity(elementData, minCapacity))。

在calculateCapacity方法中会返回一个默认的数组长度就是10,

执行ensureExplicitCapacity(10),根据返回的结果判断是不是要执行grow方法。判断条件是 minCapacity - elementData.length > 0。

然后就会调用grow(10)方法,将数组扩容至10,然后将elementData[size++],返回true。

如果添加第11个元素的时候,11 - 10 > 0 又要调用grow()方法,新数组的长度就是10 + (10 >> 2) = 15。

  • 只有第一次添加元素的时候,calculateCapacity会直接返回一个10,然后进行扩容,再都是直接返回添加元素之后的长度。如果,添加元素之后的长度大于了上一次扩容之后的长度,那么就会再次进行扩容。
  • 以后的所有的扩容,都是依次类推的。
  • 如果计算出来的将要扩容的长度的值大于MAX_ARRAY_SIZE的话就会执行hugeCapcity()方法,会创建一个Integer.MAX_VALUE长度的数组

add(int index, E element)方法

public void add(int index, E element) {
    // 检查添加的位置是否越界
    rangeCheckForAdd(index);
	
    // 判断容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将index之后(包括index)的元素整体向后移动,为新添加进来的元素留出位置。所以,ArrayList在add的时候的时间复杂度是O(size - index)。
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
   	// 将要添加的元素放到index位置上
    elementData[index] = element;
	// 增加size
    size++;
}

rangeCheckForAdd(index)方法

// 判断添加的位置的索引是否越界
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

ensureCapacityInternal(size + 1)方法已经分析过,就是先计算下实际要的容量,除了无参的方式第一次会返回10之外,其他的都是返回实际存储元素实际需要的容量大小。如果实际需要的容量大于心在elementData的长度,就会扩容。扩容,就是1.5倍进行扩容。

总结:

  1. 检查是否越界
  2. 是否要扩容
  3. 将index(包括index)之后的元素整体向后移动一位
  4. 将要添加的数据放置在index位置上
  5. size++

addAll(Collection<? extends E> c)方法

public boolean addAll(Collection<? extends E> c) {
    // 将集合c转变为Object[] 数组
    Object[] a = c.toArray();
    // 要添加的元素的数量
    int numNew = a.length;
    // 判断是否要对数组扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount
    // 将添加的数据进行拷贝到elementData的最后
    System.arraycopy(a, 0, elementData, size, numNew);
    // 更新elementData中实际放的元素的数量
    size += numNew;
    // 返回结果
    return numNew != 0;
}

总结:

  1. 转化为Object[]数组,因为ArrayList的elementData是Object[]类型
  2. 判断是否要扩容
  3. 数据拷贝
  4. 增加size

get(int index)方法

获取指定索引位置的元素,时间复杂度为O(1)。

// 泛型在初始化ArrayList的时候已经描述好
public E get(int index) {
    // 检查是否越界
    rangeCheck(index);
	
    // 返回结果即可
    return elementData(index);
}

rangeCheck(index)方法

private void rangeCheck(int index) {
    // 只有index>=size的时候才会抛出异常IndexOutOfBoundsException,也就是说如果是小数的话,不会抛出异常IndexOutOfBoundsException,但是会抛出ArrayIndexOutOfBoundsException
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

image-20230531170850332

总结:

  1. 检查是否越界
  2. 返回值

remove(int index)方法

时间复杂度还是O(n - index - 1)

public E remove(int index) {
    // 是否超过上界,是的话抛IndexOutOfBoundsException异常
    rangeCheck(index);
	
    // 修改结构次数加一
    modCount++;
    // 拿到目前index位置上的值
    E oldValue = elementData(index);
	
    // 要移动的元素的个数,因为是从0开始的,所以,要多减1
    // 为了下一步的拷贝
    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;
}

ArrayList删除元素的时候并没有缩容

总结:

  1. 检查是否越界
  2. 拿到要移动的数据的个数
  3. 原数组上进行数据的拷贝
  4. 清理垃圾数组
  5. 返回旧数据

remove(Object o)方法

public boolean remove(Object o) {
    // 判断要删除的元素是不是空,ArrayList是可以放null值的
    if (o == null) {
        // 遍历数组,删除第一个为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;
}

fastRemove(index)

private void fastRemove(int index) {
    // 结构改变次数加一
    modCount++;
    // 拿到要移动的数据
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 进行数据的拷贝,也就是覆盖掉index位置上的数据
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 将垃圾数据置空,然后在GC的帮助下清理垃圾
    elementData[--size] = null; // clear to let GC do its work
}

总结:

在删除的时候只会删除在ArrayList中第一出现的数据

  1. 判断是不是null
  2. 是就找到第一个出现的index,然后将index后边的数据整体向前移动一个,覆盖掉index位置
  3. 返回就行

retainAll(Collection<?> c)方法

public boolean retainAll(Collection<?> c) {
    // c不能为空
    Objects.requireNonNull(c);
    // 批量删除
    return batchRemove(c, true);
}

batchRemove(c, true)

private boolean batchRemove(Collection<?> c, boolean complement) {
    // 将elementData赋值给一个新的Object数组
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        // 循环遍历list中的元素,直到遍历完
        for (; r < size; r++)
			// 检查c中是否存在elementData[r]元素
            if (c.contains(elementData[r]) == complement)
                // 赋值
                elementData[w++] = elementData[r];
    } finally {
        // 正常情况下,执行完之后,r的值是要等于size的,如果在执行contains中间发生了异常,r的值就会小于size
        if (r != size) {
            // 就会把原数组从r索引开始的位置,赋值到原数组的w位置上。
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            // 更新现在数组长度,w
            w += size - r;
        }
        // w不等于size,就要将多余的数据置空,避免内存泄露
        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;
}

contains方法

public boolean contains(Object o) {
    // 如果存在就返回true,不存在,返回false
    return indexOf(o) >= 0;
}

indexOf(o)方法

public int indexOf(Object o) {
    // 判断o是不是空的
    // 如果o在elementData中有的话,就返回它的索引,没有返回-1
    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;
}

总结:

代码写的真严谨

  1. 遍历elementData数组
  2. 如果在c中出现,就加到w的位置上
  3. 遍历之后w之前的元素就是共有的
  4. 产不产生异常都会将w之后的元素置空,等待垃圾回收

removeAll(Collection<?> c)

这个方法和retainAll()基本一样,就是在batchRemove方法的第二个参数传进去false。意思就是不在c中的元素会用w指针存储到elementData中。

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

补充

System.arraycopy()Arrays.copyOf()方法

阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)toArray() 等方法中都用到了该方法!

System.arraycopy() 方法

    // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义

// 就是将原数组从srcPos位置开始拷贝length长度到目标数组的destPos之后
    /**
    *   复制数组
    * @param src 源数组
    * @param srcPos 源数组中的起始位置
    * @param dest 目标数组
    * @param destPos 目标数组中的起始位置
    * @param length 要复制的数组元素的数量
    */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

场景:

public void add(int index, E element) {
    // 越界检查
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //arraycopy()方法实现数组自己复制自己
    //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}

Arrays.copyOf()方法

// 新创建一个数组,将原数组的值从头开始拷贝到新的数组中。
// 如果原数组的长度是大于新数组的话,那么只从原数组中拷贝新数组长度的范围,也就是,原数组能拷贝多少拷贝多少,拷贝不下的,新数组不要,也装不下。如果原数组的长度小于新数组的长度的话,就将原数组的内容全部拷贝到新数组中。
public static int[] copyOf(int[] original, int newLength) {
    // 申请一个新的数组
    int[] copy = new int[newLength];
// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

两者联系和区别

联系:

看两者源代码可以发现 copyOf()内部实际调用了 System.arraycopy() 方法

区别:

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置

copyOf() 是系统自动在内部新建一个数组,并且只能从头开始拷贝,不能自定义拷贝起点并返回该数组。

序列化

在以上的源码分析中,有一个变量很奇怪modCount,只见写,不见读,这是为什么呢?

回到开始,看看elementData的定义,transient修饰,不能序列化。

transient Object[] elementData; // non-private to simplify nested class access

但是ArrayList是实现了Serializable接口的,那么怎么序列化呢?

序列化

//
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

反序列化

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

modCount会在序列化和反序列化的时候用到来确保在序列化或者反序列化的过程中,结构没有发生改变。

posted @ 2023-05-31 18:16  Sstarry  阅读(11)  评论(0)    收藏  举报