Ответ 1
ПРИМЕЧАНИЕ: nosid answer показывает, как добавить к существующей коллекции с помощью forEachOrdered()
. Это полезный и эффективный метод для изменения существующих коллекций. В моем ответе объясняется, почему вы не должны использовать Collector
для изменения существующей коллекции.
Короткий ответ нет, по крайней мере, не в общем случае, вы не должны использовать Collector
для изменения существующей коллекции.
Причина в том, что коллекторы предназначены для поддержки parallelism, даже для коллекций, которые не являются потокобезопасными. То, как они это делают, состоит в том, чтобы каждый поток работал независимо от собственной коллекции промежуточных результатов. То, как каждый поток получает свою собственную коллекцию, - это вызов Collector.supplier()
, который требуется для возврата коллекции new каждый раз.
Эти коллекции промежуточных результатов затем сливаются, опять же в поточном ограничении, пока не появится единый набор результатов. Это конечный результат операции collect()
.
Пара ответов Balder и assylias предложила использовать Collectors.toCollection()
, а затем передать поставщика, который возвращает существующий список вместо нового списка. Это нарушает требование поставщика, то есть каждый раз он возвращает новую пустую коллекцию.
Это будет работать для простых случаев, как демонстрируют примеры в их ответах. Однако это не сработает, особенно если поток запускается параллельно. (Будущая версия библиотеки может измениться каким-то непредвиденным образом, что приведет к ее сбою даже в последовательном случае.)
Возьмем простой пример:
List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
.collect(Collectors.toCollection(() -> destList));
System.out.println(destList);
Когда я запускаю эту программу, я часто получаю ArrayIndexOutOfBoundsException
. Это связано с тем, что несколько потоков работают в ArrayList
, небезопасной структуре данных. Хорошо, пусть синхронизируется:
List<String> destList =
Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));
Это больше не будет работать с исключением. Но вместо ожидаемого результата:
[foo, 0, 1, 2, 3]
он дает такие странные результаты:
[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]
Это результат описанных выше операций накопления/слияния с потоком. При параллельном потоке каждая нить призывает поставщика получить свою собственную коллекцию для промежуточного накопления. Если вы передадите поставщика, который возвращает коллекцию той же, каждый поток присоединяет ее результаты к этой коллекции. Поскольку в потоках нет порядка, результаты будут добавляться в произвольном порядке.
Затем, когда эти промежуточные коллекции объединены, это в основном объединяет список с самим собой. Списки объединяются с использованием List.addAll()
, в котором говорится, что результаты undefined, если исходная коллекция изменена во время операции. В этом случае ArrayList.addAll()
выполняет операцию копирования массива, поэтому он заканчивает копирование себя, что похоже на то, что можно было ожидать, я думаю. (Обратите внимание, что другие реализации List могут иметь совершенно другое поведение.) В любом случае, это объясняет странные результаты и дублированные элементы в месте назначения.
Вы могли бы сказать: "Я просто обязательно буду запускать свой поток последовательно" и продолжайте писать код, подобный этому
stream.collect(Collectors.toCollection(() -> existingList))
в любом случае. Я бы рекомендовал не делать этого. Конечно, если вы контролируете поток, вы можете гарантировать, что он не будет работать параллельно. Я ожидаю, что стиль программирования появится там, где потоки передаются вместо коллекций. Если кто-то передает вам поток, и вы используете этот код, он будет терпеть неудачу, если поток окажется параллельным. Хуже того, кто-то может передать вам последовательный поток, и этот код будет работать нормально, пройдет все тесты и т.д. Затем, некоторое время спустя, код в другом месте в системе может измениться, чтобы использовать параллельные потоки, которые вызовут ваш код.
ОК, а затем просто не забудьте вызвать sequential()
в любом потоке, прежде чем использовать этот код:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
Конечно, вы будете помнить об этом каждый раз, верно?:-) Скажи, что да. Затем команда разработчиков будет задаваться вопросом, почему все их тщательно продуманные параллельные реализации не обеспечивают ускорения. И еще раз они проследят его до вашего кода, который заставляет весь поток запускаться последовательно.
Не делай этого.