繁体   English   中英

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

[英]Using Iterators to remove elements from a Java Collection

有很多帖子建议使用迭代器从集合中安全地删除元素。 像这样的东西:

Iterator<Book> i = books.iterator();
while(i.hasNext()){
    if(i.next().isbn().equals(isbn)){
        i.remove();
    }
}

根据文档,使用迭代器的好处是它是“快速失败”的,因为如果任何线程正在修改集合(上面示例中的书籍),而使用迭代器,那么迭代器将抛出一个并发修改异常。 但是,此异常的文档也说

请注意,不能保证快速失败的行为,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬保证。 快速失败操作会尽最大努力抛出 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);
    }
}

Output:

[1, 3]

在循环中,永远不要使用索引删除元素

对于初学者来说,使用List#remove(int index)来删除使用 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);
    }
}

Output:

I'm inside the iterator loop.
[2]

此 output 的原因如下所示:

在此处输入图像描述

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

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)删除此列表中指定 position 处的元素(可选操作)。

boolean remove(Object o)如果指定元素存在,则从该列表中删除第一个出现的元素(可选操作)。

而被调用的是boolean remove(Object o) 由于 0 不在列表中,所以列表没有被修改,也没有错误。 这并不意味着迭代器的概念有问题,但它表明,即使在单线程情况下,仅仅因为使用了迭代器,并不意味着开发人员不会出错。

这是否意味着如果必须保证 100% 的正确性,则不能选择使用迭代器?

不必要。

首先,这取决于您的正确性标准。 正确性只能根据指定的要求来衡量。 如果您不说出要求是什么,那么说某事是 100% 正确是没有意义的。

我们也可以做出一些概括。

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

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

    • 不保证迭代在迭代开始后会看到结构更改。
    • 迭代器并非设计为由多个线程共享。
    • ConcurrentHashMap上的批量操作不是原子的。

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

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

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

这取决于您如何使用该集合。 看上面。

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM