ChatGPT解决这个技术问题 Extra ChatGPT

遍历集合,在循环中删除对象时避免 ConcurrentModificationException

我们都知道,由于 ConcurrentModificationException,您无法执行以下操作:

for (Object i : l) {
    if (condition(i)) {
        l.remove(i);
    }
}

但这显然有时有效,但并非总是如此。下面是一些具体的代码:

public static void main(String[] args) {
    Collection<Integer> l = new ArrayList<>();

    for (int i = 0; i < 10; ++i) {
        l.add(4);
        l.add(5);
        l.add(6);
    }

    for (int i : l) {
        if (i == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

当然,这会导致:

Exception in thread "main" java.util.ConcurrentModificationException

即使多个线程没有这样做。反正。

这个问题的最佳解决方案是什么?如何在不引发此异常的情况下循环从集合中删除项目?

我在这里也使用了任意的 Collection,不一定是 ArrayList,因此您不能依赖 get

读者注意:请阅读docs.oracle.com/javase/tutorial/collections/interfaces/…,它可能有更简单的方法来实现您想要做的事情。

R
Raedwald

Iterator.remove() 是安全的,您可以这样使用它:

List<String> list = new ArrayList<>();

// This is a clever way to create the iterator and call iterator.hasNext() like
// you would do in a while-loop. It would be the same as doing:
//     Iterator<String> iterator = list.iterator();
//     while (iterator.hasNext()) {
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        // Remove the current element from the iterator and the list.
        iterator.remove();
    }
}

请注意,Iterator.remove() 是在迭代期间修改集合的唯一安全方法;如果在迭代过程中以任何其他方式修改了底层集合,则行为未指定。

来源: docs.oracle > The Collection Interface

同样,如果您有 ListIterator 并且想要添加项,则可以使用 ListIterator#add,出于同样的原因,您可以使用 Iterator#remove - 它旨在允许它。

在您的情况下,您尝试从列表中删除,但如果在迭代其内容时尝试 put 进入 Map,则同样的限制适用。


如果要删除当前迭代中返回的元素以外的元素怎么办?
您必须在迭代器中使用 .remove 并且只能删除当前元素,所以没有:)
请注意,与使用 ConcurrentLinkedDeque 或 CopyOnWriteArrayList 相比,这要慢一些(至少在我的情况下)
不能将 iterator.next() 调用放在 for 循环中吗?如果没有,有人可以解释为什么吗?
@GonenI 它是为来自不可变集合的所有迭代器实现的。 List.add 在同样的意义上也是“可选的”,但您不会说添加到列表中是“不安全的”。
L
Lii

这有效:

Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
    if (iter.next() == 5) {
        iter.remove();
    }
}

我假设由于 foreach 循环是迭代的语法糖,因此使用迭代器无济于事……但它为您提供了 .remove() 功能。


foreach 循环是迭代的语法糖。但是,正如您所指出的,您需要在迭代器上调用 remove - foreach 不允许您访问。因此,您无法在 foreach 循环中删除的原因(即使您实际上在后台使用了迭代器)
+1 例如在上下文中使用 iter.remove() 的代码,Bill K 的回答[直接]没有。
L
Lii

对于 Java 8,您可以使用 the new removeIf method。应用于您的示例:

Collection<Integer> coll = new ArrayList<>();
//populate

coll.removeIf(i -> i == 5);

喔喔!我希望 Java 8 或 9 中的某些东西可能会有所帮助。这对我来说似乎仍然很冗长,但我仍然喜欢它。
在这种情况下是否也推荐实施 equals() ?
顺便说一下 removeIf 使用 Iteratorwhile 循环。你可以在 java 8 java.util.Collection.java 看到它
@omerhakanbilici 出于性能原因,某些实现(例如 ArrayList)会覆盖它。您所指的只是默认实现。
@AnmolGupta:不,这里根本没有使用 equals,因此不必实现它。 (当然,如果您在测试中使用 equals,那么它必须按照您想要的方式实现。)
a
akhil_mittal

由于问题已经得到解答,即最好的方法是使用迭代器对象的 remove 方法,我将详细介绍引发错误 "java.util.ConcurrentModificationException" 的位置。

每个集合类都有一个私有类,它实现了 Iterator 接口并提供了 next()remove()hasNext() 等方法。

next 的代码看起来像这样......

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

这里方法 checkForComodification 实现为

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

因此,如您所见,如果您明确尝试从集合中删除一个元素。它导致 modCountexpectedModCount 不同,从而导致异常 ConcurrentModificationException


很有意思。谢谢!我自己经常不调用 remove() ,而是喜欢在迭代后清除集合。并不是说这是一个很好的模式,只是我最近一直在做的事情。
M
MC Emperor

您可以像您提到的那样直接使用迭代器,或者保留第二个集合并将要删除的每个项目添加到新集合中,然后在最后 removeAll 。这允许您以增加内存使用和 cpu 时间为代价继续使用 for-each 循环的类型安全(这不应该是一个大问题,除非您有非常非常大的列表或非常旧的计算机)

public static void main(String[] args)
{
    Collection<Integer> l = new ArrayList<Integer>();
    Collection<Integer> itemsToRemove = new ArrayList<>();
    for (int i=0; i < 10; i++) {
        l.add(Integer.of(4));
        l.add(Integer.of(5));
        l.add(Integer.of(6));
    }
    for (Integer i : l)
    {
        if (i.intValue() == 5) {
            itemsToRemove.add(i);
        }
    }

    l.removeAll(itemsToRemove);
    System.out.println(l);
}

这是我通常做的,但显式迭代器是我觉得更优雅的解决方案。
很公平,只要您不使用迭代器做任何其他事情 - 暴露它可以更容易地执行诸如每次循环调用 .next() 两次等操作。这不是一个大问题,但如果您这样做可能会导致问题任何比通过列表删除条目更复杂的事情。
@RodeoClown:在最初的问题中,Claudiu 正在从集合中移除,而不是从迭代器中移除。
从迭代器中删除会从底层集合中删除......但我在最后一条评论中所说的是,如果你做的事情比使用迭代器在循环中查找删除(比如处理正确的数据)更复杂,可以做一些更容易犯错误。
如果它是一个不需要的简单删除值并且循环只做一件事,那么直接使用迭代器并调用 .remove() 绝对没问题。
L
Landei

在这种情况下,一个常见的技巧是(曾经?)倒退:

for(int i = l.size() - 1; i >= 0; i --) {
  if (l.get(i) == 5) {
    l.remove(i);
  }
}

也就是说,我很高兴您在 Java 8 中有更好的方法,例如流上的 removeIffilter


这是一个很好的技巧。但它不适用于像集合这样的非索引集合,而且在链表上它会非常慢。
@Claudiu 是的,这绝对仅适用于 ArrayList 或类似的收藏。
我正在使用一个 ArrayList,这很好用,谢谢。
索引很棒。如果它很常见,为什么不使用 for(int i = l.size(); i-->0;) {
C
Community

Claudius 相同的答案,带有 for 循环:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}

D
Donald Raab

使用 Eclipse Collections,在 MutableCollection 上定义的方法 removeIf 将起作用:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.lessThan(3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

使用 Java 8 Lambda 语法,可以这样写:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.cast(integer -> integer < 3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

此处需要调用 Predicates.cast(),因为在 Java 8 的 java.util.Collection 接口上添加了默认的 removeIf 方法。

注意:我是 Eclipse Collections 的提交者。


P
Priyank Doshi

制作现有列表的副本并迭代新副本。

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}

制作副本听起来像是在浪费资源。
@Antzi这取决于列表的大小和其中对象的密度。仍然是一个有价值且有效的解决方案。
我一直在使用这种方法。它需要更多的资源,但更加灵活和清晰。
当您不打算删除循环本身内部的对象时,这是一个很好的解决方案,但是它们是从其他线程中“随机”删除的(例如,网络操作更新数据)。如果您发现自己经常做这些副本,甚至有一个 java 实现就是这样做的:docs.oracle.com/javase/8/docs/api/java/util/concurrent/…
制作列表的副本是他们通常在 Android 上对侦听器所做的事情。这是小列表的有效解决方案。
M
Miss Chanandler Bong

人们断言无法从 foreach 循环迭代的 Collection 中删除。我只是想指出这在技术上是不正确的并准确地描述了(我知道 OP 的问题是如此先进以至于避免知道这一点)该假设背后的代码:

for (TouchableObj obj : untouchedSet) {  // <--- This is where ConcurrentModificationException strikes
    if (obj.isTouched()) {
        untouchedSet.remove(obj);
        touchedSt.add(obj);
        break;  // this is key to avoiding returning to the foreach
    }
}

并不是说您不能从迭代的 Colletion 中删除,而是您一旦这样做就不能继续迭代。因此,上面代码中的 break

抱歉,如果这个答案是一个有点专业的用例并且更适合我从这里到达的原始 thread,那个被标记为重复(尽管这个线程看起来更细微)并被锁定。


f
from56

使用传统的 for 循环

ArrayList<String> myArray = new ArrayList<>();

for (int i = 0; i < myArray.size(); ) {
    String text = myArray.get(i);
    if (someCondition(text))
        myArray.remove(i);
    else
        i++;   
}

啊,所以它实际上只是引发异常的增强循环。
FWIW - 在循环保护而不是循环主体中修改为增加 i++ 后,相同的代码仍然可以工作。
更正^:也就是说,如果 i++ 递增不是有条件的 - 我现在明白这就是你在正文中这样做的原因:)
Y
Yessy

ConcurrentHashMapConcurrentLinkedQueueConcurrentSkipListMap 可能是另一种选择,因为它们永远不会抛出任何 ConcurrentModificationException,即使您删除或添加项目也是如此。


是的,请注意,这些都在 java.util.concurrent 包中。该包中的其他一些类似/常见用例类是 CopyOnWriteArrayList & CopyOnWriteArraySet [但不限于这些]。
实际上,我刚刚了解到,虽然那些数据结构 Objects avoid ConcurrentModificationException,但在 enhanced-for-loop 中使用它们仍然会导致索引问题(即:仍然跳过元素, 或 IndexOutOfBoundsException...)
N
Nestor Milyaev

另一种方法是使用 arrayList 的副本仅用于迭代:

List<Object> l = ...
    
List<Object> iterationList = ImmutableList.copyOf(l);
    
for (Object curr : iterationList) {
    if (condition(curr)) {
        l.remove(curr);
    }
}

注意:i 不是 index,而是对象。也许称它为 obj 会更合适。
早在 2012 年就已经在上面提出了建议:stackoverflow.com/a/11201224/3969362复制列表是他们通常对 Android 上的侦听器所做的事情。这是小列表的有效解决方案。
j
james.garriss

ListIterator 允许您添加或删除列表中的项目。假设您有 Car 个对象的列表:

List<Car> cars = ArrayList<>();
// add cars here...

for (ListIterator<Car> carIterator = cars.listIterator();  carIterator.hasNext(); )
{
   if (<some-condition>)
   { 
      carIterator().remove()
   }
   else if (<some-other-condition>)
   { 
      carIterator().add(aNewCar);
   }
}

ListIterator 接口(Iterator 的扩展)中的附加方法很有趣——尤其是它的 previous 方法。
p
pedram bashiri

我知道这个问题对于 Java 8 来说太老了,但是对于那些使用 Java 8 的人来说,您可以轻松地使用 removeIf():

Collection<Integer> l = new ArrayList<Integer>();

for (int i=0; i < 10; ++i) {
    l.add(new Integer(4));
    l.add(new Integer(5));
    l.add(new Integer(6));
}

l.removeIf(i -> i.intValue() == 5);

A
Adil Karaöz

现在,您可以使用以下代码删除

l.removeIf(current -> current == 5);

y
yoAlex5

Java 并发修改异常

单线程

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String value = iter.next()
    if (value == "A") {
        list.remove(it.next()); //throws ConcurrentModificationException
    }
}

解决方案:迭代器remove()方法

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String value = iter.next()
    if (value == "A") {
        it.remove()
    }
}

多线程

复制/转换并迭代另一个集合。对于小型收藏

同步[关于]

线程安全集合[关于]


一个简洁但更全面的答案。
您的第一个示例不等于您的第二个示例或 OP 的代码。
s
svick

对于上面的问题,我有一个建议。不需要二级名单或任何额外的时间。请找到一个例子,它会以不同的方式做同样的事情。

//"list" is ArrayList<Object>
//"state" is some boolean variable, which when set to true, Object will be removed from the list
int index = 0;
while(index < list.size()) {
    Object r = list.get(index);
    if( state ) {
        list.remove(index);
        index = 0;
        continue;
    }
    index += 1;
}

这将避免并发异常。


该问题明确指出,OP 没有必要使用 ArrayList,因此不能依赖 get()。不过,否则可能是一个好方法。
(澄清 ^)OP 使用的是任意的Collection - Collection 接口不包括 get。 (虽然 FWIW List 接口确实包含“get”)。
我刚刚在这里添加了一个单独的、更详细的答案,也用于 while-循环 List。但是这个答案+1,因为它排在第一位。
S
Srinivasan Thoyyeti
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

如果您跳过内部 iterator.next() 调用,则捕获是从列表中删除元素之后。它仍然有效!虽然我不建议编写这样的代码,但它有助于理解它背后的概念 :-)

干杯!


Y
Yazon2006

线程安全集合修改示例:

public class Example {
    private final List<String> queue = Collections.synchronizedList(new ArrayList<String>());

    public void removeFromQueue() {
        synchronized (queue) {
            Iterator<String> iterator = queue.iterator();
            String string = iterator.next();
            if (string.isEmpty()) {
                iterator.remove();
            }
        }
    }
}

R
Rahul Vala

一种解决方案可能是旋转列表并删除第一个元素以避免 ConcurrentModificationException 或 IndexOutOfBoundsException

int n = list.size();
for(int j=0;j<n;j++){
    //you can also put a condition before remove
    list.remove(0);
    Collections.rotate(list, 1);
}
Collections.rotate(list, -1);

O
Oleg Tatarchuk

试试这个(删除列表中等于 i 的所有元素):

for (Object i : l) {
    if (condition(i)) {
        l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
    }
}

O
Oguzhan Cevik

您可以使用 while 循环。

Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while(iterator.hasNext()){
    Map.Entry<String, String> entry = iterator.next();
    if(entry.getKey().equals("test")) {
        iterator.remove();
    } 
}

这里的重点不是 while 循环,而是通过 Iterator. 删除
A
Alferd Nobel

我最终得到了这个 ConcurrentModificationException,同时使用 stream().map() 方法迭代列表。但是,for(:) 在迭代和修改列表时没有引发异常。

这是代码片段,如果它对任何人都有帮助:这里我正在迭代 ArrayList<BuildEntity> ,并使用 list.remove(obj) 对其进行修改

 for(BuildEntity build : uniqueBuildEntities){
            if(build!=null){
                if(isBuildCrashedWithErrors(build)){
                    log.info("The following build crashed with errors ,  will not be persisted -> \n{}"
                            ,build.getBuildUrl());
                    uniqueBuildEntities.remove(build);
                    if (uniqueBuildEntities.isEmpty()) return  EMPTY_LIST;
                }
            }
        }
        if(uniqueBuildEntities.size()>0) {
            dbEntries.addAll(uniqueBuildEntities);
        }

S
SM. Hosseini

如果使用 HashMap,在较新版本的 Java (8+) 中,您可以选择 3 个选项中的每一个:

public class UserProfileEntity {
    private String Code;
    private String mobileNumber;
    private LocalDateTime inputDT;
    // getters and setters here
}
HashMap<String, UserProfileEntity> upMap = new HashMap<>();


// remove by value
upMap.values().removeIf(value -> !value.getCode().contains("0005"));

// remove by key
upMap.keySet().removeIf(key -> key.contentEquals("testUser"));

// remove by entry / key + value
upMap.entrySet().removeIf(entry -> (entry.getKey().endsWith("admin") || entry.getValue().getInputDT().isBefore(LocalDateTime.now().minusMinutes(3)));

u
user207421

最好的方法(推荐)是使用 java.util.concurrent 包。通过使用此包,您可以轻松避免此异常。参考修改代码:

public static void main(String[] args) {
    Collection<Integer> l = new CopyOnWriteArrayList<Integer>();
    
    for (int i=0; i < 10; ++i) {
        l.add(new Integer(4));
        l.add(new Integer(5));
        l.add(new Integer(6));
    }
    
    for (Integer i : l) {
        if (i.intValue() == 5) {
            l.remove(i);
        }
    }
    
    System.out.println(l);
}

您是否考虑到性能损失?每次你“写入”这个结构时,它的内容都会被复制到一个新对象中。所有这些都对性能不利。
这不是最好的方法,也不推荐。不要对未引用的文本使用引用格式。如果被引用,请提供引用。
N
Nurlan

如果 ArrayList:remove(int index)- if(index is last element's position) 它避免没有 System.arraycopy() 并且不需要时间。

如果(索引减少),数组复制时间会增加,顺便说一下列表的元素也会减少!

最有效的移除方式是 - 按降序移除其元素:while(list.size()>0)list.remove(list.size()-1);//takes O(1) while(list.size()>0)list.remove(0);//takes O(factorial(n))

//region prepare data
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<Integer> toRemove = new ArrayList<Integer>();
Random rdm = new Random();
long millis;
for (int i = 0; i < 100000; i++) {
    Integer integer = rdm.nextInt();
    ints.add(integer);
}
ArrayList<Integer> intsForIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsDescIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsIterator = new ArrayList<Integer>(ints);
//endregion

// region for index
millis = System.currentTimeMillis();
for (int i = 0; i < intsForIndex.size(); i++) 
   if (intsForIndex.get(i) % 2 == 0) intsForIndex.remove(i--);
System.out.println(System.currentTimeMillis() - millis);
// endregion

// region for index desc
millis = System.currentTimeMillis();
for (int i = intsDescIndex.size() - 1; i >= 0; i--) 
   if (intsDescIndex.get(i) % 2 == 0) intsDescIndex.remove(i);
System.out.println(System.currentTimeMillis() - millis);
//endregion

// region iterator
millis = System.currentTimeMillis();
for (Iterator<Integer> iterator = intsIterator.iterator(); iterator.hasNext(); )
    if (iterator.next() % 2 == 0) iterator.remove();
System.out.println(System.currentTimeMillis() - millis);
//endregion

索引循环:1090 毫秒

对于 desc 索引:519 毫秒---最好的

迭代器:1043 毫秒


c
cellepo

我知道这个问题只假设一个 Collection,而不是更具体地说是任何 List。但对于那些阅读此问题并确实使用 List 参考的人,如果您想避免使用 Iterator,则可以使用 while 循环(同时在其中修改)避免 ConcurrentModificationException (如果您想在一般情况下避免它,或者专门避免它以实现不同于在每个元素处从头到尾停止的循环顺序[我相信这是 Iterator 本身可以做的唯一顺序]):

*更新:请参阅下面的评论,阐明类似的情况也可以通过传统的 for-loop 实现。

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 1;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i++);

    } else {
        i += 2;
    }
}

该代码没有 ConcurrentModificationException。

在那里我们看到循环不是从一开始就开始,也不是在 每个 元素处停止(我相信 Iterator 本身不能这样做)。

FWIW 我们还看到 getlist 上被调用,如果它的引用只是 Collection(而不是更具体的 List 类型的 Collection)则无法完成 - List 接口包括 { 1},但 Collection 接口没有。如果不是因为这种差异,那么 list 引用可以改为 Collection [因此从技术上讲,此答案将是直接答案,而不是切线答案]。

FWIWW 相同的代码在修改为从每个元素的开始到停止后仍然有效(就像 Iterator 顺序):

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 0;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i);

    } else {
        ++i;
    }
}

然而,这仍然需要非常仔细地计算要删除的指标。
此外,这只是对这个答案的更详细解释stackoverflow.com/a/43441822/2308683
很高兴知道-谢谢!另一个答案帮助我了解它是 enhanced-for-loop 会抛出 ConcurrentModificationException,但 not traditional-for-loop (另一个答案使用) - 没有意识到之前是我有动力写这个答案的原因(我当时错误地认为是 all for-loops 会抛出异常)。
F
Firas Chebbah

你也可以使用递归

java中的递归是一个方法不断调用自身的过程。 java中调用自身的方法称为递归方法。


a
ajax333221

这可能不是最好的方法,但对于大多数小情况,这应该是可以接受的:

“创建第二个空数组并仅添加您想要保留的那些”

我不记得我是从哪里读到这篇文章的……为了公正起见,我会制作这个维基,希望有人能找到它,或者只是为了不赢得我不应该得到的代表。