282

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

有什么理由更喜欢一种方法而不是另一种(例如,出于可读性的简单原因更喜欢第一种方法)?

4

9 回答 9

540

让我举几个例子和一些替代方案来避免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支持在迭代过程中删除和添加项目的 a。

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

同样,我在上面的示例中使用了“删除”方法,这正是您的问题所暗示的,但您也可以使用它的add方法在迭代期间添加新元素。

使用 JDK >= 8

对于那些使用 Java 8 或更高版本的人,您可以使用其他一些技术来利用它。

您可以在基类中使用新removeIf方法:Collection

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

在最后一种情况下,要从集合中过滤元素,您可以将原始引用重新分配给过滤的集合(ie books = filtered),或者将过滤的集合用于removeAll从原始集合(ie books.removeAll(filtered))中找到的元素。

使用子列表或子集

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

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

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

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

注意事项:

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

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

老计时器最爱(它仍然有效):

List<String> list;

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

好处:

  1. 它只遍历列表一次
  2. 没有创建额外的对象或其他不需要的复杂性
  3. 尝试使用已删除项目的索引没有问题,因为......好吧,考虑一下!
于 2020-03-26T00:45:47.107 回答
18

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

例如:

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

list.removeIf(i -> i > 2);
于 2017-10-04T07:19:48.933 回答
15

是否有任何理由偏爱一种方法而不是另一种方法

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

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

如果唯一的修改是删除当前元素,则可以通过 using 使第二种方法起作用itr.remove()(即,使用iteratorremove()方法,而不是container的方法)。对于支持remove().

于 2012-05-03T13:07:22.387 回答
5

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

于 2012-05-03T13:07:22.640 回答
1

你不能做第二个,因为即使你使用Iteratorremove()上的方法,你也会得到一个 Exception throw

就个人而言,我更喜欢所有Collection实例的第一个,尽管额外无意中听到了创建新的Collection,但我发现它在其他开发人员编辑期间不太容易出错。在某些 Collection 实现中,remove()支持 Iterator,而在其他一些实现中则不支持。您可以在Iterator的文档中阅读更多内容。

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

于 2012-05-03T13:12:17.377 回答
0

我会选择第二个,因为您不必复制内存并且迭代器工作得更快。因此,您可以节省内存和时间。

于 2012-05-03T13:08:27.513 回答
0

你可以看到这个样本;如果我们认为从列表中删除奇数值:

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);
}
于 2021-12-27T10:51:41.063 回答
-3

为什么不是这个?

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

如果它是一个地图,而不是一个列表,你可以使用 keyset()

于 2012-05-03T13:41:17.227 回答