Исключение ConcurrentModificationException отсутствует во время итерации на основе индекса

#java #loops #iteration #concurrentmodification

Вопрос:

У меня есть следующий код:

 public static void main(String[] args) {
 
    List<String> input = new ArrayList<>();
    List<String> output = new ArrayList<>();
    for(int i=0; i< 1000 ;i  ){
        input.add(i "");
    }
    
    
    for(int i=0 ; i<input.size(); i  ){
        String value = input.get(i);
        if(Integer.parseInt(value) % 2 == 0){
            output.add(value);
            input.remove(value);
        }
    }
    
    input.stream().forEach(System.out::println);
    System.out.println("--------------------------------------");
    output.stream().forEach(System.out::println);

}
 

Я ожидал, что он бросит ConcurrentModificationException , но он работает нормально. Может ли кто — нибудь объяснить причину?

Комментарии:

1. » Я ожидал, что он бросит ConcurrentModificationException » почему? Вы не используете итератор явно или неявно с помощью расширенного цикла for при изменении своего списка.

2. Вы ожидали бы этого исключения только в том случае, если бы изменили коллекцию во время ее обхода с помощью итератора или расширенного цикла for. Ваш код не делает ни того, ни другого.

3. @stridecolossus: Тогда почему это не предпочтительный способ зацикливания, когда нам нужно изменить список?

4. @Abhinav вам следует ознакомиться с этой статьей baeldung.com/java-concurrentmodificationexception

5. @Abhinav Я не уверен, что существует предпочтительный способ зацикливания при изменении просматриваемого списка. Как правило, я бы вообще избегал необходимости изменять список, например, создавая 2 результата (для вашего примера) или используя потоки. Параллельный мод тогда не был бы проблемой, и код почти наверняка был бы проще, чем цикл с произвольным доступом.

Ответ №1:

Причина в том, что вы технически не повторяете список. Вместо этого вы произвольно обращаетесь к списку, используя увеличивающийся индекс, и удаляете некоторые значения. Если вы измените код таким образом, чтобы повторить список, он выдаст ConcurrentModificationException

 public static void main(String[] args) {
    List<String> input = new ArrayList<>();
    List<String> output = new ArrayList<>();
    for(int i=0; i< 1000 ;i  ){
        input.add(i "");
    }
    
    for (String value : input) {
        if(Integer.parseInt(value) % 2 == 0){
            output.add(value);
            input.remove(value);
        }
    }

    input.stream().forEach(System.out::println);
    System.out.println("--------------------------------------");
    output.stream().forEach(System.out::println);
}
 

Продолжение о том, почему это может быть не предпочтительным способом по сравнению с итератором. Одна из причин-производительность. Вот некоторый тестовый код, использующий JMH, чтобы проверить это.

 package bench;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static java.util.concurrent.TimeUnit.SECONDS;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 1, time = 3, timeUnit = SECONDS)
@Measurement(iterations = 3, time = 2, timeUnit = SECONDS)
public class JmhBenchmark {
    private List<String> input;

    @Param({"100", "1000", "10000"})
    public int length;

    @Setup(Level.Invocation)
    public void createInputList() {
        input = new ArrayList<>();
        for (int i = 0; i < length; i  ) {
            input.add(i   "");
        }
    }

    @Benchmark
    public void iterateWithVariable() {
        for (int i = 0; i < input.size(); i  ) {
            String value = input.get(i);
            if (Integer.parseInt(value) % 2 == 0) {
                input.remove(value);
            }
        }
    }

    @Benchmark
    public void iterateWithIterator() {
        final Iterator<String> iterator = input.iterator();
        while (iterator.hasNext()) {
            String value = iterator.next();
            if (Integer.parseInt(value) % 2 == 0) {
                iterator.remove();
            }
        }
    }

}
 

Результаты теста в моей системе были

 Benchmark                         (length)  Mode  Cnt   Score    Error  Units
JmhBenchmark.iterateWithIterator       100  avgt   15   0.002 ±  0.001  ms/op
JmhBenchmark.iterateWithIterator      1000  avgt   15   0.033 ±  0.001  ms/op
JmhBenchmark.iterateWithIterator     10000  avgt   15   1.670 ±  0.017  ms/op
JmhBenchmark.iterateWithVariable       100  avgt   15   0.005 ±  0.001  ms/op
JmhBenchmark.iterateWithVariable      1000  avgt   15   0.350 ±  0.014  ms/op
JmhBenchmark.iterateWithVariable     10000  avgt   15  33.591 ±  0.455  ms/op
 

Таким образом, мы можем видеть, что использование итератора для удаления некоторых элементов из списка намного (>20 раз) быстрее, чем подход, заданный этим вопросом. Что имеет смысл, вам нужно выполнить случайный поиск в списке, затем определить, нужно ли его удалять, а затем выполнить другой поиск, чтобы найти и удалить его.

Комментарии:

1. @Kumar Я обновил ответ, включив в него вопрос «почему это не предпочтительный способ зацикливания, когда нам нужно изменить список?».