Javadoc of Collector показывает, как собирать элементы потока в новый список. Есть ли одна строка, которая добавляет результаты в существующий ArrayList?
Javadoc of Collector показывает, как собирать элементы потока в новый список. Есть ли одна строка, которая добавляет результаты в существующий ArrayList?
Ответы:
ПРИМЕЧАНИЕ: ответ nosid показывает, как добавить в существующую коллекцию с помощью forEachOrdered()
. Это полезный и эффективный метод для изменения существующих коллекций. В моем ответе объясняется, почему вы не должны использовать Collector
для изменения существующей коллекции.
Краткий ответ - нет , по крайней мере, вообще, вы не должны использовать Collector
для изменения существующей коллекции.
Причина в том, что коллекторы предназначены для поддержки параллелизма даже над коллекциями, которые не являются поточно-ориентированными. Они делают так, чтобы каждый поток работал независимо со своей собственной коллекцией промежуточных результатов. Способ, которым каждый поток получает свою собственную коллекцию, состоит в том, чтобы вызывать метод, Collector.supplier()
который требуется для возврата новой коллекции каждый раз.
Эти коллекции промежуточных результатов затем объединяются, снова в потоке-ограниченном режиме, пока не будет единой коллекции результатов. Это конечный результат collect()
операции.
Пара ответы от Бальдра и 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()
, который говорит, что результаты не определены, если исходная коллекция была изменена во время операции. В этом случае ArrayList.addAll()
выполняется операция копирования массива, поэтому она в конечном итоге дублирует себя, что , скорее всего, и следовало ожидать. (Обратите внимание, что другие реализации List могут иметь совершенно другое поведение.) В любом случае, это объясняет странные результаты и дублированные элементы в месте назначения.
Вы можете сказать: «Я просто позабочусь о том, чтобы мой поток запускался последовательно», а затем напишу такой код
stream.collect(Collectors.toCollection(() -> existingList))
тем не мение. Я бы рекомендовал не делать этого. Конечно, если вы управляете потоком, вы можете гарантировать, что он не будет работать параллельно. Я ожидаю, что появится стиль программирования, в котором вместо коллекций будут передаваться потоки. Если кто-то передает вам поток и вы используете этот код, произойдет сбой, если поток окажется параллельным. Хуже того, кто-то может передать вам последовательный поток, и этот код некоторое время будет нормально работать, проходить все тесты и т. Д. Затем, через некоторое произвольное время, код в другом месте системы может измениться на использование параллельных потоков, что вызовет ваш код сломать.
Хорошо, тогда просто не забудьте позвонить sequential()
в любой поток, прежде чем использовать этот код:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
Конечно, вы будете помнить делать это каждый раз, верно? :-) Допустим, вы делаете. Тогда команде по производительности будет интересно, почему все их тщательно разработанные параллельные реализации не обеспечивают никакого ускорения. И снова они проследят это до вашего кода, который заставляет весь поток работать последовательно.
Не делай этого.
toCollection
метода возвращал новую и пустую коллекцию каждый раз, убеждает меня не делать этого. Я действительно хочу разорвать контракт Javadoc на основные классы Java.
forEachOrdered
. Побочные эффекты включают добавление элементов в существующую коллекцию независимо от того, есть ли у нее уже элементы. Если вы хотите поместить элементы потока в новую коллекцию, используйте collect(Collectors.toList())
или toSet()
или toCollection()
.
Насколько я вижу, все остальные ответы до сих пор использовали коллектор для добавления элементов в существующий поток. Однако есть более короткое решение, и оно работает как для последовательных, так и для параллельных потоков. Вы можете просто использовать метод forEachOrdered в сочетании со ссылкой на метод.
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
Единственным ограничением является то, что источник и цель - это разные списки, потому что вы не можете вносить изменения в источник потока, пока он обрабатывается.
Обратите внимание, что это решение работает как для последовательных, так и для параллельных потоков. Тем не менее, он не выигрывает от параллелизма. Ссылка на метод, переданная в forEachOrdered , всегда будет выполняться последовательно.
forEach(existing::add)
в качестве возможности в ответ два месяца назад . Я должен был также добавить forEachOrdered
...
forEachOrdered
вместо forEach
?
forEachOrdered
работает как для последовательных, так и для параллельных потоков. Напротив, forEach
может выполнять переданный объект функции одновременно для параллельных потоков. В этом случае объект функции должен быть правильно синхронизирован, например, с помощью Vector<Integer>
.
target::add
. Независимо от того, из каких потоков вызывается метод, данных не происходит . Я бы ожидал, что вы это знаете.
Краткий ответ - нет (или должно быть нет). РЕДАКТИРОВАТЬ: да, это возможно (см. Ответ Ассилии ниже), но продолжайте читать. РЕДАКТИРОВАТЬ 2: но посмотрите ответ Стюарта Маркса по еще одной причине, почему вы все еще не должны это делать!
Чем дольше ответ:
Цель этих конструкций в Java 8 состоит в том, чтобы ввести некоторые концепции функционального программирования в язык; в функциональном программировании структуры данных обычно не модифицируются, вместо этого новые создаются из старых с помощью таких преобразований, как карта, фильтр, сложение / уменьшение и многие другие.
Если вам нужно изменить старый список, просто соберите сопоставленные элементы в новый список:
final List<Integer> newList = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
а затем сделайте list.addAll(newList)
- снова: если вы действительно должны.
(или создайте новый список, объединяющий старый и новый, и назначьте его обратно list
переменной - это немного больше в духе FP, чем addAll
)
Что касается API: даже если API это позволяет (опять же, смотрите ответ ассилий), вы должны стараться избегать этого независимо, по крайней мере, в целом. Лучше не бороться с парадигмой (FP) и пытаться изучать ее, а не бороться с ней (хотя Java обычно не является языком FP), а прибегать к «более грязной» тактике только в случае крайней необходимости.
Очень длинный ответ: (т. Е. Если вы включите усилия по нахождению и чтению вступления / книги по FP, как это было предложено)
Чтобы выяснить, почему изменение существующих списков, как правило, является плохой идеей и приводит к снижению удобства сопровождения кода - если вы не изменяете локальную переменную, а ваш алгоритм не является коротким и / или тривиальным, что выходит за рамки вопроса поддерживаемости кода - найти хорошее введение в функциональное программирование (их сотни) и начать чтение. Объяснение «предварительного просмотра» будет выглядеть примерно так: оно более математически обосновано и легче рассуждать о том, что не нужно изменять данные (в большинстве частей вашей программы), и приводит к более высокому уровню и менее техническому (а также более дружественному к человеку, как только ваш мозг) переходы от императивного мышления старого стиля) определения программной логики.
Эрик Аллик уже привел очень веские причины, по которым вы, скорее всего, не захотите собирать элементы потока в существующий список.
В любом случае, вы можете использовать следующий однострочный, если вам действительно нужна эта функциональность.
Но, как объясняет Стюарт Маркс в своем ответе, вы никогда не должны этого делать, если потоки могут быть параллельными потоками - используйте на свой страх и риск ...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
Вы просто должны сослаться на свой первоначальный список, чтобы быть тем, который Collectors.toList()
возвращает.
Вот демо:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Reference {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
System.out.println(list);
// Just collect even numbers and start referring the new list as the original one.
list = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(list);
}
}
И вот как вы можете добавить вновь созданные элементы в ваш исходный список в одну строку.
List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList())
);
Вот что обеспечивает эта парадигма функционального программирования.
targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());
Я бы объединял старый список и новый список в виде потоков и сохранял результаты в списке назначения. Параллельно работает тоже хорошо.
Я буду использовать пример принятого ответа, который дал Стюарт Маркс:
List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
destList = Stream.concat(destList.stream(), newList.stream()).parallel()
.collect(Collectors.toList());
System.out.println(destList);
//output: [foo, 0, 1, 2, 3, 4, 5]
Надеюсь, поможет.
Collection
»