ArrayList源码解析(二)

本文深入解析了Java中ArrayList的工作原理,包括元素添加、删除、扩容等核心操作的实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在末尾添加一个元素add( E e )

public boolean add(E e) {
        ensureCapacityInternal(size + 1);//确保内部容量够用,必要的话进行扩容。否则elementData[size]将会数组越界
        elementData[size++] = e;//相当于elementData[size] = e; size++;先赋值,再++。
        return true;
    }

从源码上看,添加元素之前调用ensureCapacityInternal(size+1)扩充容量。现在我们来看看具体的扩容操作

如何扩容 ensureCapacityInternal( int minCapacity )

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);  //从这里可以看出,只要添加了元素,ArrayList的容量最少为10
        }

        ensureExplicitCapacity(minCapacity);
    }

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

        if (minCapacity - elementData.length > 0)//当需要的容量大于当前容量时,进行扩容
            grow(minCapacity);
    }

private void grow(int minCapacity) { //扩容核心代码
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//容量扩充到原来的1.5倍,整除
        if (newCapacity - minCapacity < 0) //如果扩容后的新容量还是没有传入的所需的最小容量大
                                                 //(主要在addAll(Collection e)方法中),以较大的也就是传入的容量为准
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//是否超过最大限制
            newCapacity = hugeCapacity(minCapacity);

        elementData = Arrays.copyOf(elementData, newCapacity);  //扩充容量,将elementData数组的容量增加到newCapacity                                                                         
        // 并将原有数据复制过去  
    }

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

从上述源码上看,add方法先调用ensureCapacityInternal扩充自身容量,如果扩充后的容量比常量DEFAULT_CAPACITY(10)小,则扩充到DEFAULT_CAPACITY,也就是说填充了元素的list最小容量为10。
如果扩充后的容量比数组elementData的长度大,那么调用grow()方法将数组大小增加为原来的1.5倍,如果1.5倍的数组大小还不够,那么将数组内存扩大到指定的容量大小。(MAX_ARRAY_SIZE=Integer.MAX_VALUE-8)

注意:一般情况下扩充到1.5倍是够的,因为add方法传入的最小扩容量为size+1, 1.5倍的数组大小肯定是够的,这里主要考虑到是addAll(Collection c),它传入的最小扩容量为size+c.length,如果c.length比较大,那么1.5倍的size可能不够,那么扩充的容量就以传入的为准。总得来说,1.5倍size和传入的最小扩容量取大的那个。

这里还考虑到了另一种极端情况:溢出。

 int newCapacity = oldCapacity + (oldCapacity >> 1)
 if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

如果oldCapacity非常大,接近了MAX_ARRAY_SIZE.那么1.5倍的oldCapacity也就是newCapacity可能会发生溢出,newCapacity可能会变成一个负数。那么newCapacity - MAX_ARRAY_SIZE > 0 成立(一个比最小负数还小的数会变成正数,这也是溢出的一种),那么执行hugeCapacity(minCapacity)进行溢出处理。如果将代码改成
if (newCapacity > MAX_ARRAY_SIZE )那么对溢出情况就会遗漏。

另外,容量的扩展会执行整个数组内容的复制( Arrays.copyOf(elementData, newCapacity);),如果能够大概预知list的容量在创建的时候指定初始容量,或者调用ensureCapacity(ensureCapacity),减少扩容次数,能够有效提高效率。
注意上面的方法是私有的,jdk还提供了一个共有的扩容方法ensureCapacity(ensureCapacity)

公有扩容方法:ensureCapacity(int minCapacity)

  public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            ? 0 : DEFAULT_CAPACITY;

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

ensureExplicitCapacity方法:

在如何扩容 ensureCapacityInternal( int minCapacity )
中讲过

在指定位置插入元素add( int index, E element )

 public void add(int index, E element) {
        //检查索引范围
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    //检查索引范围,在0到size之间
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
              throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

首先判断索引index是否合法,然后调用ensureCapacityInternal调整容量(容量不够会进行扩容),之后调用System.arraycopy进行数组复制,将elementData从index位后面的数据复制到index+1位置上,也就是index之后的数据统一向后挪一位,然后将element复制到elementData[index]位置上。
这里说明一下

System.arraycopy(Object src,int srcPos,Object dest,int destPos,int length)

arraycopy可以执行数组的复制,它是一个本地方法。src为源数组,dest为目的数组,srcPos为源数组要复制的起始位置,destPos为目的数组放置的目标位置,length为要复制的长度。src和dest为同一个数组时,可以复制自身数组。当然前提时保证目的数组的长度足够,ArrayList先调整了容量,不会出现数组溢出的情况( java.lang.ArrayIndexOutOfBoundsException)。

将集合c添加到list的尾部 addAll( Collection< ? extends E > c )

/**按照c的迭代器所返回的元素顺序,将元素添加到list的尾部
@param c collection containing elements to be added to this list
@return <tt>true</tt> if this list changed as a result of the call
@throws NullPointerException if the specified collection is null
**/
 public boolean addAll(Collection<? extends E> c) {
 //调用c.toArray()将从转换为对象数组
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

先将c转换为对象数组a,然后调整容量,调用system.arraycopy将a的内容复制到elementData尾部。有一个注意的点,它的返回值是看list是否被改变;如果c为null,将报空指针异常。
使用场景:
如果数据量很大,那么推荐使用addall。原因如下:
system.arraycopy在内存中进行复制,速度快。
system.arraycopy是本地方法,执行效率高。
如果数据量比较小,几十个左右。可以用for循环来添加。

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

public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

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

执行逻辑跟addAll类似,不过增加了一步

int numMoved = size - index;
if (numMoved > 0)
      System.arraycopy(elementData, index, elementData, index + numNew,numMoved);
//将数组缓存中index后面的数据向后挪numNew位(numNew为c的长度),
//为下一步将c中的数据插入到elementData中让出位置,否则会被覆盖掉。

删除指定位置的元素并返回该元素remove( int index )

/**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their indices).所有后续元素左移,索引减1
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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; // 先执行size--,在执行elementData[size] = null。末尾元素置空,等待垃圾回收

        return oldValue;
    }

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

首先检查索引范围,修改次数加1。保留要被移除的元素,将index后面的数据左移一位,size减1,将末尾元素置空,返回被移除的元素。

移除第一次出现的指定对象remove( Object o )

 /**
     * Removes the first occurrence of the specified element from this list,if it is present.  
     * If the list does not contain the element, it is unchanged.  More formally, removes the element with the lowest index.
     * @param o element to be removed from this list, if present
     * @return true if this list contained the specified element
     */

    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
    }

首先判断传入对象o是否为null,如果是null,for循环遍历缓存数组,如果找到某个元素为null,则删除该元素,size-1并返回true。如果o不为null,for循环遍历缓存数组,通过equals判断如果有某个元素等于指定对象o,则删除该对象size-1并返回true。如果找不到该对象,返回false。
这里通过对象的equals方法判断元素是否为某个对象。

删除所有在指定的集合c中包含的元素 removeAll( Collection< ? > c )

/**
     * Removes from this list all of its elements that are contained in the specified collection.
     * @param c collection containing elements to be removed from this list
     * @return {@code true} if this list changed as a result of the call
     * @throws ClassCastException if the class of an element of this list is incompatible with the specified collection
     * @throws NullPointerException if this list contains a null element and the specified collection does not permit 
     * null elements or if the specified collection is null
     */

public boolean removeAll(Collection<?> c) {
    if (c == null)
        throw new NullPointerException();
    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 {
        //r先右移
            for (; r < size; r++)
                // 如果c中不包含elementData[r]这个元素  
                if (c.contains(elementData[r]) == complement)       
                    elementData[w++] = elementData[r];// 则直接将r位置的元素赋值给w位置的元素,w自增。保留不在c中的元素
        } finally {
            if (r != size) {//c.contains(elementData[r])出现了异常,for循环没有执行到最后
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);// // 将r未右移完成的位置的元素赋值给w右边位置的元素 
                w += size - r; // 修改w值增加size-r 
            }
            if (w != size) { // w==size表示所有元素都保留了下来,说明没有删除任何元素。w!=size则有部分元素被删除,
                            //将w后面的元素置空,modified=true。说明删除成功,列表被改变了。w前面的元素就是保留下来的元素。
                // 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;
    }

遍历elementData,如果c不包含elementData的元素e,那么elementData[w] = e;将c中不包含的数据从头到尾保留下来,也就变相删除了在c中包含的数据。finally处理c.contains(elementData[r]出现异常的情况,因为有些集合不允许null值,c.contains()可能会抛出异常。finally保证了抛出异常之前的操作。

慎用 removeAll( Collection< ? > c )

  1. 内部使用双层for循环,c.contains()方法经常也是一个循环遍历。效率低下。
    例如现有主ArrayList a:2万条数据,子ArrayList b : 1万条数据。那么调用a.removeAll(b)将循环2万*1万次。效率及其地下。
  2. finally块中对异常情况进行了特殊处理,保证了抛出异常之前的操作。但我们一般的主观印象是一旦出现异常,一切应该回到原点(类似于事务)
    调用a.removeAll(b),如果出现异常,我们主观上可能会认为a会保持不变。但实际上在抛出异常之前的操作会被保留,这可能会导致不可预知的错误。

举例说明:现有ArrayList< Integer > a={1,2,3,4,5,6,7,8,9,null,10,11,12,13},
TreeSet b = {1,2,3,12,13}。 TreeSet调用contains(null),会抛出异常。
那么执行
a.removeAll(b)。在循环到a[9],也就是null的时候,抛出异常。但在抛出异常之前,已经删除了1,2,3三个元素,这个操作会被保留下来。最终结果是抛出了异常,但a已经被改变了。

删除指定索引范围内的数据  removeRange( int fromIndex, int toIndex )

   /**
     * Removes from this list all of the elements whose index is between {@code fromIndex}, inclusive,
     *  and {@code toIndex}, exclusive.
     * Shifts any succeeding elements to the left (reduces their index).所有后续元素左移,索引减1
     * This call shortens the list by {@code (toIndex - fromIndex)} elements.
     * (If {@code toIndex==fromIndex}, this operation has no effect.)
     *
     * @throws IndexOutOfBoundsException if {@code fromIndex} or
     *         {@code toIndex} is out of range
     *         ({@code fromIndex < 0 ||
     *          fromIndex >= size() ||
     *          toIndex > size() ||
     *          toIndex < fromIndex})
     */

    protected void removeRange(int fromIndex, int toIndex){
        modCount++;
        int numMoved = size - toIndex;
        System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved);

        // clear to let GC do its work
        int newSize = size - (toIndex-fromIndex);
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
        size = newSize;
    }

注意这是一个protected的方法,并不是public。如果要达到相同的效果,可以调用subList(fromIndex,toIndex).clear(),效果是一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值