List容器一边遍历,一边删除在一些业务逻辑上总是会存在的。
小栗子
例如有一个列表,在UI上的反馈就是用户勾选了几个需要删除的,勾选后点击确认删除;代码逻辑就是在列表中标识勾选的项,然后遍历删除;
先不说这个例子有多少实现的方法,就针对遍历删除来讨论。
- for-each
- for-each修订版
- for循环
- for循环倒序
- 迭代器
for-each
public class ForTest {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
intList.add(i);
}
//操作检测异常:ConcurrentModificationException;for-each遍历的时候,不允许删除;
//对应的操作就是:for-each迭代初始化时内部会有一个标签expectedModCount记录全局的modCount(数字类型),这个modCount数字是全局的,做增删的时候会+1;
// 而for-each内部的记录标签是局部的,所以当循环跑起来的时候,记录了一个数字,remove的时候modCount会改变,则循环下一次开始的时候检测比对不同则弹出该异常
for (Integer integer : intList) {
if (integer == 2 || integer == 3) {
intList.remove(integer);
}
}
System.out.println(Arrays.toString(intList.toArray()));
}
}
上面的for循环,我没有使用int来遍历而是使用Integer是因为在调用List.remove这个API时,它有重载,
- remove(Object object)
- remove(int index);
而我想要的是第一个,所以用了装箱Integer
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 ForTest.main(ForTest.java:27)
上面的操作会产生一个关于modCount的异常:ConcurrentModificationException,解释也在代码注释中写了,这里也放一下:
- 操作检测异常:ConcurrentModificationException;for-each遍历的时候,不允许删除;
- 对应的操作就是:for-each迭代初始化时内部会有一个标签expectedModCount记录全局的modCount(数字类型),这个modCount数字是全局的,做增删操作的时候会+1;
- 而for-each内部的记录标签expectedModCount是局部的,所以当循环跑起来的时候(初始化),记录了一下(expectedModCount = modCount),而remove的时候全局modCount会改变,则循环下一次开始的时候检测比对不同则弹出该异常(expectedModCount !=modCount )
for-each修订版
public class ForTest {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
intList.add(i);
}
//正常,因为每次循环都只走了一次,然后就用了新的List,所以避开了第二次循环时的mountCount检查
for (Integer integer : new ArrayList<Integer>(intList)) {
System.out.println(integer);
if (integer == 2 || integer == 3) {
intList.remove(integer);
}
}
System.out.println(Arrays.toString(intList.toArray()));
}
}
[0, 1, 4]
上面这个结果是符合预期的;关键点就在于for (Integer integer : new ArrayList(intList)) {};
因为每次循环都只走了一次,然后就用了新的List,所以避开了第二次循环时的modCount检查。
结果虽然是对的,但是不提倡使用,因为List的长度越长,重新开辟的新的List越多,浪费大量内存;
这个方式是在《Thinking in Java》中看一个别的例子中看到的,最开始我看到的时候把那个 new ArrayList(intList) 看漏掉了,以为就是普通的for-each;然后就觉得不可思议,这明显是玩不通的,大师还会犯这种错吗,等发现了这个关键点的时候才发现大师是玩的真六。
上面这个仅限于可能性实践讨论,不建议使用。
for循环
public class ForTest {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
intList.add(i);
}
//下标错位,移除了第一个,下标前进,数据前移,数据总长度会改变,所以下标会跳跃一位
for (int i = 0; i < intList.size(); i++) {
if (intList.get(i) == 2 || intList.get(i) == 3 || intList.get(i) == 4) {
intList.remove(i);
}
}
System.out.println(Arrays.toString(intList.toArray()));
}
[0, 1, 3]
上面的for循环理论上应该删除[2, 3, 4],还剩下[0, 1]的,但是结果是剩下了[0, 1, 3];
首先0,1跑过去肯定没问题;下标到2的时候,2删除了;然后下标跑到3,此时因为2删除了,所以下标在3的位置就是4了;于是3躲过一劫,4被删除了;此时下标跑到4,但是List的长度只为3了,for循环结束;
总的来说就是移除一位后,List的size()就变了,List删除的后一位就会下标补位前进;如果遍历下标仍然按照之前的走,就会跳跃一位,造成下标错位,误删漏删。
for循环倒序
public class ForTest {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
intList.add(i);
}
//正常,因为下标是往前移动;如果下标对应的某个数据符合条件被移除,则下标对应的数组后面的数据往前移动,但是因为下标是往前走的,前面的数据没改变,所以移除是正常的
for (int i = intList.size() - 1; i > 0; i--) {
if (intList.get(i) == 2 || intList.get(i) == 3 || intList.get(i) == 4) {
intList.remove(i);
}
}
System.out.println(Arrays.toString(intList.toArray()));
}
}
[0, 1]
上面这个是正确的;和正序相比,List删除后往前补位,但是只能补到被删除元素的当前下标,而下一轮遍历,下标是往前走的,所以没有影响。
迭代器
public class ForTest {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
intList.add(i);
}
//迭代器是正常的,因为游标每一步都在检测是否有下一个,不依托于下标,所以既不会越界也不会下标错乱
Iterator<Integer> iterator = intList.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
if (next == 2 || next == 3 || next == 4) {
iterator.remove();
}
}
System.out.println(Arrays.toString(intList.toArray()));
}
}
[0, 1]
迭代器是正常的;因为游标每一步都在检测是否有下一个,不依托于下标,所以既不会越界也不会下标错乱
所以以上可行的就是三个
- for-each的修订版
- for循环倒序
- 迭代器
但是在投入使用中,还是for循环倒序和迭代器。
在Java8中的Collection接口中有一个叫做removeIf的default方法,它接收一个过滤函数,内部实现就是迭代器,可以做迭代器的简写版(操作符)
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;
}
上面迭代器的使用可以改做:
public class ForTest {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
intList.add(i);
}
//迭代器是正常的,因为游标每一步都在检测是否有下一个,不依托于下标,所以既不会越界也不会下标错乱
intList.removeIf(next -> next == 2 || next == 3 || next == 4);
System.out.println(Arrays.toString(intList.toArray()));
}
}