迭代时从集合中删除元素


215

AFAIK有两种方法:

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

例如,

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()
}

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


1
很好奇,为什么在第一个示例中创建一个foolist副本而不是仅仅遍历foolist?
哈兹(Haz)

@Haz,所以我只需要循环一次。
user1329572 2012年

15
注意:对于迭代器,也优选使用“ for”而不是“ while”来限制变量的范围:for(Iterator <Foo> itr = fooList.iterator(); itr.hasNext();){}
Puce

我不知道whilefor
亚历山大·米尔斯

在更复杂的情况下,您可能有一个fooList实例变量的情况,您在循环中调用了一个方法,最终调用了同一个类中的另一个方法fooList.remove(obj)。见过这种情况。在这种情况下,复制列表最安全。
戴夫·格里菲思

Answers:


415

让我举几个例子,并提出一些避免方案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或更高版本的用户,您可以使用多种其他技术来利用它。

您可以removeIfCollection基类中使用新方法:

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从原始集合中找到的元素(即books.removeAll(filtered))。

使用子列表或子集

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

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

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

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

注意事项:

您使用哪种方法可能取决于您打算做什么

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

太棒了!这是权威指南。
Magno C

这是一个完美的答案!谢谢。
威廉

6
在有关JDK8 Streams的段落中,您提到removeAll(filtered)。快捷方式是removeIf(b -> b.getIsbn().equals(other))
ifloop

Iterator和ListIterator有什么区别?
亚历山大·米尔斯

没有考虑过removeIf,但这是我祈祷的答案。谢谢!
Akabelle

15

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

例如:

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

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

13

是否有任何理由比另一种更喜欢一种方法

第一种方法可以使用,但是复制列表有明显的开销。

第二种方法不起作用,因为许多容器不允许在迭代过程中进行修改。这包括ArrayList

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


糟糕,抱歉...这意味着我将使用迭代器的remove方法,而不是容器的方法。复制列表会产生多少开销?它不能太多,并且由于它只限于一种方法,因此应该相当快地对其进行垃圾回收。见编辑..
user1329572

1
@aix我认为值得一提的是,Iterator接口的remove方法在Javadocs中被标记为可选,这意味着可能存在可能抛出的Iterator实现UnsupportedOperationException。因此,我想说这种方法不如第一种安全。根据打算使用的实现方式,第一种方法可能更合适。
Edwin Dalorzo

remove()原始集合本身上的@EdwinDalorzo 也可能抛出UnsupportedOperationExceptiondocs.oracle.com/javase/7/docs/api/java/util/…。不幸的是,Java容器接口被定义为极其不可靠(说实话,它破坏了接口的要点)。如果您不知道将在运行时使用的确切实现,那么最好以一种不变的方式进行操作-例如,使用Java 8+ Streams API过滤掉元素并将其收集到新容器中,然后完全替换旧的。
马修

5

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


2
第一次尝试遍历副本,这意味着他可以修改原始副本。
Colin D


1

您无法执行第二个操作,因为即使您remove()Iterator上使用方法,也会抛出Exception

就个人而言,Collection尽管创建新的代码时有过偷听的Collection感觉,但我还是希望所有实例都使用第一个实例,但我发现它在其他开发人员编辑期间不太容易出错。在某些Collection实现中,remove()支持Iterator ,而在其他实现中则不支持。您可以在文档中了解有关Iterator的更多信息

第三种选择是创建一个new Collection,在原始对象上进行迭代,然后将第一个的所有成员添加Collection到第二个中Collection,这些成员不可以删除。Collection与第一种方法相比,取决于的大小和删除的数量,这可能会大大节省内存。


0

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


迭代器工作更快 ”。有什么证据可以支持这项要求?另外,制作列表副本的内存占用非常微不足道,特别是因为它将在方法内确定范围并几乎立即进行垃圾回收。
user1329572 2012年

1
第一种方法的缺点是我们必须迭代两次。我们在foor循环中进行迭代以查找元素,一旦找到该元素,便要求将其从原始列表中删除,这意味着需要进行第二次迭代才能查找该给定的项目。这将支持这样的主张,至少在这种情况下,迭代器方法应该更快。我们必须考虑到,只有集合的结构空间是正在创建的空间,集合内部的对象不会被复制。这两个集合将保留对相同对象的引用。当GC发生时,我们无法分辨!!!
Edwin Dalorzo

-2

为什么不呢?

for( int i = 0; i < Foo.size(); i++ )
{
   if( Foo.get(i).equals( some test ) )
   {
      Foo.remove(i);
   }
}

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


4
这种方法有许多主要缺点。首先,每次删除元素时,索引都会重新组织。因此,如果删除元素0,则元素1变为新的元素0。如果要进行此操作,至少要向后进行操作以避免此问题。其次,并非所有List实现都提供对元素的直接访问(就像ArrayList一样)。在LinkedList中,这将是非常低效的,因为每次发出an时get(i),都必须访问所有节点,直到到达i
Edwin Dalorzo

从来没有考虑过,因为我通常只是用它来删除我要查找的单个项目。很高兴知道。
德雷克·克拉里斯

4
我参加聚会迟到了,但是您可以确定在if块代码中Foo.remove(i);应该这样做i--;吗?
Bertie Wheen 2013年

因为它被窃听了
杰克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.