Java-итератороподобная конструкция для одновременной модификации карты

Скажем, я делаю что-то вроде:

for (X x : some_map.values ())
    doSomething (x);

где doSomething() косвенно через несколько слоев кода добавляет больше значений к some_map. С итераторами (как в приведенном выше примере кода) мне бросают ConcurrentModificationException в лицо.

Я могу сделать some_map LinkedHashMap, то есть иметь предсказуемый порядок итераций. Кроме того, когда к нему добавляется новый элемент, он всегда добавляется в конце порядка итераций. Другими словами, если ConcurrentModificationException каким-то образом не было выброшено, цикл будет просто перебирать только что добавленные элементы в конце, т. е. будет работать отлично. Или, другими словами, у меня есть параллельная модификация, но я могу гарантировать, что ее поведение четко определено и не является ошибкой.

Вопрос: есть ли что-то "похожее" на итераторы, которые я мог бы использовать в приведенном выше цикле, чтобы избежать исключений одновременного изменения?

Обратите внимание, что из-за некоторых дополнительных ограничений я не могу указать, где добавляются элементы, с учетом цикла. Я также не могу изменить это на что-то, что не является картой. Это всего лишь один фрагмент кода, но some_map также используется в других местах и ​​не просто так является картой.

EDIT: Мой вопрос заключается в том, могу ли я повторять (не обязательно стандартным способом) ту же карту, на которую я добавляю элементы. Очевидно, что вместо этого я могу перебирать копию, после цикла сравнивать указанную копию с оригиналом, чтобы найти новые элементы, перебирать их и так далее. Вопрос в том, могу ли я полностью избежать этого, потому что в моем случае единственная проблема заключается в чрезмерном бросании ConcurrentModificationException. Ответ «нет, вы не можете» для меня лучше, чем «вы можете сделать… вместо этого», потому что я могу сам придумать для этого альтернативный код. Мне просто интересно, не пропустил ли я какое-то элегантное решение.


person doublep    schedule 08.11.2016    source источник
comment
Вы можете отслеживать, какие элементы следует добавить, а затем повторно запускать функцию для них до тех пор, пока не останется элементов для добавления. После того, как вы закончите все итерации, вы можете добавить все новые элементы по порядку.   -  person marstran    schedule 08.11.2016
comment
Если новое значение было добавлено одновременно, вы хотите иметь возможность видеть его напрямую, если вы выполняете итерацию по значениям одновременно?   -  person Nicolas Filotto    schedule 08.11.2016
comment
@marstran: Это не вариант, потому что место, куда добавляются элементы, ничего не знает о цикле и из-за этого не может иметь обходных путей (например, добавления в какую-то другую коллекцию).   -  person doublep    schedule 08.11.2016
comment
Просто уточняю: если doSomething() добавляет запись на карту, вы хотите, чтобы ваш цикл также повторялся для этого значения? Т.е. вы хотите, чтобы ваша итерация увидела изменения в вашей карте. Если да, то как насчет изменения значения записи и удаления?   -  person Bohemian♦    schedule 08.11.2016
comment
@NicolasFilotto: В идеале да, хотя цикл можно изменить. Однако то, что я не могу изменить, — это точка добавления новых элементов.   -  person doublep    schedule 08.11.2016
comment
@Bohemian: Да, в цикле должны появиться новые значения. Элементы никогда не будут удалены или изменены. т.е. в этом случае я просто знаю, что все возможные одновременные модификации в порядке. Но реализация (LinkedHashMap) этого не знает и выдает ConcurrentModificationException, потому что предполагает, что модификация была неправильной.   -  person doublep    schedule 08.11.2016
comment
@doublep Тогда я, вероятно, подумал бы о рефакторинге, чтобы сломать некоторые зависимости, которые у вас есть в вашем коде. Это не должно быть проблемой, если это сделано правильно в первую очередь.   -  person marstran    schedule 08.11.2016
comment
@doublep Я действительно думаю, что это запах кода, когда вам нужно добавлять элементы в коллекцию во время ее повторения. Но, конечно, иногда это выходит из-под вашего контроля, и вам приходится обходить это :)   -  person marstran    schedule 08.11.2016


Ответы (4)


Это может показаться спорным, но если вас не волнует одновременная модификация, почему бы вам не использовать LinkedHashMap и не игнорировать ConcurrentModificationException?

В принципе:

try {
   myMap.values().forEach(this::doSomething);
}
catch (ConcurrentModificationException ignored) {
}

Я думаю, что это будет работать с LinkedHashMap, но это явно не по назначению. Однако вы можете реализовать свою собственную версию, предназначенную для такого использования.

«Правильный» способ - сделать копии значений или ключей, которые вы хотите перебрать, а затем проверить, было ли что-то добавлено. Грубо:

final Set<K> processedKeys = new HashSet<>();

do {
   final Set<K> keysToProcess = new HashSet<>(myMap.keySet());
   keysToProcess.removeAll(processedKeys);
   keysToProcess.forEach(key -> doSomething(myMap.get(key)));
} while (!keysToProcess.isEmpty());

Обновление для @doublep и @RealSkeptic — почему я думаю, что простое игнорирование исключения в myMap.values().forEach(this::doSomething); сработает.

См. код для forEach или коллекции, возвращенной values() в LinkedHashMap:

    public final void forEach(Consumer<? super V> action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e.value);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }

Итак, forEach выполняет итерацию по связанному списку. Если значения добавляются на карту, они будут добавлены в список, поэтому цикл for будет повторяться, пока не достигнет конца связанного списка. И сначала затем метод проверяет количество модификаций. Таким образом, действие будет эффективно применено ко всем значениям, даже к вновь добавленным.

person lexicore    schedule 08.11.2016
comment
Как поможет игнорирование исключения? Вы просто застрянете на одном и том же месте в итераторе, не так ли? - person RealSkeptic; 08.11.2016
comment
В первом примере я почти уверен, что значения после ConcurrentModificationException никогда не будут обработаны циклом, что совершенно неверно. Насчет второго спасибо, но вопрос не об этом. Да, я знаю, что мог бы переписать это таким образом, но, пожалуйста, прочтите правку. - person doublep; 08.11.2016
comment
@RealSkeptic Смотрите мое обновление. Нет, ты не будешь. forEach проверяет модификацию только в конце итерации. Однако это зависит от реализации. - person lexicore; 08.11.2016
comment
@lexicore: Вы правы, с forEach() все по-другому. Но я не хочу зависеть от чего-то (например, выдает ли он только в конце или проверяет после каждой итерации), что можно рассматривать как деталь реализации. Я думаю, что проголосую за это как за правильный ответ, но я не приму его;) - person doublep; 08.11.2016
comment
@doublep Ну, вы просили не обязательно стандартным способом, так что вот. :) - person lexicore; 08.11.2016
comment
@doublep И не забывайте, тот факт, что вы получаете ConcurrentModificationException также является деталью реализации. Проблема здесь не в том, что это деталь реализации, а в том, что это недокументированная деталь реализации/не часть контракта. - person lexicore; 08.11.2016

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

for(int i=myList.size()-1;i>=0;i--) {
        Object item = myList.get(i);
        if(needsToBeRemoved(item)) {
            myList.remove(i);
        }
}

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

Вы также можете применить ту же технику при обработке карт, если она следует какой-то сортировке, которая не изменяется при обработке (упорядоченная карта).

Если вы хотите или должны использовать итераторы, то нет другого варианта, кроме как дублировать информацию (используя вторую карту).

Обновление:

Вы также можете использовать вспомогательный итератор (список ключей). Например.:

public static void main(String[] args) {
        Map<Long, String> map = new HashMap<>();
        map.put(1L, "Start");
        map.put(10L, "End");

        // This throws ConcurrentModificationException
        // for (Long value : map.keySet()) {
        // map.put(value + 1, "Other");
        // }
        for (Long value : new ArrayList<Long>(map.keySet())) {
            // This works ok
            map.put(value + 1, "Other");
        }

        System.out.println(map);
        // Prints: {1=Start, 2=Other, 10=End, 11=Other}
    }
person Fernando Miguélez    schedule 08.11.2016
comment
У меня есть карта, а не список. Я не могу преобразовать его из карты, потому что это должна быть карта для других целей. - person doublep; 08.11.2016

Итерация по индексам, а не по фактической коллекции - малоизвестный трюк, который здесь сработает. Вы можете получить n-й элемент с помощью потокового API. Я не уверен, насколько это будет эффективно из-за создания всех промежуточных объектов.

LinkedHashMap<String, String> lhm = new LinkedHashMap();        
// fill lhm
for ( int idx=0; idx < lhm.size(); idx++ ) {
    String val = lhm.values().stream().skip(idx).findFirst().get();
    // process val...
}
person Michael Bar-Sinai    schedule 08.11.2016
comment
Он преобразует O(n) простую итерацию в O(n²). Не очень, даже если в моем случае на практике n обычно невелико. - person doublep; 08.11.2016
comment
Не обязательно, но реализация values(), stream() и skip() должна быть оптимизирована и использовать список, поддерживаемый LinkedHashMap. Хотя я не уверен, что это так. - person Michael Bar-Sinai; 08.11.2016
comment
Список связанный, а не случайный доступ. Следовательно, skip() просто не может быть O(1), это O(n). - person doublep; 09.11.2016

Встроенного способа нет, но это не так сложно, если вы используете вариант шаблона производитель/потребитель, где очередь обработки обновляется на каждой итерации:

Map<K, V> map; // assuming
Set<V> processed = new HashSet<>();
while (!map.values().containsAll(processed)) {
    List<V> queue = new ArrayList<>(map.values());
    queue.removeAll(processed);
    V x = queue.get(0);
    processed.add(x);
    doSomething(x);
}
person Bohemian♦    schedule 09.11.2016