Pull to refresh

Comments 26

<сарказм>
использовать immutable data structures
</сарказм>
ConcurrentModificationException к многопоточности никакого отношения не имеет
Имеет прямое отношение (появился задолго до пакета concurrent), но этот эксепшн также можно получить и без многопоточности.

Нету смысла создавать свои коллекции, чтобы избегать этого эксепшена. В примере со стримами, вы не должны модифицировать коллекцию с которой работаете. Если вам нужна новая коллекция — вызовите коллект метод и получите новый сет:
Set<Integer> filteredSet = set.stream().filter(v -> v % 2 == 0).collect(Collectors.toSet());
или так, со статическим импортом:
Set<Integer> filteredSet = set.stream().filter(v -> v % 2 == 0).collect(toSet());

Если вы не хотите создавать новый сет по причине «экономии памяти», тогда стримы с лямбдами вам и вовсе не стоит использовать. В этом случае используйте for-i цикл или все тот же явный итератор.

ConcurrentModificationException — это не та причина, чтобы создавать еще один велосипед фреймворк коллекций.
Имеет прямое отношение

Нет, всё таки не имеет. Эта штука происходит при наличии итератора и изменениях производимых в том же потоке, о чём подробно написано во введении. Не, наверное, можно извернуться и получить это в многопоточной среде, прямо скажем, очень очень мало кто такое делал.

Нету смысла создавать свои коллекции,

Конечно есть. Как минимум это очень интересно.

В примере со стримами, вы не должны модифицировать коллекцию с которой работаете.
Почему нет-то? А если мне надо? Ну вот есть у меня большая коллекция по ней надо часто итерировать и иногда в процессе итерирования удалять элементы. Лично у меня и правда была такая коллекция. Почему я это не могу сделать? Разработчики на других языках вот могут. Например, JS разработчикам ничто не мешает итерировать по их джаваскриптовым сетам и мапам и одновременно менять их, а у нас тут какой-то анахронизм, мне обидно вот.

Как ешё бывает, в процессе обхода элемент отдаётся какому-то другому методу и уже он решает что делать. Это довольно удобно когда он может принять решение и сам удалить элемент.

ConcurrentModificationException — это не та причина, чтобы создавать еще один велосипед фреймворк коллекций.
Вы так категоричны, но почему нет? Программист не состоялся как программист если не написал свой фреймворк. Видели, сколько фреймворков уже понаписано? Их просто тьма. Ну и как показано в статье, ничто в общем-то не мешает стандартным LinkedHashSet и LinkedHashMap перестать кидаться такими эксепшенами. У них всё для этого уже есть. И это даже обратную совместимость не нарушит.
Не, наверное, можно извернуться и получить это в многопоточной среде, прямо скажем, очень очень мало кто такое делал.
Между Java 1.2 (ConcurrentModificationException) и Java 1.5 (появление пакета канкарренси и цикла foreach) прошло ~6 лет. Все это время обычные ArrayList и HashMap использовали в многопоточной среде. Если выполнялись действия без синхронизации, этот эксепшен говорил про то, что в программе есть ошибка.
В целом и сейчас это говорит про ошибку, но не обязательно ошибку синхронизации.

Почему нет-то? А если мне надо?
Потому, что это идеологически не правильно. Стримы — это функциональный подход, который предполагает неизменяемость данных и отсутствие побочных эффектов. forEach — метод, который позволяет сделать произвольные действия (побочные эффекты), но над Элементом коллекции, а не над коллекцией. Если Вы в этом методе изменяете коллекцию — Вы сами стреляете себе в ногу.
Если нужно — посмотрите в комментариях, как это делается правильно.

JS разработчикам ничто не мешает итерировать по их джаваскриптовым сетам и мапам и одновременно менять их, а у нас тут какой-то анахронизм, мне обидно вот
Стоит отметить, что в js нету многопоточности и такой проблемы не может быть в принципе. К слову, в js это тоже плохая практика, JS девелоперы Вам первые скажут, что Вы не должны изменять коллекцию через forEach =)
Между Java 1.2 (ConcurrentModificationException) и Java 1.5 (появление пакета канкарренси и цикла foreach) прошло ~6 лет. Все это время обычные ArrayList и HashMap использовали в многопоточной среде. Если выполнялись действия без синхронизации, этот эксепшен говорил про то, что в программе есть ошибка.
В целом и сейчас это говорит про ошибку, но не обязательно ошибку синхронизации.
Это теория, на практике всё несколько иначе, как мне кажется. Они, конечно, использовались в многопоточной среде. Вот только чтоб получить ConcurrentModificationException нам нужен итератор, а итератор не thread-safe. Он не thread-safe даже если мы обернули нашу коллекцию в synchronized декоратор. А значит использование итератора в многопоточной среде выглядит как-то так:

synchronized (sychronizedSet) {
   Iterator i = sychronizedSet.iterator();
   while (i.hasNext()) {
      process(i.next());
   }
}

Тут у нас мьютекс, который не даст никакому другому треду поменять коллекцию, и ConcurrentModificationException не случится, чтоб его добиться это надо использовать wait/notify на synchonizedSet да ещё и при наличии итератора. Всё вместе это маловероятно и маловостребованно. В общем, если у вас есть популярный use-case, я с удовольствием на него взгляну. В однопоточной же среде ConcurrentModificationException получается на раз-два.
Я об этом и говорю. До появления канкарренси пакета в 2004 в java были только классические коллекции, у которых нету синхронизации. Написать многопоточное приложение на этих коллекциях требовало синхронизации по какому-то объекту, не обязательно по самой коллекции.

И если кто-то не сделал синхронизацию на нужном объекте, то данный эксепшн показывал, что в системе есть ошибка.

Не забывайте, что java коллекциям уже 19 лет. И тот факт, что публичный API нельзя просто взять и изменить.
Не, данный эксепшен совсем не об этом, а если доступ к коллекции в многопоточной среде осуществляется не в thead-safe манере, как вы предлагаете, то там может быть вообще черте что помимо ConcurrentModificationException, а ConcurrentModificationException может и вовсе не быть, ни что в этом случае не гарантирует что значение поля modCount в треде осуществляющем небезопасный доступ актуально, а значит не гарантирует и выбрасывание СoncurrentModificationException.

Чтобы понять роль данного эксепшена имеет смысл попробовать самому написать примитивный ArrayList, с парочкой мутаторов (add, remove по индексу) и итератором. Вы почти сразу наступите на туже проблему что и разработчики java collection framework и почувствуете откуда берется ConcurrentModificationException и какова его настоящая роль. Всё вместе это у вас займет минут 10 примерно.

И тот факт, что публичный API нельзя просто взять и изменить.

При предложенных изменениях обратная совместимость не нарушается, подобным образом публичный API менялся и не раз.
Потому, что это идеологически не правильно.
Вы думаете, есть принципиальная разница между forEach циклом и forEach методом на стримах? Я и там и там могу делегировать обработку элемента какому-то методу, который в своих глубинах решит удалить элемент. Там этот метод в глубине стека вызовов понятия не будет иметь ни о наличии стрима ни о наличии forEach цикла. И тем не менее разработчику этого метода наличие итератора выше по стеку придется принимать во внимание, хотя идеологически правильнее, чтобы логика метода могла разрабатываться без учета наличия итераторов в контексте вызова.

Стоит отметить, что в js нету многопоточности и такой проблемы не может быть в принципе. К слову, в js это тоже плохая практика, JS девелоперы Вам первые скажут, что Вы не должны изменять коллекцию через forEach =)
Ещё раз, в java ConcurrentModificationException отлично случается в одном потоке. Только там и случается. Это самый популярный способ его воспроизвести на который многие наступали. На самом деле в джаваскрипте эта проблема вполне могла бы быть, в реализациях map-ов и set-ов без использования связного списка (как это сделано в HashMap и HashSet) при наличии итератора необходимо запрещать изменения вызовами методов коллекции. Просто в javascript грамотно спроектировали свои коллекции, что помешало сделать разработчикам джава платформы тоже самое разрабатывая LinkedHashMap и LinkedHashSet — совершенно не понятно.
>Нету смысла создавать свои коллекции, чтобы избегать этого эксепшена.

Не, ну вообще-то автор дальше детально пояснил, чего он на самом деле хочет. Он хочет параллельно итерироваться и модифицировать коллекцию. Вполне законное желание. Другое дело, что без транзакционности в том или оном виде все равно всех проблем предложенный вариант не решает — но это уже боюсь не лечится совсем, и мы это прекрасно видим на базах данных — универсального решения нет, есть много для разных случаев.
Так а в чем проблема использовать для этой задачи итераторы?
Да, код с foreach красивее немного, но суть цикла foreach совсем в другом.

foreach – это механизм для работы с элементами коллекции, а не с самой коллекцией. Использование foreach для модификации коллекции – это уже не правильно. А писать костыли и велосипеды, чтобы «расширить возможности» конструкции языка – и вовсе плохая затея. Этот подход сломает возможность заменить реализацию А на реализацию Б без танцев с бубном и переписывания всего кода, где коллекция модифицировалась в foreach.

ps: вот один из классических подходов работы с итераторами:
for (Iterator<Integer> it = set.iterator(); it.hasNext(); ) {
  if (it.next() % 2 == 0) {
    it.remove();
  }
}
collection.removeIf(element -> iDontLikeThisElement(element))

— и велосипеды больше не нужны
Кстати хороший поинт. Но бывает, что решение об удалении происходит в глубине цепочки вызовов, и т.е. надо тащить обратно этот флаг, ну и вообще те методы могут хотеть возвращать что-то своё.
Set set2 = set.stream().filter(v -> v % 2 == 0).peek(set::remove).collect(Collectors.toSet());

А если вот так?
UFO just landed and posted this here
неа, не будет эсепшена, не для того мы в данной статье разрабатывали коллекцию свободную от ConcurrentModificationException
Может это хвост? tail?
 private LinkedElement<E> head = placeholder;
Почему хвост? Вроде всё же голова. В начале они, конечно, совпадают, когда коллекция пуста.
В этой статье приняты следующие обозначения: итерируем от головы к хвосту (placeholder идет сразу после хвоста) переходя от предыдущего (prev) элемента к следующему (next), добавляются элементы в хвост.

На картинке: голова слева, а хвост — справа.

Но сами понимаете, это названия сути не меняют, если вам удобнее итерировать от хвоста к голове, а добавлять элементы в голову, то просто переименуйте так как вам удобно.
Твой placeholder идет сразу после головы, и элементы добавляются в голову т.е. в placeholder вообще-то.
LinkedSet<Integer> ls = new LinkedSet<>();
        ls.add(0);
        ls.add(1);
        ls.add(2);
        ls.add(3);
        ls.add(4);
        System.out.println(ls.head.prev); null
        System.out.println(ls.head.next); LinkedSet$LinkedElement@140e19d

        System.out.println(ls.placeholder.prev); LinkedSet$LinkedElement@17327b6
        System.out.println(ls.placeholder.next); null
Эммм не понял в чём проблема. Я исходил из того что голова в начале т.е. слева, а хвост в конце т.е. справа., добавалем в хвост, итерируем с головы. Все имена и обозночения соответсвуют этой идее кажется.
Концептуально это не правильно всё таки. В смысле на картинке одно, а у тебя в коде другое.
System.out.println(ls.head.prev); null
У тебя если пройти по голове нет ссылки на предыдущий! А на следующий есть!
Так ведь нет у хеда предыдущего. Только следующий. Потому ls.head.prev is null.
И итерируешся ты с хвоста в голову. Так?
Ну мне приятно думать, что я итерирую от головы к хвосту. Но это всего лишь названия, так? Если назвать их наоборот, то получтся что итератор бежит от хвоста к голове
Sign up to leave a comment.

Articles