java集合--ArrayList

写在前面

最近在学习java的底层代码,写博客是为了记录自己在学习过程中的一些笔记。这是我的第一篇博客,完成“写一篇博客”成就~~~

ArrayList

我们直接来看ArrayList的底层源码。其底层是使用Object数组实现的。会使用size来记录其容量。

transient Object[] elementData; 
private int size;

由于其底层是使用数组实现的,因此插入和删除时,其时间复杂度受元素所在位置影响。

modCount是在使用迭代器遍历时的一个重要参数。如果modCount发生改变,那么迭代就有可能出现异常。

基本方法

首先我们先看基本的get,set,remove方法。

首先是add方法。我们看到,在调用add(int index, E element)方法时,首先我们会去检查是否越界,然后我们会将数组从index位置开始全部向后移动一位,然后将新的元素插入到index的位置。由于这里对index之后的元素都进行了移动, 因此我们说ArrayList的插入操作的时间复杂度为O(n)。

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}


/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
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操作时,都会调用ensureCapacityInternal方法。这个方法主要是进行扩容操作的。当ArrayList的数据存储的元素达到最大容量,这时我们想要继续向其中插入元素时,就需要对数据进行扩容。我们看一下这个方法的实现:

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

首先调用的时calculateCapacity计算出所需最小容量:

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

如果是默认设置的话,需要判断默认的容量和最小容量中大的数,否则就直接返回传入参数。
计算出最小容量后,我们就要调用ensureExplicitCapacity方法。

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

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

可以看到这里对modCount进行了操作。至于操作有什么用处,后续讨论迭代器时我们再进行讨论。这里我们来判断数组的长度和最小容量比较,如果最小容量比数组长度大的话,我们就需要最底层数据进行扩容了。

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
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);
}

这里我们看到每次扩容都是增加原来容量的一半,如果最后最小的容量大于MAX_ARRAY_SIZE,就将容量设定为Integer.MAX_VALUE。

那么既然有了扩容的机制,有没有减少容量的方法呢?在阅读ArrayList源码过程中,我们发现有trimToSize这样一个方法。由于ArrayList的底层是使用数组实现的,因此数组会预留一定的空间插入新的元素。trimToSize就是将现在的容量减到现在数组中存储的数据个数,释放多余的存储空间。

/**
 * Trims the capacity of this <tt>ArrayList</tt> instance to be the
 * list's current size.  An application can use this operation to minimize
 * the storage of an <tt>ArrayList</tt> instance.
 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

然后是set方法和remove方法。可以看到两个方法在调用前都会去检查是否越界。在调用remove时,我们发现这里也是将index之后的元素全部向前移动了一位。因此,ArrayList的删除操作的时间复杂度也是O(n)。

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


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

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

如果我们需要查询某个元素是否在ArrayList中,就需要调用contains方法。我们来看一下contains的实现。这里看到,在调用indexOf方法查询list中是否存在元素时,需要从头到尾逐一遍历。因此查找的时间复杂度为O(n)。

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    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;
}
ArrayList中的Iterator

我们在看ArrayList中的add和remove方法时,我们看到,这里每次都会对modCount进行++操作。那么这个modCount是干什么用的呢?另外,我们在对list进行遍历时,经常会有人提醒说不要在遍历的时候进行remove操作。若要在遍历时进行remove操作,一定要使用iterator中的remove方法进行操作,即Iterator.remove()。

我们通过一段实例代码来看一下删除的情况:

    public static void main(String[] args) {
        int n = 50;
        List<Integer> ints = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            ints.add(i);
        }
//         不会报错,但是删除的index不对
        for (int i = 0; i < ints.size(); i++) {
            if (30 < i && i < 40) {
                ints.remove(i);
            }
        }
        System.out.println(ints);  // // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 49]

        // 会报错
        for (Integer anInt : ints) {
            if (30 < anInt && anInt < 40) {
                ints.remove(anInt);
            }
        }
        System.out.println(ints);

        // Exception in thread "main" java.util.ConcurrentModificationException
        //	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
        //	at java.util.ArrayList$Itr.next(ArrayList.java:859)
        //	at com.fyk.collection.list.testListDelete.main(testListDelete.java:23)

        ints.removeIf(anInt -> 30 < anInt && anInt < 40);
        System.out.println(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
    }

使用第一种fori的方式删除,虽然没有抛出异常,但是删除的元素并不是我们想要删除的30-40之间的数,结果不对。

使用第二种方式删除,直接就会抛出ConcurrentModificationException。我们知道,第二种方式实际上使用的就是Iterator进行遍历的,但是我们没有使用Iterator本身的remove方法,而是使用了ArrayList的remove方法,导致modCount发生了变化,抛出了异常。稍后会做解释。

而第三种调用的是Collection中的removeIf方法,我们看一下源码:

    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

可以看到,其本身也是使用Iterator进行遍历,并使用iterator中的remove操作的,这里没有抛出异常,并且结果也是正确的。

那么为什么使用iterator的remove操作不会有这样的问题呢?我们从源码入手来进行分析。首先我们来看ArrayList中的Itr是怎么实现的。

int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

我们看Itr中的三个属性,cursor是当前遍历的元素的位置,lastRet是上一次遍历的元素的位置。而这里又初始化了一个叫expectedModCount的属性,其初始值为modCount。那么这个属性有什么作用的?我们继续看Itr中的hasNext和next方法。

hasNex方法比较简单,就是判断一下当前的元素位置是否已经达到list的size。

public boolean hasNext() {
    return cursor != size;
}

再来看一下next方法。这里首先会调用checkForComodification方法,然后将cursor加1,lastRet赋值为当前的cursor,然后返回数据。这里看到其中的位置指向已经发生了变化。

@SuppressWarnings("unchecked")
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

我们看一下checkForComodification方法,可以看到,这里判断了modCount和expectedModCount是否相等。如果不相等则会抛出异常。因此,如果我们在Iterator迭代的过程中,如果modCount发生了变化,即对数组进行了add,remove操作,这里迭代就会抛出异常。因此,使用Iterator进行遍历可以保证在遍历的过程中,数组的大小不会发生变化。

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

我们再来看一下iterator中的remove操作,可以看到,我们在进行remove操作时,会将expectedModCount进行更新,另外会将cursor置为lastRet,将lastRet置为-1,这样就保证了在迭代的过程中,迭代顺序的正确性以及不会抛出ConcurrentModificationException这样的异常。这也是为什么要在遍历时使用Iterator来进行remove操作。同理,使用ListIterator进行遍历,可以在遍历的过程中进行add和set操作。原理与remove一样,因此不做赘述。

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();


    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

ArrayList的其他源码就比较简单了,后面不再进行讨论。
总结一下:

  • ArrayList的底层是使用数组实现的,因此随机访问快,时间复杂度为O(1);插入和删除慢,时间复杂度为O(n);
  • ArrayList会自动进行扩容操作,每次扩容大小为当前容量的一半;
  • modCount是保证在使用Iterator遍历时,可以对数组进行remove,add操作而不会出现异常。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值