迭代时从集合中移除元素
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() }
有没有什么理由比其他方式更喜欢一种方法(例如,由于可读性的原因,宁愿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()