ArrayList源码解析(一):初始化,扩容以及增删改查

956 阅读9分钟

前言

  ArrayList是我们特别常用的一种集合类,但是它提供的一些函数和功能需要对源码有一定的了解才能正确使用。由于ArrayList源码的内容比较多,本文先只介绍ArrayList的初始化,扩容以及基本的增删改查(不包括迭代器的增删改查),其它部分诸如迭代器会在之后介绍。

主要属性

// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 用来真正存储数据的数组
transient Object[] elementData; // non-private to simplify nested class access
// ArrayList对象内元素的实际个数
private int size;

  其中,elementData是真正存储数据的数组,是一个Object类型的数组;EMPTY_ELEMENTDATA 是以指定容量为0或者以空集合初始化时,elemantData 指向的空数组,否则,以无参来初始化,会令 elemantData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA;size是ArrayList对象包含的实际元素个数。

初始化

// 给定容量
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() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 用集合初始化
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;
    }
}

  可见,一共有三种初始化方式,前两种非常简单,就不说了,主要说下第三种
用集合初始化:先通过c.toArray( )得到了集合c对应的数据数组。如果集合c也是ArrayList,直接将c.toArray( )的结果赋给elementData。先看下ArrayList里的 toArray( )方法。

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}
// 位于Java/util/Arrays.java里
public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}
// 位于Java/util/Arrays.java里
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

  最终是调用了Arrays.copyOf的这个三参数版本的函数。这个三参数的copyOf函数比较复杂,作用就是返回一个指定的newType类型的数组,这个数组的长度是newLength,值从original拷贝而来。拷贝的功能由System.arraycopy( )完成:如果newLength大于原数组的长度,多出来的元素初始化为null;如果小于原数组长度,将会进行截断操作。在这里,两参版本调用三参版本的三个参数为original, newLength, original.getClass(),故得到的数组元素类型和原数组类型一致,长度为newLength,数据由原数组复制而来。

  总之,ArrayList的无参版toArray( )返回了一个和elemantData一模一样的拷贝数组。所以判断c也是ArrayList对象时,直接令elemantData 为c.toArray( )了。

  否则,会执行elementData = Arrays.copyOf(a, size, Object[].class);,经过上面的分析,三参的copyOf( )是返回一个数据内容和a一模一样的数组,但是数组类型转为Object[ ]类型。之所以有这条语句,猜想可能是某些集合的toArray( )方法,返回的数组不是Object[ ]类型,比方说用一个类继承ArrayList,并且重写toArray( )方法,让它返回一些别的类型。

扩容

public void ensureCapacity(int minCapacity) {
    if (minCapacity > elementData.length
        && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
             && minCapacity <= DEFAULT_CAPACITY)) {
        modCount++;
        grow(minCapacity);
    }
}
// 真正的扩容函数
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    // 计算扩容后的新容量
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

private Object[] grow() {
    return grow(size + 1);
}

  先看 grow(int minCapacity)函数,这是真正进行扩容的函数。先看else分支。else分支也就是oldCapacity <= 0 && elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,也就是无参构造的ArrayList第一次扩容,那么,直接调用return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];进行初始化。

  接着看if分支,如果不是上面那种情况,都会到这个分支来。首先调用ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1);计算了新容量newCapacity。

// 计算新容量
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
    if (newLength - MAX_ARRAY_LENGTH <= 0) {
        return newLength;
    }
    return hugeLength(oldLength, minGrowth);
}  

public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;

private static int hugeLength(int oldLength, int minGrowth) {
    int minLength = oldLength + minGrowth;
    if (minLength < 0) { // overflow
        throw new OutOfMemoryError("Required array length too large");
    }
    if (minLength <= MAX_ARRAY_LENGTH) {
        return MAX_ARRAY_LENGTH;
    }
    return Integer.MAX_VALUE;
}

  这里的minGrowth是minCapacity - oldCapacity,也就是满足我们需要的容量;prefGrowth是oldCapacity >> 1,就是原来容量的一半(可能Java的设计者认为每次扩一半是prefer的选择),在没有溢出时,扩容时扩的容量就是这俩里大的那个;如果溢出了,就不会考虑prefGrowth了,调用hugeLength( )来优先满足minGrowth,当然,如果还溢出,最多也只能给Integer.MAX_VALUE这么多容量。

  计算好了长度之后,调用return elementData = Arrays.copyOf(elementData, newCapacity);,这个两参的Arrays.copyOf上文说过,得到一个长度改变,元素类型和原数组相同的新数组。可以看出,整个扩容阶段最重要的就是确定扩容之后的数组长度。

  再看ensureCapacity(int minCapacity)函数,代码比较简单,它是public方法,我们也可以调用,可以用来手动扩容。

增加元素

增加一个元素

// 尾部增加一个元素
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

  先是最常用的add( )方法。咱们调用的是下面的public方法,首先自增modCount,接着调用上面的private版add( )方法。不难看出,调用时传入了size(size和elemantData.length是不同的)。比方说size是3,由于数组从0开始计数,elementData[3] = e;正好可以向数组尾增加了一个元素。当然,首先要判断size和elemantData.length的关系,如果相等了,说明elementData已经满了,需要扩容,于是调用了grow( )方法进行扩容。

private Object[] grow() {
    return grow(size + 1);
}

  grow方法实际上调用了grow(size + 1),这个带参的grow( )函数上文已经讲过。这里也可以看到上文提到的ensureCapacity(int minCapacity)的函数的好处:我们每次调用add( )都是有几率触发扩容的,特别是ArrayList容量比较小的时候。调用很多次add( ),可能会进行很多次扩容。如果我们预估了ArrayList的最终容量,可以提前手动调用ensureCapacity(int minCapacity),省的之后很多次的自动扩容了。

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 特定位置增加元素
public void add(int index, E element) {
// 检查index是否合法
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow();
    // 把index位置后的元素向后挪
    System.arraycopy(elementData, index,
                     elementData, index + 1,
                     s - index);
    elementData[index] = element;
    size = s + 1;
}

  如上代码,还可以在特定位置增加元素。首先会调用 rangeCheckForAdd 去判断index是否合法。若是,自增modCount,再检查是否需要扩容。

  接下来再次看一下System.arraycopy( )函数。这里的调用语句为System.arraycopy(elementData, index, elementData, index + 1, s - index);,在这里,原数组和目的数组是一个数组,我们想知道这样的复制会带来什么效果。可惜这个函数是native方法,看不到源码,不过,在此函数的声明处有这么一段注释:
If the {@code src} and {@code dest} arguments refer to the same array object, then the copying is performed as if the components at positions {@code srcPos} through {@code srcPos+length-1} were first copied to a temporary array with {@code length} components and then the contents of the temporary array were copied into positions {@code destPos} through {@code destPos+length-1} of the destination array,就是说,如果原数组和目的数组是一个数组的话,这个语句作用就好比先把此数组索引从srcPossrcPos+length-1的元素都先复制到一个中间表里,接着把中间表的这些内容复制到此表从destPos索引开始直到destPost+length-1结束的位置。在这条语句里,srcPosdestPos分别为index和index+1,相当于把index开始的s-index个元素向后挪动一位。

  经过上面分析,先把index位置后的元素向后挪动一位,接着再赋值index位置上的元素。由此见得,在特定位置上增加元素的时间复杂度为O(n)。

增加一堆元素

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    modCount++;
    int numNew = a.length;
    if (numNew == 0)
        return false;
    Object[] elementData;
    final int s;
    // elemantData剩余容量不足,扩容
    if (numNew > (elementData = this.elementData).length - (s = size))
        elementData = grow(s + numNew);
    // 从数组尾部增加元素
    System.arraycopy(a, 0, elementData, s, numNew);
    size = s + numNew;
    return true;
}

  代码也比较类似,自增modCount,将c中元素在尾部依次添加即可。此函数还有一个从指定index位置的版本。

public boolean addAll(int index, Collection<? extends E> c) {
// 检查index是否合法
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    modCount++;
    // 要加进来的元素个数
    int numNew = a.length;
    if (numNew == 0)
        return false;
    Object[] elementData;
    final int s;
    if (numNew > (elementData = this.elementData).length - (s = size))
        elementData = grow(s + numNew);
    // 计算多少个元素需要往后挪
    int numMoved = s - index;
    if (numMoved > 0)
    // 要在index位置,新增numNew个元素,所以从index位置开始,往后挪numNew位,一共有numMoved个元素需要挪动
        System.arraycopy(elementData, index,
                         elementData, index + numNew,
                         numMoved);
    // 空出来的位置承载那些要添加的元素
    System.arraycopy(a, 0, elementData, index, numNew);
    size = s + numNew;
    return true;
}

  还有从特定位置添加集合里的元素,主要就是向后挪动相关的几个量的计算,注释里已经写了。

删除元素

删除一个元素

// 按下标删
public E remove(int index) {
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index);

    return oldValue;
}  

private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    // 判断是否需要挪动元素
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    // 挪动之后剩的那个位置赋为null
    es[size = newSize] = null;
}

  先是按下标删,还是先检查index是否合法,接着取到oldValue值也就是要删的元素值,然后调用fastRemove( )函数。在fastRemove里,首先自增modCount,再判断要删的元素是不是elemantData的第size个元素(也就是实际上的最后一个元素),如果是,不需要挪动元素操作,直接赋值为null即可,否则,还需要将删除位置之后的元素都往前挪一位。可见,在指定位置删除元素复杂度也为O(n)。

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    // 正序找对应元素下标
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        // 没找到,没法删
        return false;
    }
    // 找到了,调用fastRemove,进行删除
    fastRemove(es, i);
    return true;
}

  还有个删除指定元素的方法,主要就是先找到下标,接着按下标删。

删除一堆元素

// 删除集合c里的所有元素
public boolean removeAll(Collection<?> c) {
    return batchRemove(c, false, 0, size);
}
// 保留集合c里的所有元素
public boolean retainAll(Collection<?> c) {
    return batchRemove(c, true, 0, size);
}
// 真正的批量删除函数
boolean batchRemove(Collection<?> c, boolean complement,
                    final int from, final int end) {
    // c不能是空集合
    Objects.requireNonNull(c);
    final Object[] es = elementData;
    int r;
    // 找到删除的开始位置r
    for (r = from;; r++) {
        if (r == end)
            return false;
        if (c.contains(es[r]) != complement)
            break;
    }
    // w用于写入保留的元素
    int w = r++;
    try {
        for (Object e; r < end; r++)
        // 这个元素可以保留,把它赋值给es[w]
            if (c.contains(e = es[r]) == complement)
                es[w++] = e;
    } catch (Throwable ex) {
        System.arraycopy(es, r, es, w, end - r);
        w += end - r;
        throw ex;
    } finally {
        // 善后工作
        modCount += end - w;
        shiftTailOverGap(es, w, end);
    }
    return true;
}

  removeAll是删除集合c里存在的元素,而retainAll是删除c里不存在的元素。不管是removeAll 还是retainAll都是调用batchRemove函数。看一下batchRemove函数。

  代码首先声明了变量'r',接着是一段for循环。由于removeAll和retainAll调用的from 和end都是0和size,所以这段代码是从头到尾用r作为索引遍历数组。遇到if (c.contains(es[r]) != complement)为真则结束循环。removeAll时,complement传的是false,此判断为真,也就是c.contains(es[r]为true,也就是c中有es[r]这个元素,这是我们想要删的元素,那么r是将要删除的第一个元素的索引;同理,retainAll时,complement传的是true,此判断为真,也就是c.contains(es[r]为false,r还是想要删的第一个元素的索引。

  总之,删除从索引r开始。

  if (r == end)成立,也就是遍历完了数组,都没有出现要删的元素。那没什么可删的,返回false即可。

  找到了这个位置r,运行int w = r++;,这里是r++,语义是int w = r;r++;,此时w是第一个要删除的位置。之后索引r从w+1开始循环遍历数组,当判断条件if (c.contains(e = es[r]) == complement)时,执行es[w++] = e;

  首先看判断条件:上面分析过,if (c.contains(es[r]) != complement)为真,代表的是r是要删除的元素索引,那么if (c.contains(e = es[r]) == complement)为真,则是要保留的元素,那么运行es[w++] = e;,一开始,w是第一个要删的元素索引,找到要保留的元素,则把索引w的元素赋值为此元素,再自增w。这样子r一遍遍历完成后,要保留的元素也都向前移动好了。接下来做一些善后工作就可以。不发生异常的情况下,增加modCount的值,并调用shiftTailOverGap(es, w, end);进行善后工作。下面看这个函数:

private void shiftTailOverGap(Object[] es, int lo, int hi) {
    // 从索引hi开始,有size-hi个元素需要往左挪,这些元素依次挪到lo以及lo之后的位置,
    // 它们都向左挪了hi-lo个单位
    System.arraycopy(es, hi, es, lo, size - hi);
    // 挪动之后,原先的索引size-1的元素,对应的是size-1-(hi-lo),这个索引之后的元素都赋值为null
    for (int to = size, i = (size -= hi - lo); i < to; i++)
        es[i] = null;
}

  这个函数的作用其实就是把索引hi开始到索引size-1的元素全部往左挪hi-lo个单位,再把剩下的元素都赋为null,主要就是一些挪动的相关量的计算,在上面的代码注释里已经写了。此函数官方的注释为Erases the gap from lo to hi, by sliding down following elements,可以说很形象了。不过这里调用shiftTailOverGap里的参数hi就是end,而end目前一直也没有改,还是size,所以System.arraycopy(es, hi, es, lo, size - hi);语句也不会运行,调用shiftTailOverGap的目的也就是重新设置size(在for循环的初始化语句里),并且把剩下的元素赋为null。

  上面说的是正常情况,不过这个batchRemove还有一个try-catch语句块。有些资料说c.contains(e = es[r]可能会抛出异常,这种情况下,在catch里,直接调用了System.arraycopy(es, r, es, w, end - r);,并抛出异常。然后在finally进行一些善后操作。这种情况下,我判断得到的结果是不准确的,因为r还没有遍历完,直接就把后面的元素全部挪到左边了,少了那部分元素的判断,所以可能并没有删完。

  批量删除还有一个函数:

protected void removeRange(int fromIndex, int toIndex) {
    if (fromIndex > toIndex) {
        throw new IndexOutOfBoundsException(
                outOfBoundsMsg(fromIndex, toIndex));
    }
    modCount++;
    shiftTailOverGap(elementData, fromIndex, toIndex);
}

  按集合删除的代码是有难度的,看完了那个,这个也没什么可说的了,直接调用shiftTailOverGap 把toIndex和fromIndex之间的元素‘erase’了。

修改元素

public E set(int index, E element) {
    Objects.checkIndex(index, size);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}  
// 判断index是否合法
public static
int checkIndex(int index, int length) {
    return Preconditions.checkIndex(index, length, null);
}  

public static <X extends RuntimeException>
int checkIndex(int index, int length,
               BiFunction<String, List<Integer>, X> oobef) {
    if (index < 0 || index >= length)
        throw outOfBoundsCheckIndex(oobef, index, length);
    return index;
}

  修改元素的代码相当简单,就是判断下index是否合法,这个判断index合法的代码我也粘贴了一下,也比较常规。合法的话,对这个元素的值进行改变,并返回元素的旧值。

查询元素

public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index);
}

  有一个最基本的按index查询的函数,先判断index合法性,再按索引从数组elementData里取元素。

public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }
    return -1;
}

  还有个indexOf函数,就是根据元素找索引。从左往右找,找到第一个索引就返回;当然,还有个lastIndexOf,从右往左找。

其它函数

  迭代器代码之前,还有些其它函数,譬如equals,hashCode等等,这些函数都很简单,不需要一个个介绍了。

总结

  本文对ArrayList的迭代器之前得源码进行了解释,剖析了初始化,扩容以及增删改查的代码,下一步要看看迭代器的代码。

PS

  直到这篇博客我才知道代码高亮格式是怎么用的/(ㄒoㄒ)/~~  选定了主题和高亮之后,在```后面加js就行了,之前博客里的代码样式可以说是很丑了,可读性极低/(ㄒoㄒ)/~~

image.png

  用了代码高亮之后,代码不要好看太多。

image.png