Итерация по коллекции, избегая исключения ConcurrentModificationException при удалении объектов в цикле
Мы все знаем, что вы не можете сделать это:
for (Object i : l) {
if (condition(i)) {
l.remove(i);
}
}
ConcurrentModificationException
и т.д... это, видимо, иногда работает, но не всегда. Вот некоторый конкретный код:
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
,
35 ответов
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> Интерфейс коллекции
И точно так же, если у вас есть ListIterator
и хотите добавить предметы, вы можете использовать ListIterator#add
по той же причине вы можете использовать Iterator#remove
- это разработано, чтобы позволить это.
В вашем случае вы пытались удалить из списка, но при попытке put
в Map
перебирая его содержание.
Это работает:
Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
if (iter.next() == 5) {
iter.remove();
}
}
Я предположил, что, поскольку цикл foreach является синтаксическим сахаром для итерации, использование итератора не поможет... но он дает вам это .remove()
функциональность.
Поскольку на этот вопрос уже дан ответ, т. Е. Лучше всего использовать метод удаления объекта итератора, я бы подробно остановился на том месте, где произошла ошибка. "java.util.ConcurrentModificationException"
брошен
Каждый класс коллекции имеет закрытый класс, который реализует интерфейс Iterator и предоставляет такие методы, как next()
, remove()
а также hasNext()
,
Код для следующего выглядит примерно так...
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();
}
Итак, как вы можете видеть, если вы явно пытаетесь удалить элемент из коллекции. Это приводит к modCount
отличаться от expectedModCount
, что приводит к исключению ConcurrentModificationException
,
Вы можете использовать итератор, как вы упомянули, или оставить вторую коллекцию и добавить каждый элемент, который вы хотите удалить, в новую коллекцию, а затем удалить все в конце. Это позволяет вам продолжать использовать безопасность типов для каждого цикла за счет увеличения использования памяти и времени процессора (не должно быть большой проблемой, если у вас нет действительно очень больших списков или действительно старого компьютера)
public static void main(String[] args)
{
Collection<Integer> l = new ArrayList<Integer>();
Collection<Integer> itemsToRemove = 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));
}
for (Integer i : l)
{
if (i.intValue() == 5)
itemsToRemove.add(i);
}
l.removeAll(itemsToRemove);
System.out.println(l);
}
В таких случаях обычная хитрость - это (было?) Идти назад:
for(int i = l.size() - 1; i >= 0; i --) {
if (l.get(i) == 5) {
l.remove(i);
}
}
Тем не менее, я более чем счастлив, что у вас есть лучшие способы в Java 8, например removeIf
или же filter
на потоках.
Тот же ответ, что и у Клавдия с циклом for:
for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
Object object = it.next();
if (test) {
it.remove();
}
}
С Eclipse Collections (ранее GS Collections), метод removeIf
определенный на MutableCollection будет работать:
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()
здесь необходимо, потому что по умолчанию removeIf
метод был добавлен на java.util.Collection
интерфейс в Java 8.
Примечание: я являюсь коммиттером для Eclipse Collections.
Сделайте копию существующего списка и переберите новую копию.
for (String str : new ArrayList<String>(listOfStr))
{
listOfStr.remove(/* object reference or index */);
}
Люди утверждают, что нельзя удалить из коллекции, повторяемой циклом foreach. Я просто хотел указать, что это технически неверно, и точно описать (я знаю, что вопрос ОП настолько сложен, чтобы избежать знания этого) код, лежащий в основе этого предположения:
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
в коде выше.
Извините, если этот ответ является несколько специализированным вариантом использования и больше подходит для исходного потока, из которого я прибыл сюда, что он помечен как дубликат (несмотря на то, что этот поток выглядит более нюансированным) этого и заблокирован.
С традиционным для цикла
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++;
}
ConcurrentHashMap или ConcurrentLinkedQueue или ConcurrentSkipListMap могут быть другой опцией, потому что они никогда не сгенерируют исключение ConcurrentModificationException, даже если вы удалите или добавите элемент.
Другой способ - создать копию вашего arrayList:
List<Object> l = ...
List<Object> iterationList = ImmutableList.copyOf(l);
for (Object i : iterationList) {
if (condition(i)) {
l.remove(i);
}
}
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);
}
}
Теперь вы можете удалить с помощью следующего кода
l.removeIf(current -> current == 5);
Исключение одновременной модификации
- Один поток
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String value = iter.next()
if (value == "A") {
//throws ConcurrentModificationException
list.remove(it.next());
}
}
Решение: итератор
remove()
метод
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String value = iter.next()
if (value == "A") {
it.remove()
}
}
- Многопотоковый
- копировать / конвертировать и перебирать еще одну коллекцию. Для небольших коллекций
-
synchronize
[О] - потокобезопасная коллекция [О программе]
Я знаю, что этот вопрос слишком старый, чтобы быть о 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);
Вы можете использовать цикл 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();
}
}
Лучший способ (рекомендуется) - использование пакета 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);
}
У меня есть предложение по проблеме выше. Нет необходимости вторичного списка или дополнительного времени. Пожалуйста, найдите пример, который сделал бы то же самое, но по-другому.
//"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;
}
Это позволит избежать исключения параллелизма.
Одним из решений может быть поворот списка и удаление первого элемента, чтобы избежать исключения 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);
Я закончил с этим
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);
}
for (Integer i : l)
{
if (i.intValue() == 5){
itemsToRemove.add(i);
break;
}
}
Улов будет после удаления элемента из списка, если вы пропустите внутренний вызов iterator.next(). это все еще работает! Хотя я не предлагаю писать такой код, это помогает понять концепцию, стоящую за ним:-)
Ура!
Попробуйте это (удаляет все элементы в списке, которые равны i
):
for (Object i : l) {
if (condition(i)) {
l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
}
}
Пример модификации потока безопасной коллекции:
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();
}
}
}
}
Итераторы не всегда полезны, когда другой поток также изменяет коллекцию. Я пробовал много способов, но потом понял, что обход коллекции вручную намного безопаснее (назад для удаления):
for (i in myList.size-1 downTo 0) {
myList.getOrNull(i)?.also {
if (it == 5)
myList.remove(it)
}
}
При использовании 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)));
В случае ArrayList: remove (int index)- если (index - позиция последнего элемента), то избегает без System.arraycopy()
и не требует времени для этого.
время массива увеличивается, если (индекс уменьшается), кстати, элементы списка также уменьшаются!
лучший эффективный способ удаления - удаление его элементов в порядке убывания:while(list.size()>0)list.remove(list.size()-1);
// берет O(1)while(list.size()>0)list.remove(0);
// принимает O(факториал (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 мсек
Я знаю, что этот вопрос предполагает только Collection
а не конкретнее List
, Но для тех, кто читает этот вопрос, кто действительно работает с List
ссылка, вы можете избежать ConcurrentModificationException
с while
-loop (при изменении внутри него) вместо этого, если вы хотите избежать Iterator
(либо если вы хотите избежать этого в целом, либо избегать его специально для достижения порядка зацикливания, отличного от остановки до конца у каждого элемента [который, я считаю, является единственным порядком Iterator
сам могу сделать))
* Обновление: см. Комментарии ниже, которые поясняют, что аналогичное также возможно с традиционным циклом -for.
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 мы также видим get
быть призванным list
что не могло быть сделано, если его ссылка была просто Collection
(вместо более конкретного List
-тип Collection
) - List
интерфейс включает в себя get
, но 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;
}
}
Вы можете перебрать список, используя цикл for, и вам нужно вызвать list.remove(0). Вам нужно жестко закодировать индекс параметром удаления индекса с нуля. Смотрите также этот ответ:
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
int list_size = list.size();
for (int i = 0; i < list_size; i++) {
list.remove(0);
}