简体   繁体   English

迭代时从集合中删除元素

[英]Remove elements from collection while iterating

AFAIK, there are two approaches: AFAIK,有两种方法:

  1. Iterate over a copy of the collection遍历集合的副本
  2. Use the iterator of the actual collection使用实际集合的迭代器

For instance,例如,

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

and

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

Are there any reasons to prefer one approach over the other (eg preferring the first approach for the simple reason of readability)?有什么理由更喜欢一种方法而不是另一种(例如,出于可读性的简单原因更喜欢第一种方法)?

Let me give a few examples with some alternatives to avoid a ConcurrentModificationException .让我举几个例子来避免ConcurrentModificationException

Suppose we have the following collection of books假设我们有以下书籍集合

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")));

Collect and Remove收集和删除

The first technique consists in collecting all the objects that we want to delete (eg using an enhanced for loop) and after we finish iterating, we remove all found objects.第一种技术包括收集我们想要删除的所有对象(例如,使用增强的 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);

This is supposing that the operation you want to do is "delete".这是假设您要执行的操作是“删除”。

If you want to "add" this approach would also work, but I would assume you would iterate over a different collection to determine what elements you want to add to a second collection and then issue an addAll method at the end.如果您想“添加”这种方法也可以,但我假设您将遍历不同的集合以确定要添加到第二个集合的元素,然后在最后发出addAll方法。

Using ListIterator使用 ListIterator

If you are working with lists, another technique consists in using a ListIterator which has support for removal and addition of items during the iteration itself.如果您正在使用列表,另一种技术是使用ListIterator ,它支持在迭代过程中删除和添加项目。

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

Again, I used the "remove" method in the example above which is what your question seemed to imply, but you may also use its add method to add new elements during iteration.同样,我在上面的示例中使用了“删除”方法,这似乎是您的问题所暗示的,但您也可以使用它的add方法在迭代期间添加新元素。

Using JDK >= 8使用 JDK >= 8

For those working with Java 8 or superior versions, there are a couple of other techniques you could use to take advantage of it.对于使用 Java 8 或更高版本的用户,您可以使用其他一些技术来利用它。

You could use the new removeIf method in the Collection base class:您可以在Collection base class 中使用新的removeIf方法:

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

Or use the new stream API:或者使用新的 stream API:

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

In this last case, to filter elements out of a collection, you reassign the original reference to the filtered collection (ie books = filtered ) or used the filtered collection to removeAll the found elements from the original collection (ie books.removeAll(filtered) ).在最后一种情况下,要从集合中过滤元素,您将原始引用重新分配给过滤集合(即books = filtered )或使用过滤集合从原始集合中删除所有找到的元素(即removeAll books.removeAll(filtered) )。

Use Sublist or Subset使用子列表或子集

There are other alternatives as well.还有其他选择。 If the list is sorted, and you want to remove consecutive elements you can create a sublist and then clear it:如果列表已排序,并且您想要删除连续的元素,您可以创建一个子列表,然后将其清除:

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

Since the sublist is backed by the original list this would be an efficient way of removing this subcollection of elements.由于子列表由原始列表支持,这将是删除此元素子集合的有效方法。

Something similar could be achieved with sorted sets using NavigableSet.subSet method, or any of the slicing methods offered there.使用NavigableSet.subSet方法或那里提供的任何切片方法可以通过排序集实现类似的操作。

Considerations:注意事项:

What method you use might depend on what you are intending to do您使用什么方法可能取决于您打算做什么

  • The collect and removeAl technique works with any Collection (Collection, List, Set, etc). collect 和removeAl技术适用于任何 Collection(Collection、List、Set 等)。
  • The ListIterator technique obviously only works with lists, provided that their given ListIterator implementation offers support for add and remove operations. ListIterator技术显然只适用于列表,前提是它们给定的ListIterator实现提供了对添加和删除操作的支持。
  • The Iterator approach would work with any type of collection, but it only supports remove operations. Iterator方法适用于任何类型的集合,但它只支持删除操作。
  • With the ListIterator / Iterator approach the obvious advantage is not having to copy anything since we remove as we iterate.使用ListIterator / Iterator方法的明显优势是不必复制任何内容,因为我们在迭代时删除。 So, this is very efficient.所以,这是非常有效的。
  • The JDK 8 streams example don't actually removed anything, but looked for the desired elements, and then we replaced the original collection reference with the new one, and let the old one be garbage collected. JDK 8 流示例实际上并没有删除任何内容,而是查找所需的元素,然后我们用新的元素替换原始收集引用,并让旧的收集引用进行垃圾收集。 So, we iterate only once over the collection and that would be efficient.因此,我们只对集合进行一次迭代,这样会很有效。
  • In the collect and removeAll approach the disadvantage is that we have to iterate twice.在 collect 和removeAll方法中,缺点是我们必须迭代两次。 First we iterate in the foor-loop looking for an object that matches our removal criteria, and once we have found it, we ask to remove it from the original collection, which would imply a second iteration work to look for this item in order to remove it.首先,我们在 for 循环中迭代,寻找符合我们删除标准的 object,一旦找到它,我们要求将其从原始集合中删除,这意味着第二次迭代工作以查找该项目以去掉它。
  • I think it is worth mentioning that the remove method of the Iterator interface is marked as "optional" in Javadocs, which means that there could be Iterator implementations that throw UnsupportedOperationException if we invoke the remove method.我认为值得一提的是, Iterator接口的 remove 方法在 Javadocs 中被标记为“可选”,这意味着如果我们调用 remove 方法,可能会有Iterator实现抛出UnsupportedOperationException As such, I'd say this approach is less safe than others if we cannot guarantee the iterator support for removal of elements.因此,如果我们不能保证迭代器支持删除元素,我会说这种方法不如其他方法安全。

Old Timer Favorite (it still works):老计时器最爱(它仍然有效):

 List<String> list; for(int i = list.size() - 1; i >= 0; --i) { if(list.get(i).contains("bad")) { list.remove(i); } }

Benefits:好处:

  1. It only iterates over the list once它只遍历列表一次
  2. No extra objects created, or other unneeded complexity没有创建额外的对象或其他不需要的复杂性
  3. No problems with trying to use the index of a removed item, because... well, think about it尝试使用已删除项目的索引没有问题,因为...好吧,考虑一下

In Java 8, there is another approach.在 Java 8 中,还有另一种方法。 Collection#removeIf 收藏#removeIf

eg:例如:

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

Are there any reasons to prefer one approach over the other是否有任何理由偏爱一种方法而不是另一种方法

The first approach will work, but has the obvious overhead of copying the list.第一种方法可以工作,但复制列表的开销明显。

The second approach will not work because many containers don't permit modification during iteration.第二种方法不起作用,因为许多容器不允许在迭代期间进行修改。 This includes ArrayList . 这包括ArrayList

If the only modification is to remove the current element, you can make the second approach work by using itr.remove() (that is, use the iterator 's remove() method, not the container 's).如果唯一的修改是删除当前元素,则可以使用itr.remove()使第二种方法起作用(即,使用迭代器remove()方法,而不是容器的)。 This would be my preferred method for iterators that support remove() .对于支持remove()的迭代器,这将是我的首选方法。

Only second approach will work.只有第二种方法可行。 You can modify collection during iteration using iterator.remove() only.您只能在迭代期间使用iterator.remove()修改集合。 All other attempts will cause ConcurrentModificationException .所有其他尝试将导致ConcurrentModificationException

You can't do the second, because even if you use the remove() method on Iterator , you'll get an Exception thrown .你不能做第二个,因为即使你在Iterator上使用remove()方法, 你也会得到一个 Exception throw

Personally, I would prefer the first for all Collection instances, despite the additional overheard of creating the new Collection , I find it less prone to error during edit by other developers.就个人而言,我更喜欢所有Collection实例的第一个,尽管额外无意中听到了创建新Collection ,但我发现它在其他开发人员编辑期间不太容易出错。 On some Collection implementations, the Iterator remove() is supported, on other it isn't.在某些 Collection 实现中,支持 Iterator remove() ,而在另一些实现中则不支持。 You can read more in the docs for Iterator .您可以在Iterator的文档中阅读更多内容。

The third alternative, is to create a new Collection , iterate over the original, and add all the members of the first Collection to the second Collection that are not up for deletion.第三种选择是创建一个新的Collection ,遍历原始集合,并将第一个Collection的所有成员添加到第二个Collection中,这些成员不会被删除。 Depending on the size of the Collection and the number of deletes, this could significantly save on memory, when compared to the first approach.根据Collection的大小和删除的数量,与第一种方法相比,这可以显着节省 memory。

I would choose the second as you don't have to do a copy of the memory and the Iterator works faster.我会选择第二个,因为您不必复制 memory 并且迭代器的工作速度更快。 So you save memory and time.因此,您节省了 memory 和时间。

There is also a simple solution to "iterate" a Collection and removing each items. 还有一个简单的解决方案可以“迭代” Collection并删除每个项目。

List<String> list = new ArrayList<>();
//Fill the list

It simply conciste on looping until the list is empty, and on each iteration, we remove the first element with remove(0) . 它只是简单地循环直到列表为空,然后在每次迭代中,我们使用remove(0)删除第一个元素。

while(!list.isEmpty()){
    String s = list.remove(0);
    // do you thing
}

I don't believe this has any improvement compare to the Iterator , it still required to have a mutable list but I like the simplicity of this solution. Iterator相比,我认为这没有任何改进,它仍然需要具有可变列表,但是我喜欢此解决方案的简单性。

You can see this sample;你可以看到这个样本; If we think remove odd value from a list:如果我们认为从列表中删除奇数值:

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

why not this?为什么不是这个?

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

And if it's a map, not a list, you can use keyset()如果是 map,而不是列表,则可以使用 keyset()

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

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