Java 集合——ArrayList

Java 集合 —— ArrayList

介绍

  ArrayList 是 List 的动态数组实现,它的底层结构是一个数组,它与一般的数组的区别,则是它可以动态扩容,当 list 中添加的元素个数大于数组的容量,则会触发扩容,扩容之后的新数组为原数组的 1.5 倍,同时也会将旧元素复制到新数组中。

功能实现

  ArrayList 继承于 List,对元素的增删操作就转化为对数组的操作,下面分别介绍 ArrayList 的各个操作

add

  ArrayList 有两个 add 操作的方法,他们的方法原型如下:

// 直接添加元素到 ArrayList 中
public boolean add(E e);

// 在 index 位置上插入元素
public boolean add(int index, E e);

  从方法原型可看出一个是直接将元素添加到数组后面;另一个则是在数据的 index 位置插入元素,他们两者实际上操作并没有多大区别,只是后者需要判断数组在 index 位置上是否存在元素。

以下为 add 方法实现

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

public boolean add(int index, E e) {
    // 该步骤检查 index 是否大于当前数组含有的元素,若大于,则会抛出异常
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  

    // 将插入的位置的元素以及后面元素向后移动一个位置
    System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
    elementData[index] = element;
    size++;
}

  从两个方法看,他们都调用 ensureCapacityInternal 方法,这是个判断当前是否需要扩容的元素。我们都知道,ArrayList 底层是一个数组,我们添加元素是直接添加到数组上,而数组的长度是固定的,如果我们一直添加元素进去,数组肯定会塞满,因此我们则需要将数组扩容,那么我们只需要在每次添加元素前,判断当前 size+1 是否会超过数组长度,不超过则安全,超过,则将数组长度扩容 1.5 倍。

以下为扩容的实现:

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
    }

private void ensureExplicitCapacity(int minCapacity) {
    /*
     该参数是用于记录当前 ArrayList 对象被修改过多少次,只记录数组
     元素个数改变次数,修改元素不会改变该参数
    */
    modCount++;

    // 如果当前添加元素后的容量大于数组长度,则进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}


private void grow(int minCapacity) {
    // 扩容 1.5 倍
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 将旧数组复制到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

remove

  remove 操作也有两个方法,方法实现如下:

public E remove(int index) {
    // 判断当前 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;

    return oldValue;
}

public boolean remove(Object o) {
    if (o == null) {

    /*
        因为 ArrayList 可以存放 null,对象判断相同是采用 equal 方
        法,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(index) 一样,将被删除元素后面向前移动一个位置
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; 
}

  前者删除数组上 index 位置上的元素 ,先判断 index 是否合法,合法则将被删除元素后面的位置向前移动一个位置。后者是先遍历数组,找到 list 集合中第一个与欲删除元素相等的元素,然后删除该位置的元素,该方法只会删除第一个与欲删除元素相等的元素,list 是可以存放重复元素,因此调用 remove(obj) 是不会删除所有与 obj 相等的元素,只会删除第一个。
  另外,每个删除操作的最后都会执行 elementData[–size] = null,我们都知道我们会将元素向前移动,如果只是单纯地将 size–,那么数组对最后的元素就会有两个应用,实际上是只有一个的,这样会导致发生 GC 时,若该对象已经失效,但可能回收该对象会失败,该操作是为了保证 GC 工作正常。

Iterator

  Iterator 是迭代器,用于迭代集合中的元素,使用过 ArrayList 的人都知道,ArrayList 是线程不安全的,一个线程使用 Iterator 遍历集合时,如果其他线程对该集合进行了修改(增加或删除了元素),则会导致迭代失败,如下是 ArrayList 中的 Iterator 实现

private class Itr implements Iterator<E> {
    int cursor;       // 游标
    int lastRet = -1; // 上一个刚访问的元素

    // 记住创建迭代时,list 已被修改多少次,通过该参数判断数组是否被修改过
    int expectedModCount = modCount;

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

    final void checkForComodification() {
        // 如果 list 被修改了,则两者不相同,则抛出异常
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        }
    }

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

    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 使用 Iterator 遍历时,是需要依赖 modCount,该值表示 ArryList 的元素个数共被修改多少次(共插入删除多少次),当创建 Iterator 时,会保存当前的 modCount,当做迭代遍历时,每次都会与当前的 modCount作比较,若 modCount 发生改变,则代表该 ArrayList 被修改了,所以抛出 ConcurrentModificationException。

总结

  ArrayList 是 List 的动态数组实现,增删元素操作都实际上是对底层数组进行操作。插入元素时,都会检查元素个数是否会超过数组长度,若超过了,则会进行扩容,扩容的新数组长度为原数组的 1.5 倍;若未超过,则可直接插入。同时插入和删除元素后都是采用将元素前移或后移来保证数组保存的元素连续,因此此操作的时间复杂度为 O(logn)。
  另外,ArrayList 是线程不安全的,它使用 modCount 来记录对 list 的修改次数,当使用 Iterator 遍历时,会记录当前的 modCount,若在遍历的过程中,存在其他线程修改了 ArrayList,则会导致 modCount 发生改变,此时 Iterator 遍历时会检查两者是否相同,不相同则抛出 ConcurrentModificationException。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值