ArrayList方法流程分析

ArrayList代码流程

从一个简单的例子开始

List<Integer> list = new ArrayList<>();
list.add(1);

构造方法

ArrayList()无参构造方法

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  // 默认数组长度 10
}

使用无参构造方法创建对象时,会创建一个默认长度为10Object数组

ArrayList(int initialCapacity)

在创建对象时也可以传递初始的数组长度,根据这个合法的容量来创建对应长度的Object数组

if (initialCapacity > 0) {
    this.elementData = new Object[initialCapacity];
}

ArrayList(Collection<? extends E> c)

创建对象时传递另外一个集合,引用从另外一个集合中继承数据

public ArrayList(Collection<? extends E> c) {
    // 获取集合的Object数组
    Object[] a = c.toArray();  
    // 集合元素个数大于0
    if ((size = a.length) != 0) {  
        // 如果传入的集合c是ArrayList
        if (c.getClass() == ArrayList.class) {
            // 因为和当前类类型相同,那么c的数组数据类型也是Object,所以直接赋值
            elementData = a;
        } else {
            // c的内部数据类型可能不是Object数组,
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // 创建一个空的Object数组
        elementData = EMPTY_ELEMENTDATA;
    }
}

add方法

提到ArrayList的add方法就不得不重点关注ArrayList数组的扩容方法

ensureCapacityInternal(int minCapacity)

根据capacity来判断当前数组是否需要扩容

// minCapacity,即添加元素后容量的大小
// 例如执行了一次add方法,那么minCapacity为size+1
// 执行了一次addAll()方法,那么minCapacity则为size+c.size()
private void ensureCapacityInternal(int minCapacity) {
    // 计算容量
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算容量
// elementData为具体存储数据的数组
// minCapacity为执行了add方法,数组存放元素的数量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果当前elementData数组为空的话
    // 判断minCapacity和默认的数组容量哪个大,返回其中大的一方
    // ps: 默认的数组容量为`10`
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
// 此时minCapacity为计算后的容量大小
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 差值大于0,则表示当前 elementData 数组已经无法容纳下新的元素了
    // 需要进行数组的扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// 扩容方法
private void grow(int minCapacity) {
    // 旧数组的长度
    int oldCapacity = elementData.length;
    // 新数组的长度 = 旧数组长度 * 1.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 判断新数组长度是否大于最大长度
    // ps: 最大长度为 Integer.MAX_VALUE - 8
    // 即 2147483639
    // hugeCapacity用于保证新数组的长度最大不会超过Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 将旧数组中元素拷贝到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

add(E e)

public boolean add(E e) {
    // 判断是否需要扩容
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}
// size+1如果大于当前数组容量,则进行数组扩容
if (minCapacity - elementData.length > 0)
    grow(minCapacity);

add(int index, E element)

将数据插入到指定位置

public void add(int index, E element) {
    // 判断index是否越界,如果越界(小于0||大于length)则抛出数组越界异常
    rangeCheckForAdd(index);
	 // 判断是否需要扩容
    ensureCapacityInternal(size + 1);
    // 将index位置开始的数据拷贝到index+1位置
    // [1] [2] [3] [4] [5] [ ] [] []
    // [1] [0] [2] [3] [4] [5] [] []
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 修改index位置的数据
    elementData[index] = element;
    size++;
}

addAll(Collection<? extends E> c)

将集合c中的全部元素添加到当前ArrayList中

public boolean addAll(Collection<? extends E> c) {
    // 集合c元素拷贝
    Object[] a = c.toArray();
    int numNew = a.length;
    // 判断是否需要扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount
    // 将集合c中的数据拷贝到数组中
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

addAll(int index, Collection<? extends E> c);

将集合c中的全部元素添加到当前ArrayList的index位置上

public boolean addAll(int index, Collection<? extends E> c) {
    // 检查是否有数组越界
    rangeCheckForAdd(index);
   	 // 集合c数组拷贝 
    Object[] a = c.toArray();
    int numNew = a.length;

    // 检查是否需要扩容 
    ensureCapacityInternal(size + numNew);  // Increments modCount

    // numMoved即需要移动几个位置
    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    // 数组拷贝
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

remove方法

remove(int index)

删除指定位置的元素,并返回被删除的元素

public E remove(int index) {
    // 检查index是否发生数组越界
    rangeCheck(index);

    modCount++;
    // 获取将要被删除的元素
    E oldValue = elementData(index);
	
    // 要移动数字的个数
    // eg: size=10,index=5
    // 删除5位置的元素,则需要将6~9位置的元素向前移动
    // numMoved则为 size - index - 1 = 10 - 5 - 1 = 4
    // 则有4个元素需要移动
    // 0 1 2 3 4 5 6 7 8 9
    //        m
    //         |--4--|
    int numMoved = size - index - 1;
    
    // numMoved大于0,则表示删除的是数组中间的元素
    // 如果numMoved等于0的话,则表示删除的是数组末尾的数字,不需要移动
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    
    // 将最后位置的数据引用置为null
    // 以确保GC回收时回收该索引指向的对象
    elementData[--size] = null; // clear to let GC do its work
	
    // 返回被删除位置的数据
    return oldValue;
}

remove(Object o)

删除数据o

// 删除具体的数据存在两种情况
// 删除的数据o为null和不为null
// 如果为null的话则遍历元素数组,找到第一个为null的位置,将其删除
// 如果不为null,则遍历元素数组,找到第一个equals为true的元素,将其删除
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;
}
// 和普通的remove类似
private void fastRemove(int index) {
    modCount++;
    // 需要移动元素的个数
    int numMoved = size - index - 1;
    // 大于0表示删除的元素在元素数组中间
    // 如果等于0则表示删除的是最后一个元素,不需要再进行数组的拷贝来覆盖
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 将最后位置引用置为null,方便垃圾回收
    elementData[--size] = null; // clear to let GC do its work
}
**`removeAll(Collection c)`**

removeAll是一个很有意思的方法,它可以删除当前集合与集合c相交的部分

集合A: {1, 2, 3, 4, 5 , 6}

集合B: {7, 6, 5, 9, 0}

那么A.removeAll(B)的结果则为{1, 2, 3, 4},删除了相交部分{5, 6}

public boolean removeAll(Collection<?> c) {
    // // 传入的集合c不能为空
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}
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++)
            // 如果集合c中没有当前元素的话
            // 将当前r位置的元素写入到w位置
            // 如果此时c中含有当前位置元素即elementData[r]
            // 则会跳过这个元素
            // eg: A: {1, 2, 3, 4, 5}
            //    B: {3, 4}
            // 那么则会有A集合中{3, 4}元素依次被覆盖的情况
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        // r不等于size,表示try代码块发生了异常
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        // w不等于size,表示当前集合中删除了元素
        // 需要将删除位置的引用置为null
        if (w != size) {
            // 将余下的引用置为null,方便GC
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

trimToSize方法

ArrayList中只有add时会检查是否需要扩容并进行容量的自动扩充,扩容为原先的1.5倍。

ArrayList不会自动的进行缩容,在remove时,只会将索引位置的引用置为空,方便垃圾回收不需要的对象,没有自动缩容的实现,要想进行容量的缩减,则需要手动的调用trimToSize方法

public void trimToSize() {
    modCount++;
    // size为存储数据的个数
    // elementData为存储数据的具体的数组
    if (size < elementData.length) {
        // 如果size小于数组长度,即当前数组没有存满的情况
        // 如果size为0的话,则将数组置为空数组{},注意不是**null**
        // 如果size不为0,则将数组空余部分都删减掉
        // eg: List size=4 cap=10
        // 即List内部数组长度为10,但是只存储了4个元素
        // 那么则会将数组的cap缩减为4
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

modCount

在ArrayList中经常会看到对modCount的操作,modCount是用来记录对集合操作的次数

如果单线程情况下,操作、修改和遍历ArrayList不会出现什么问题,但是如果多线程的环境下可能就会出现异常情况。在多线程情况下,我们使用iterator迭代器对ArrayList进行遍历时,就会检测出异常。

ArrayList集合中有这样的一个内部类

private class Itr implements Iterator<E> {
    int cursor;       // 下一个元素的索引
    int lastRet = -1;   // 最后一个元素的索引
    int expectedModCount = modCount;  // expectedModCount期望的修改次数
	
    // ......省略了其他方法或属性
    
    // 检测在遍历集合时,集合是否被修改
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

一个例子

// java.util.ConcurrentModificationException
public void doTest() {
    ArrayList<Integer> list1 = new ArrayList<>();
    list1.add(1);
    
    // 因为foreach本质上是使用的iterator迭代器进行遍历
    // 而迭代器在获取下一个元素时会执行上面的 checkForComodification 方法
    // 来检测当前集合是否被修改
    // 在foreach循环中我们向集合中添加了一个元素
    // 这就导致了modCount与exceptModCount不一致,导致了抛出了异常
    
    for (Integer i: list1) {
        list1.add(1);
    }
}

注意!!!

在代码中要是使用普通的for循环来遍历ArrayList集合的话

建议是提前保存集合的size一方面是不必每次循环都调用一次方法

另外一方面是如果在for循环体中执行的add方法的话,会导致ArrayList的size一直变化,i < list1.size()的判断条件一直不满足,导致死循环!

// 死循环!!!
public void doTest() {
    ArrayList<Integer> list1 = new ArrayList<>();
    list1.add(1);

    for (int i = 0; i < list1.size(); i++) {
        list1.add(1);  // 调用的add导致size+1,即使i+1也永远不会满足i<size
        System.out.println(list1.size());  // ......
    }
}
posted @ 2022-10-02 21:27  INEEDSSD  阅读(37)  评论(0编辑  收藏  举报