在迭代时从集合中移除元素

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

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

378125 次浏览

有什么理由偏爱其中一种方法而不是另一种吗

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

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

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

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

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

让我举几个例子,用一些替代方法避免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();
}
}

同样,我在上面的例子中使用了“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从原始集合中找到的元素(即books.removeAll(filtered))。

使用子列表或子集

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

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

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

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

注意事项:

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

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

你不能做第二个,因为即使你在迭代器就会抛出一个异常上使用remove()方法。

就我个人而言,对于所有Collection实例,我更喜欢第一个,尽管创建新的Collection有额外的麻烦,但我发现它在其他开发人员编辑时不太容易出错。在一些Collection实现中,支持Iterator remove(),在其他实现中则不支持。你可以在迭代器的文档中阅读更多。

第三种方法是创建一个新的Collection,遍历原来的Collection,并将第一个Collection的所有成员添加到第二个Collection中,这些将被删除。根据Collection的大小和删除的数量,与第一种方法相比,这种方法可以显著节省内存。

为什么不是这个?

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

如果它是映射,而不是列表,你可以使用keyset()

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

例如:

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


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

旧时最爱(现在还能用):

List<String> list;


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

好处:

  1. 它只遍历列表一次
  2. 没有创建额外的对象或其他不必要的复杂性
  3. 尝试使用已删除项的索引没有问题,因为…好吧,想想看!

你可以看到这个例子;如果我们考虑从列表中移除奇数值:

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