#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 Я обновил ответ, включив в него вопрос «почему это не предпочтительный способ зацикливания, когда нам нужно изменить список?».