前言
在遍历ArrayList
时候进行删除一不小心就会报错,之前遇到直接查询答案用Iterator
遍历再删除就可以了,没深究,今天有时间想好好研究下,就准备把List的所有遍历方法都尝试一下,看看哪些会报错,为什么会错,没想到在准备测试数据阶段就载了个跟头
准备数据阶段
看下面的代码,会报错吗?什么错?
private List list;
@Test
public void t10() {
for (Integer tar : list) {
if (tar >= 2)
list.remove(tar);
}
}
@Before
public void before() {
Integer[] arr = {0, 1, 2, 3, 4};
list = Arrays.asList(arr);
}
我说一个异常,大家肯定熟悉ConcurrentModificationException
,这个异常是遍历时不正确删除导致的,具体哪些不正确我下面再说。问题是上面的代码不是报这个错,而是:
java.lang.UnsupportedOperationException
at java.util.AbstractList.remove(AbstractList.java:161)
Arrays.asList的真面目
找到我们上面遇到的异常处,什么鬼?AbstractList
不想和你说话,并向你抛出了一个异常:
/**
* {@inheritDoc}
*
* <p>This implementation always throws an
* {@code UnsupportedOperationException}.
*
* @throws UnsupportedOperationException {@inheritDoc}
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
throw new UnsupportedOperationException();
}
确实什么事都没做直接抛,注释也说了这个实现总会抛出异常,不只这一个方法,像add(int index, E element)
, remove(int index)
也是直接抛的,这是个抽象类嘛,实现的一部分可以共用,另一部分要子类具体实现的,说明Attars.asList
返回的List
并没有实现这个方法,进到Arrays
里面:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
哇,是ArrayList
,真的是我们熟悉的那个ArraysList
吗,不,根本没有这个构造,它的真面目是:
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
。。。。。。
}
只是Arrays
中的一个静态内部类,Objects.requireNonNull
是一个是否为空的判断,不用管它。对于这个ArrayList
,没有实现add(E e)
, remove(int index)
和remove(Object o)
等方法,自然是不可修改的。它的用处也不算少见,要想操作数据可以:
List<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(arr));
ArrayList遍历删除
错误1
由于foreach
语法比fori
简单好用,大多数场合我们是能用就用,懒就要有懒的代价啊,要是用fori
,你怎么删除都不会报错,但删除后可能并不是你想要的效果哟!详见错误3。回到正题,foreach
其实是用Iterator
来遍历的,对于集合来说,只要实现了java.lang.Iterable
接口,就可以使用foreach
。
看下面代码,有什么错误?
Integer[] arr = {0, 1, 2, 3, 4};
List<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(arr));
for (Integer tar : list) {
if (tar == 2){
list.remove(tar);
}
}
答案:报错ConcurrentModificationException
位置在下面代码的倒数第三行,下面是 ArrayList
中的迭代器。当我们使用foreach
时,就是使用这个迭代器工作的,cursor
是游标,指示当前已取出元素的下一个元素,lastRet
指示当前已取出元素,expectedModCount
是期待的修改次数,modCount
是实际修改次数,每次循环都会先调用hasNext()
,当游标不等于(即小于)list.size()
时说明还有下一个元素,再调用next
取出下一个值,next()
方法的第一个方法就是checkForComodification()
,检查期待的修改次数是否与实际相等,不相等就抛异常,expectedModCount
变量范围是这个迭代器,使用list.remove(Object obj)
只会使modCount++
,expectedModCount
的值不变自然就出错了。所以采用Iterator
遍历是个明智的选择,它的remove()
方法里面ArrayList.this.remove(lastRet)
会 让modCount++
,但随后又把modCount
的值赋给了expectedModCount
,继续循环不会出问题。
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;
public boolean hasNext() {
return cursor != size;
}
@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();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
小提问:把上面的遍历tar == 2
改成tar == 3
还会错吗?
如果你把上面的解释仔细看了的话,想必已经知道答案了,不会。
解答:当tar == 3
时,当前游标cursor
是4,size
是5,但当删除了tar
,size
就变成了4,和cursor
相等了,到下一次循环,hasNext()
判断时为false
,所以结束了循环,不给它抛异常的机会。
错误2
你以为用了Iterator
遍历就高枕无忧了吗?下面代码正确吗?
Integer[] arr = {0, 1, 2, 3, 4};
List<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(arr));
for (Iterator<Integer> iter = list.iterator(); iter.hasNext(); ) {
Integer tar = iter.next();
if (tar == 2)
list.remove(tar);
}
依然微笑的给你抛个异常,虽然用的是Iterator
遍历,但是删除方法依然是ArrayList
自己的,不是iter.remove()
,这个错误和错误1是一样的,属于脱裤子放屁。
错误3
这个是特殊的,不报错,但是你处理完成后的数据很可能不是你想要的。
猜猜看剩下什么?
Integer[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
List<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(arr));
for (int i = 0; i < list.size(); i++) {
if (list.get(i) > 3)
list.remove(i);
}
答案:[0,1,2,3,5,7,9,11]
解析:这一切都是字母i
的锅,删除4之前,i = 4
,list.get(i) = 4
, 删除4之后i
不变,但是list.get(i) = 5
, 因为后面的元素都向前进了一步,所以每删除一个,这个被删除元素的下一个就幸免于难了。
解决方法:list.remove(i--)
, 真是机智又优雅, 开心,其实我最喜欢的遍历方法就是这个。
总结
上面的错误方法都附了解答,有些小伙伴不喜欢看步骤,就要答案,那么总结下ArrayList
的正确遍历方法
Integer[] arr = {0, 1, 2, 3, 4, 5, 6, 7};
List<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(arr));
//第一种
for (Iterator<Integer> iter = list.iterator(); iter.hasNext(); ) {
Integer tar = iter.next();
if (tar == 2)
iter.remove();
}
//第二种
for (int i = 0; i < list.size(); i++) {
if (list.get(i) > 3)
list.remove(i--);
}
上面只是针对在遍历时需要多次删除时使用,如果你不删除,或者只删除一次然后break
结束循环,你随便用任何你喜欢的方式。
还有remove(int index)
和remove(Object o)
两个方法,你可以认为是一样的,remove(Object o)
无非是内部遍历查找equals
为true
的元素或者都为null
的元素,然后调用fastRemove(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;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
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;
}
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; // clear to let GC do its work
}
网友评论