集合删除的陷阱

集合删除的陷阱

基本原理

说到集合,最典型的就属ArrayList了,这里来说说其原理。

内部有一个数组elementData保存元素,一个整数size记录实际元素个数。

private transient Object[] elementData;
private int size;

add方法如下:

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

add方法中,先用ensureCapacityInternal方法来判断容量是否足够,是否需要扩容,接着把添加的元素给到当前数组。
ensureCapacityInternal方法如下:

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

calculateCapacity方法内部为计算最小容量,ensureExplicitCapacity方法判断是否需要扩容:

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

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

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

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

综上所述,执行添加元素add方法时,会判断是否需要扩容,modCount(修改次数)会+1,且是用的Arrays.copyOf实现扩容。

再来看看remove方法:

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

可看到,执行删元素法remove方法时,,modCount(修改次数)也会+1,且使用的System.arraycopy实现元素的删除,index往后的元素都往前移动一位。

删除的陷阱(一)

for (Integer item : list) {
        if (item <= 2) {
            list.remove(item);
        }
    }

上面这段代码,会抛异常:

java.util.ConcurrentModificationException

因为迭代器内部会维护一些索引位置相关的数据,要求在迭代过程中,容器不能发生结构性变化,否则这些索引位置就失效了。

什么是结构性变化?如何解决

结构性变化就是添加、插入和删除元素,只是修改元素内容不算结构性变化,源码中使用modCount来体现。

使用迭代器来解决,如下:

Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    if (it.next() <= 2) {
        it.remove();
    }
}

迭代器的原理

我们来看迭代器为什么能解决以上的问题。

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;
    //...
}

迭代器创建时,会把modCount赋值给expectedModCount。

重点在Iterator中的next方法,我们来看ArrayList中的迭代器next方法:

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

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

由上面checkForComodification可知,每次发生结构性变化的时候modCount都会增加,而每次迭代器操作的时候都会检查expectedModCount是否与modCount相同,这样就能检测出结构性变化。

这时就好知道了,迭代器中的删除,一定也会更新保持这两个变量的值相等,否则下一次调用next时就会报错,删除方法如下:

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

删除的陷阱(二)

重载的选择,是我们在集合删除时,碰到的第二个陷阱。

如集合中有5个元素[1,2,3,4,5],删除前两个,可能暴露的问题代码,如下所示:

Set<Integer> set = new HashSet<>();
List<Integer> list = new ArrayList<>();
//[1,2,3,4,5]
for (int i = 1; i <= 5; i++) {
    set.add(i);
    list.add(i);
}
//删除前两个
for (int i = 1; i <= 2; i++) {
    set.remove(i);
    list.remove(i);
}
System.out.println("set:" + set);   //结果为[3,4,5]
System.out.println("list:" + list); //结果为[1,3,5]

Set方法内部的删除的是:

boolean remove(Object o);

因此set方法没有疑问。

ArrayList方法内部有两个删除方法:

public E remove(int index)
public boolean remove(Object o)

很明显,调用到了以下标为参数的方法:

public E remove(int index)

改正如下,迫使其选择正确的重载方法:

list.remove((Integer) i);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值