微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

使用迭代器从 Java 集合中删除元素

如何解决使用迭代器从 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]

此输出的原因如下所示:

enter image description here

,

这是一段有趣的代码(可能是一个很好的面试问题)。这个程序会编译吗?如果是这样,它会无例外地运行吗?

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% 正确是没有意义的。

我们还可以进行一些概括。

  1. 如果一个集合(及其迭代器)仅被一个线程使用,则可以保证 100% 的正确性。

  2. 并发 集合类型可以通过其迭代器从任意数量的线程安全地访问和更新。不过有一些注意事项:

    • 不能保证迭代会看到迭代开始后所做的结构更改。
    • 迭代器不能被多个线程共享。
    • ConcurrentHashMap 的批量操作不是原子操作。

    如果您的正确性标准不依赖于这些东西,那么可以保证 100% 的正确性。

注意:我并不是说迭代器保证正确性。我是说迭代器可以成为正确解决方案的一部分,前提是您以正确的方式使用它们。

我是否需要以这样一种方式设计我的代码,即在修改集合时删除总是会导致正确的行为?

这取决于您如何使用该集合。见上文。

但作为一般规则,您确实需要设计实现您的代码是正确的。 (正确不会靠魔法发生……)

如果是这样,谁能举一个例子,说明在测试之外使用迭代器的 remove() 方法是有用的吗?

在任何只有一个线程可以访问集合的示例中,对于所有标准集合类,使用 remove() 都是 100% 安全的。

在集合是并发类型的许多示例中,remove() 是 100% 安全的。 (但不能保证如果另一个线程同时尝试添加一个元素,它会保持被移除。或者它会因此被添加。)

最重要的是,如果您的应用程序是多线程的,那么您必须理解不同线程如何与共享集合交互。没有办法避免这种情况。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。