迭代时从集合中移除元素

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

有没有什么理由比其他方式更喜欢一种方法(例如,由于可读性的原因,宁愿select第一种方法)?

让我举几个例子来避免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循环内删除的所有logging,并在完成迭代后,删除所有find的logging。

 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 ,它在迭代过程中支持remove / add方法。

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

再次,我使用了“删除”方法,这是你的问题似乎暗示,但你也可以使用它的add方法在迭代过程中添加新的元素。

使用JDK 8stream

或者使用JDK 8stream,lambdas / closures:

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

在最后两种情况下,将元素从集合中过滤出来,并将原始引用重新分配给过滤集合(即books = filtered ),或者使用过滤集合从原始集合中books.removeAll(filtered)find的元素(即books.removeAll(filtered) )) 。

使用子列表或子集

还有其他的select。 如果列表已sorting,并且想要删除连续的元素,则可以创build一个子列表,然后清除它:

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

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

使用NavigableSet.subSet方法或者其中提供的任何切片方法可以实现类似的操作。

注意事项:

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

  • collect和remove方法适用于任何Collection(Collection,List,Set等)。
  • ListIterator方法只适用于列表,只要它们的给定ListIterator实现提供了对添加和删除操作的支持。
  • 如果只打算使用迭代器的remove方法,那么一般来说Iterator方法可以用于任何集合。
  • 在ListIterator /迭代器方法中,显而易见的好处是不需要复制任何东西。
  • 第三方和JDK 8stream示例并没有实际删除任何内容,而是查找所需的元素,然后可以replace原始引用,并将旧引用replace为旧引用。
  • 在收集和删除方法的缺点是,我们必须迭代两次。 我们遍历foor-loop寻找一个元素,一旦我们find它,我们要求从原始列表中删除它,这意味着要寻找这个给定的项目的第二个迭代工作。
  • 我认为值得一提的是Iterator接口的remove方法在Javadocs中被标记为可选的,这意味着可能有Iterator实现可能抛出UnsupportedOperationException 。 因此,我认为这种方法比第一种方法更安全。

有没有什么理由更喜欢一种方法

第一种方法将工作,但复制列表明显的开销。

第二种方法将不起作用,因为许多容器在迭代期间不允许修改。 这包括ArrayList

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

只有第二种方法才行。 您可以在迭代过程中仅使用iterator.remove()修改集合。 所有其他尝试将导致ConcurrentModificationException

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

例如:

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

我会select第二个,因为你不必做一个内存的副本,迭代器工作得更快。 所以你节省了内存和时间。

你不能做第二个,因为即使你在Iterator上使用remove()方法, 你也会得到一个抛出的exception 。

就个人而言,我更喜欢所有Collection实例的第一个,尽pipe额外听到创build新的Collection ,我发现在其他开发人员编辑过程中不容易出错。 在一些Collection实现中,Iterator remove()被支持,而另一个则不支持。 你可以在Iterator的文档中阅读更多内容。

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

为什么不呢?

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

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