java集合-ArrayList

81 阅读9分钟

介绍

ArrayList底层是通过一个维护一个数组,并通过扩容方法来实现的容量动态更改。下面将分别介绍一下其中有哪些属性属性和方法。

size(), isEmpty(), get(), set()方法均能在常数时间内完成,add()方法的时间开销跟插入位置有关,addAll()方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。

ArrayList是线程不安全的

属性介绍

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

DEFAULT_CAPACITY

默认的初始化容量,在不指定初始化容量时,会以此容量进行初始化。

EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA

用于执行构造函数时的共享示例。

两者区别:

EMPTY_ELEMENTDATA用于构造函数中,指定初始化容量为0时使用。

在后续扩容时判断如果是EMPTY_ELEMENTDATA会从0开始初始化,而如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA会从DEFAULT_CAPACITY开始初始化。

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
    
        //如果指定是0初始化,会使用EMPTY_ELEMENTDATA作为数组示例
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

这里主要影响的就会是后续扩容的起始,举例查看,初始化不指定和指定0时,逐个添加元素,内部数组容量的变化如下

List<Integer> list = new ArrayList<>(0);
//通过反射获取list元素数组elementData长度
Field elementData = list.getClass().getDeclaredField("elementData");
elementData.setAccessible(true);
Object[] o = (Object[]) elementData.get(list);
System.out.println(Arrays.toString(o));
list.add(1);
o = (Object[]) elementData.get(list);
System.out.println(Arrays.toString(o));
list.add(2);
o = (Object[]) elementData.get(list);
System.out.println(Arrays.toString(o));
list.add(3);
o = (Object[]) elementData.get(list);
System.out.println(Arrays.toString(o));
list.add(4);
o = (Object[]) elementData.get(list);
System.out.println(Arrays.toString(o));
list.add(5);
o = (Object[]) elementData.get(list);
System.out.println(Arrays.toString(o));

//输出为
[]
[1]
[1, 2]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3, 4, 5, null]


//而如果不指定初始容量的话,这里的输出会是
[]
[1, null, null, null, null, null, null, null, null, null]
[1, 2, null, null, null, null, null, null, null, null]
[1, 2, 3, null, null, null, null, null, null, null]
[1, 2, 3, 4, null, null, null, null, null, null]
[1, 2, 3, 4, 5, null, null, null, null, null]

elementData将

存放元素的数组缓冲区,ArrayList的容量就是该数组的长度

size

存放元素的数量

方法

ArrayList使用的是惰性加载机制,在第一次add元素时,会判断容量是否满足需求

ArrayList(Collection<? extends E> c)

这里构造方法除了前面介绍的无参和带容量的,还有一个是传入一个集合作为参数

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

这里需要记住的主要是如果传入是一个空集合,相当于传入一个参数0的初始化容量。如果传入的就是ArrayList这里也不会再去创建复制元素,而是会直接将引用指向c的elementData,也就是复制出来的a

add()

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

看第一个方法,这里有ensureCapacityInternal(size + 1);

可以猜出来,这里是判断目前的容量是否足够添加当前这个元素

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

首先会进行一次容量计算,可以看到这个calculateCapacity方法主要有两个目的:

  • 如果目前元素数组还未初始化,并且构造函数是无参构造函数,那么会将容量设置为DEFAULT_CAPACITY和size+1中较大的那一个。
  • 如果已经初始化,返回size+1。

计算完需要的容量,会执行这里的ensureExplicitCapacity方法,主要是确保容量足够,否则扩容

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

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

首先会modCount++代表数组经过了一次变化,之后判断目前需求的容量是否超过了目前最大容量,如果是,会进行下一步,进入真正的数组扩容

查看grow方法

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

解释一下这个方法:

首先通过oldCapacity记录旧数组容量,然后通过1.5倍扩容计算新数组容量。 这里的计算是通过位运算进行的

判断扩展后容量和需求容量之间的关系,如果1.5倍之后,依然小于需求容量,就将新容量设置为需求容量

判断新容量是否超过最大长度限制,如果超过最大长度限制,就执行hugeCapacity方法。如果请求的最小容量小于 Integer.MAX_VALUE 但大于 MAX_ARRAY_SIZE,它将尝试返回 Integer.MAX_VALUE,因为这是最后一次尝试在不溢出的情况下分配尽可能大的数组。如果你尝试添加更多元素,将会抛出 OutOfMemoryError

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

在计算完成最终扩展的容量之后,会通过Arrays.copyOf()方法完成数据的实际扩展和拷贝

最终的方法执行是在Arrays类中的一个重载方法

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

这里会先进行判断待创建数组是否是Object类型,如果不是,会通过反射创建出对应类型的扩容数组copy[],否则直接创建Object[]

//验证(Object)newType == (Object)Object[].class
Object[] objects = new Object[10];
String[] strings = new String[10];
System.out.println((Object) strings.getClass() == (Object) Object[].class);//false
System.out.println((Object) objects.getClass() == (Object) Object[].class);//true

最后会通过一个本地方法进行数据拷贝,传参基本就是源数组,拷贝起始位置,目的数组,起始位置,拷贝长度

经过这一系列的操作之后。完成扩容或者容量验证,之后就是向size++位置添加元素。

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

这个add方法带有index,所以在实际执行时,会先使用rangeCheckForAdd进行一次index检查,主要是确保index不超过有效范围

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

之后步骤相同,只是在赋值时,会显示用System.arraycopy将index处位置腾出来。之后进行添加

addAll(Collection<? extends E> c)

这个方法适用于批量添加,本质思路大致相同,首先会调用toArray()方法,转换为实际数组类型。之后进行容量担保确认,进行扩容等操作。最后依然是通过arraycopy方法进行的元素添加

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();//Arrays.copyOf()
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

remove(int index)

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越界检查,记录modCount

之后计算出index之后的元素数量,方便使用arraycopy方法将这些元素左移。最终返回删除的元素值

另外一个remove(Object o)方法,作用是删除列表中出现的第一个o

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

removeAll()和retainAll()

这里的方法传参是一个集合,作用分别是从此列表中删除集合中的所有元素和仅保留集合中的元素

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

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

可以看到最终实现是batchRemove方法中执行的

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++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        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;
}

这里方法巧妙指出是通过complement来进行了保留或删除的操作

如果是true,会在try的循环当中,将所有c中包含的元素,依次添加到elementData中,

如果是false,则会在循环中,依次将所有c不包含的元素,添加到elementData中,完成删除或保留的操作。

最后会在finally中,首先根据r的状态,判断前面的遍历过程是否出现了异常。如果出现异常,会从异常出现的位置将后续元素复制到w位置,也就是已遍历修改后的元素位置。

之后会根据w的长度,判断整体是否进行了修改,如果确实进行了修改,也就是会导致w长度少于size,这里就会讲w之后的元素置为null,记录修改次数。完成修改

removeIf(Predicate<? super E> filter)

removeIf方法中,接受一个Function传参,用于判断元素是否可删除。下面是代码

public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    // figure out which elements are to be removed
    // any exception thrown from the filter predicate at this stage
    // will leave the collection unmodified
    int removeCount = 0;
    final BitSet removeSet = new BitSet(size);
    final int expectedModCount = modCount;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        @SuppressWarnings("unchecked")
        final E element = (E) elementData[i];
        if (filter.test(element)) {
            removeSet.set(i);
            removeCount++;
        }
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }

    // shift surviving elements left over the spaces left by removed elements
    final boolean anyToRemove = removeCount > 0;
    if (anyToRemove) {
        final int newSize = size - removeCount;
        for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
            i = removeSet.nextClearBit(i);
            elementData[j] = elementData[i];
        }
        for (int k=newSize; k < size; k++) {
            elementData[k] = null;  // Let gc do its work
        }
        this.size = newSize;
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

    return anyToRemove;
}

这里就用到了fail-fast策略,通过判断modCount实现

首先在方法开始时,会加载当前的modCount,过程中会判断目前modCount是否发生了变化,如果有变化,就会抛出异常throw new ConcurrentModificationException();

方法执行总体流程为

  • 预先定义一个长度为size的BitSet用于表示该位置元素是否需要删除
  • 遍历所有元素判断是否满足filter条件
  • 从头遍历所有的保留元素,也就是removeSet.nextClearBit(i),将其逐个按照顺序依次放入elementData中
  • 将剩余元素位置变为null

set(int index, E element)

set方法比较简单,基本就是index检查,元素替换

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

另一个修改元素的函数为replaceAll(UnaryOperator operator)

    public void replaceAll(UnaryOperator<E> operator) {
    Objects.requireNonNull(operator);
    final int expectedModCount = modCount;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        elementData[i] = operator.apply((E) elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

get(int index)

get方法较为简单,基本就是直接返回数据

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

    return elementData(index);
}

其它方法

将实际数组容量缩减为实际size

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

还有一些内部的迭代器ListIteratorIterator

其它的内部类SubListSpliterator