迭代时从集合中删除元素

Posted

技术标签:

【中文标题】迭代时从集合中删除元素【英文标题】:Remove elements from collection while iterating 【发布时间】:2012-05-13 00:05:11 【问题描述】:

AFAIK,有两种方法:

    遍历集合的副本 使用实际集合的迭代器

例如,

List<Foo> fooListCopy = new ArrayList<Foo>(fooList);
for(Foo foo : fooListCopy)
    // modify actual fooList

Iterator<Foo> itr = fooList.iterator();
while(itr.hasNext())
    // modify actual fooList using itr.remove()

是否有任何理由偏爱一种方法而不是另一种(例如,出于可读性的简单原因而偏爱第一种方法)?

【问题讨论】:

只是好奇,你为什么要创建一个傻瓜的副本而不是在第一个例子中循环遍历傻瓜? @Haz,所以我只需要循环一次。 注意:更喜欢 'for' 而不是 'while' 也使用迭代器来限制变量的范围:for(Iterator itr = fooList.iterator(); itr.hasNext();) 我不知道while 的作用域规则与for 不同 在更复杂的情况下,您可能会遇到fooList 是实例变量的情况,并且您在循环期间调用了一个方法,该方法最终调用了与fooList.remove(obj) 相同的类中的另一个方法。见过这种情况。在这种情况下,复制列表是最安全的。 【参考方案1】:

你可以看到这个样本;如果我们认为从列表中删除奇数:

public static void main(String[] args) 
    Predicate<Integer> isOdd = v -> v % 2 == 0;
    List<Integer> listArr = Arrays.asList(5, 7, 90, 11, 55, 60);
    listArr = listArr.stream().filter(isOdd).collect(Collectors.toList());
    listArr.forEach(System.out::println);

【讨论】:

【参考方案2】:

最喜欢的老计时器(它仍然有效):

List<String> list;

for(int i = list.size() - 1; i >= 0; --i) 

        if(list.get(i).contains("bad"))
        
                list.remove(i);
        

好处:

    它只迭代列表一次 没有创建额外的对象或其他不必要的复杂性 尝试使用已删除项目的索引没有问题,因为...好吧,考虑一下!

【讨论】:

有时它是唯一可行的解​​决方案。 乍一看你可能会错过它,但秘诀是向后遍历列表。这样可以防止每次删除都会更改未来潜在删除的索引。 我实际上更愿意从头开始迭代列表,删除项目,然后递减计数器。在我看来,这具有更好的可读性。所以只需使用:for (int i = 0; i &lt; list.size(); i++) ...remove(i); i--;...【参考方案3】:

让我举几个例子来避免ConcurrentModificationException

假设我们有以下书籍集合

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

收集和删除

第一种技术包括收集我们想要删除的所有对象(例如,使用增强的 for 循环),在完成迭代后,我们删除所有找到的对象。

ISBN isbn = new ISBN("0-201-63361-2");
List<Book> found = new ArrayList<Book>();
for(Book book : books)
    if(book.getIsbn().equals(isbn))
        found.add(book);
    

books.removeAll(found);

这是假设您要执行的操作是“删除”。

如果您想“添加”这种方法也可以,但我假设您将遍历不同的集合以确定要添加到第二个集合的元素,然后在最后发出 addAll 方法.

使用 ListIterator

如果您使用列表,另一种技术是使用 ListIterator,它支持在迭代过程中删除和添加项目。

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext())
    if(iter.next().getIsbn().equals(isbn))
        iter.remove();
    

再次,我在上面的示例中使用了“删除”方法,这似乎是您的问题所暗示的,但您也可以使用它的 add 方法在迭代期间添加新元素。

使用 JDK >= 8

对于使用 Java 8 或更高版本的用户,您可以使用其他几种技术来利用它。

您可以在Collection 基类中使用新的removeIf 方法:

ISBN other = new ISBN("0-201-63361-2");
books.removeIf(b -> b.getIsbn().equals(other));

或使用新的流 API:

ISBN other = new ISBN("0-201-63361-2");
List<Book> filtered = books.stream()
                           .filter(b -> b.getIsbn().equals(other))
                           .collect(Collectors.toList());

在最后一种情况下,要从集合中过滤元素,您可以将原始引用重新分配给过滤后的集合(即books = filtered)或使用过滤后的集合来removeAll从原始集合中找到的元素(即@ 987654334@).

使用子列表或子集

还有其他选择。如果列表已排序,并且您想要删除连续的元素,您可以创建一个子列表然后将其清除:

books.subList(0,5).clear();

由于子列表由原始列表支持,这将是删除此元素子集合的有效方法。

使用NavigableSet.subSet 方法或那里提供的任何切片方法可以通过排序集实现类似的效果。

注意事项:

你使用什么方法可能取决于你打算做什么

collect 和removeAl 技术适用于任何集合(集合、列表、集合等)。 ListIterator 技术显然只适用于列表,前提是它们给定的ListIterator 实现支持添加和删除操作。 Iterator 方法适用于任何类型的集合,但它仅支持删除操作。 使用ListIterator/Iterator 方法的明显优势是不必复制任何内容,因为我们在迭代时会删除。所以,这是非常有效的。 JDK 8 流示例实际上并没有删除任何内容,而是寻找所需的元素,然后我们用新的元素替换了原始的收集引用,并让旧的收集引用。因此,我们只对集合进行一次迭代,这样会很有效。 在 collect 和 removeAll 方法中,缺点是我们必须迭代两次。首先,我们在 foo 循环中迭代,寻找一个符合我们移除标准的对象,一旦我们找到它,我们就要求将它从原始集合中移除,这意味着第二次迭代工作来寻找这个项目,以便去掉它。 我认为值得一提的是Iterator 接口的remove 方法在Javadocs 中被标记为“可选”,这意味着如果我们调用remove 方法,可能会有Iterator 实现抛出UnsupportedOperationException。因此,如果我们不能保证迭代器支持删除元素,我会说这种方法不如其他方法安全。

【讨论】:

太棒了!这是权威指南。 这是一个完美的答案!谢谢。 在您提到removeAll(filtered) 的有关JDK8 Streams 的段落中。一个快捷方式是removeIf(b -&gt; b.getIsbn().equals(other)) Iterator 和 ListIterator 有什么区别? 没有考虑 removeIf,但这是我祈祷的答案。谢谢!【参考方案4】:

在 Java 8 中,还有另一种方法。 Collection#removeIf

例如:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

list.removeIf(i -> i > 2);

【讨论】:

这里不回答OP的问题,这里没有迭代【参考方案5】:

有什么理由更喜欢一种方法而不是另一种方法

第一种方法可行,但复制列表的开销明显。

第二种方法不起作用,因为许多容器不允许在迭代期间进行修改。 This includes ArrayList.

如果唯一的修改是删除当前元素,您可以使用itr.remove() 使第二种方法起作用(即,使用迭代器remove() 方法,而不是容器的)。 对于支持remove() 的迭代器,这将是我的首选方法。

【讨论】:

哎呀,对不起...这暗示我将使用迭代器的删除方法,而不是容器的。复制列表会产生多少开销?它不能太多,因为它的范围是一个方法,所以它应该很快被垃圾收集。见编辑.. @aix 我认为值得一提的是Iterator 接口的remove 方法在Javadocs 中被标记为可选,这意味着可能存在可能抛出UnsupportedOperationException 的Iterator 实现。因此,我想说这种方法不如第一种方法安全。根据打算使用的实现,第一种方法可能更合适。 @EdwinDalorzo remove() 对原始集合本身也可能抛出 UnsupportedOperationException:docs.oracle.com/javase/7/docs/api/java/util/…。遗憾的是,Java 容器接口被定义为极其不可靠(老实说,违背了接口的观点)。如果您不知道将在运行时使用的确切实现,最好以不可变的方式执行操作——例如,使用 Java 8+ Streams API 过滤元素并将它们收集到新容器中,然后用它完全替换旧的。【参考方案6】:

你不能做第二个,因为即使你在Iterator,you'll get an Exception thrown上使用remove()方法。

就个人而言,我更喜欢所有Collection 实例的第一个,尽管额外无意中听到了创建新的Collection,但我发现它在其他开发人员编辑期间不太容易出错。在某些 Collection 实现中,支持 Iterator remove(),而在其他实现中则不支持。您可以在Iterator 的文档中阅读更多内容。

第三种选择是创建一个新的Collection,迭代原来的,并将第一个Collection 的所有成员添加到第二个Collection删除。根据Collection 的大小和删除次数,与第一种方法相比,这可以显着节省内存。

【讨论】:

【参考方案7】:

为什么不这样?

for( int i = 0; i < Foo.size(); i++ )

   if( Foo.get(i).equals( some test ) )
   
      Foo.remove(i);
   

如果是地图,不是列表,可以使用keyset()

【讨论】:

这种方法有很多主要缺点。首先,每次删除元素时,都会重新组织索引。因此,如果删除元素 0,则元素 1 将成为新的元素 0。如果要这样做,至少要向后执行以避免此问题。其次,并非所有 List 实现都提供对元素的直接访问(就像 ArrayList 一样)。在 LinkedList 中,这将非常低效,因为每次发出 get(i) 时,您都必须访问所有节点,直到到达 i 从来没有考虑过这个,因为我通常只是用它来删除我正在寻找的单个项目。很高兴知道。 我迟到了,但肯定在Foo.remove(i); 之后的 if 块代码中你应该做i--; 因为它被窃听了【参考方案8】:

我会选择第二个,因为您不必复制内存并且迭代器工作得更快。这样可以节省内存和时间。

【讨论】:

"迭代器工作得更快"。有什么可以支持这种说法的吗?此外,制作列表副本的内存占用非常小,特别是因为它会在方法内限定范围并且几乎会立即进行垃圾收集。 第一种方法的缺点是我们必须迭代两次。我们在 foo 循环中迭代寻找一个元素,一旦找到它,我们要求将其从原始列表中删除,这意味着第二次迭代工作来寻找这个给定的项目。这将支持这样的说法,即至少在这种情况下,迭代器方法应该更快。我们必须考虑到只有集合的结构空间是被创建的,集合内的对象没有被复制。两个集合都会保留对相同对象的引用。当 GC 发生时,我们无法判断!!!【参考方案9】:

只有第二种方法可行。您只能在迭代期间使用iterator.remove() 修改集合。所有其他尝试都会导致ConcurrentModificationException

【讨论】:

第一次尝试迭代一个副本,这意味着他可以修改原件。

以上是关于迭代时从集合中删除元素的主要内容,如果未能解决你的问题,请参考以下文章

Python:如何在迭代列表时从列表中删除元素而不跳过未来的迭代

在迭代时从地图(或任何其他STL容器)中删除/删除内容

如何在迭代字典时从字典中删除项目?

在迭代python时从剩余数组中找到2个元素的最小差异

忘记不修改原始集合 Laravel 5.7

单击元素时从 mongodb 中删除特定的数组元素