如何解决使用迭代器从 Java 集合中删除元素
有很多帖子建议使用迭代器从集合中安全地删除元素。像这样:
Iterator<Book> i = books.iterator();
while(i.hasNext()){
if(i.next().isbn().equals(isbn)){
i.remove();
}
}
根据文档,使用迭代器的好处是它是“快速失败”的,因为如果任何线程正在修改集合(上面示例中的书籍),而使用迭代器,则迭代器会抛出 ConcurrentModificationException。 但是,此异常的文档也说
请注意,不能保证快速失败行为,因为一般来说,在存在非同步并发修改的情况下不可能做出任何硬保证。快速失败操作会尽最大努力抛出 ConcurrentModificationException。因此,编写一个依赖此异常来保证其正确性的程序是错误的:ConcurrentModificationException 应该仅用于检测错误。
这是否意味着如果必须保证 100% 的正确性,则不能使用迭代器?我是否需要以这样一种方式设计我的代码,即在修改集合时删除总是会导致正确的行为?如果是这样,任何人都可以举一个例子,说明在测试之外使用迭代器的 .remove() 方法是有用的吗?
解决方法
Iterator.remove
就可以工作。有时它是一个方便的功能。
说到多线程环境,这真的取决于你如何组织代码。 例如,如果您在 Web 请求中创建一个集合并且不与其他请求共享它(例如,如果它通过方法参数传递给某些方法),您仍然可以安全地使用这种遍历集合的方法。
另一方面,如果您说在所有请求之间共享指标快照的“全局”队列,则每个请求都会向该队列添加统计信息,并且其他一些线程读取队列元素并删除指标,这样就赢了不合适。 因此,这完全取决于用例以及您如何组织代码。
至于您要求的示例,假设您有一个字符串集合,并希望通过修改现有集合来删除所有以字母“a”开头的字符串
Iterator<String> i = strings.iterator();
while(i.hasNext()){
if(i.next().startsWith('a')){
i.remove();
}
}
当然,在 Java 8+ 中,您可以几乎用 Streams 实现相同的效果:
strings.stream()
.filter(s -> !s.startsWith('a'))
.collect(Collectors.toList());
但是,此方法创建了一个新集合,而不是修改现有集合(就像使用迭代器的情况一样)。
在 Java 8 之前的世界中(并且在 Java 8 可用之前就已经出现了迭代器),我们甚至没有流,因此编写这样的代码并不是真正简单的任务。
,Iterator#remove
保证单线程处理的 100% 正确性。在数据的多线程处理中,这取决于您如何处理数据(同步/异步处理,使用不同的列表来收集要删除的元素等)。
只要不想修改同一个集合,可以将要删除的元素集合起来,放到一个单独的List
中,使用List#removeAll(Collection<?> c)
,如下图:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
List<Integer> elementsToBeRemoved = new ArrayList<>();
for (Integer i : list) {
if (i % 2 == 0) {
elementsToBeRemoved.add(i);
}
}
list.removeAll(elementsToBeRemoved);
System.out.println(list);
}
}
输出:
[1,3]
在循环中,永远不要使用索引删除元素
对于初学者来说,使用 List#remove(int index)
使用索引删除元素可能很诱人,但每个删除操作都会调整 List
的大小这一事实使它产生令人困惑的结果,例如
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
public class Main {
public static void main(String[] args) {
List<Integer> list = new Vector<>();
list.add(1);
list.add(2);
Iterator<Integer> i = list.iterator();
while (i.hasNext()) {
System.out.println("I'm inside the iterator loop.");
i.next();
list.remove(0);
}
System.out.println(list);
}
}
输出:
I'm inside the iterator loop.
[2]
此输出的原因如下所示:
,这是一段有趣的代码(可能是一个很好的面试问题)。这个程序会编译吗?如果是这样,它会无例外地运行吗?
List<Integer> list = new Vector<>();
list.add(1);
list.add(2);
Iterator<Integer> i = list.iterator();
while (i.hasNext()) {
i.next();
list.remove(0);
}
回答:是的。它将毫无例外地编译和运行。那是因为列表有两种删除方法:
E remove(int index) 移除此列表中指定位置的元素(可选操作)。
布尔值删除(对象 o) 从此列表中删除第一次出现的指定元素(如果存在)(可选操作)。
被调用的是boolean remove(Object o)。由于0不在列表中,所以列表没有被修改,也没有错误。这并不意味着迭代器的概念有问题,但它表明,即使在单线程情况下,仅仅因为使用了迭代器,并不意味着开发人员不能犯错误。
,这是否意味着如果必须保证 100% 的正确性,则不能使用迭代器?
不一定。
首先,这取决于您的正确性标准。正确性只能根据指定的要求来衡量。如果你不说要求是什么,那么说 100% 正确是没有意义的。
我们还可以进行一些概括。
-
如果一个集合(及其迭代器)仅被一个线程使用,则可以保证 100% 的正确性。
-
并发 集合类型可以通过其迭代器从任意数量的线程安全地访问和更新。不过有一些注意事项:
- 不能保证迭代会看到迭代开始后所做的结构更改。
- 迭代器不能被多个线程共享。
- 对
ConcurrentHashMap
的批量操作不是原子操作。
如果您的正确性标准不依赖于这些东西,那么可以保证 100% 的正确性。
注意:我并不是说迭代器保证正确性。我是说迭代器可以成为正确解决方案的一部分,前提是您以正确的方式使用它们。
我是否需要以这样一种方式设计我的代码,即在修改集合时删除总是会导致正确的行为?
这取决于您如何使用该集合。见上文。
但作为一般规则,您确实需要设计和实现您的代码是正确的。 (正确不会靠魔法发生……)
如果是这样,谁能举一个例子,说明在测试之外使用迭代器的 remove()
方法是有用的吗?
在任何只有一个线程可以访问集合的示例中,对于所有标准集合类,使用 remove()
都是 100% 安全的。
在集合是并发类型的许多示例中,remove()
是 100% 安全的。 (但不能保证如果另一个线程同时尝试添加一个元素,它会保持被移除。或者它会因此被添加。)
最重要的是,如果您的应用程序是多线程的,那么您必须理解不同线程如何与共享集合交互。没有办法避免这种情况。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。