Как добавить элементы потока Java8 в существующий список

Javadoc коллектора показывает как собирать элементы в новый список. Есть ли однострочный, который добавляет результаты в существующий ArrayList?

5 ответов


Примечание: nosid это!--22--> показывает, как добавить в существующую коллекцию с помощью 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() выполняет операцию копирования массива, поэтому она в конечном итоге дублирует себя, что является своего рода ожидаемым, я думаю. (Обратите внимание, что другие реализации списка могло быть совершенно другое поведение.) Во всяком случае, это объясняет странные результаты и дублированные элементы в пункте назначения.

вы можете сказать: "я просто проверю, чтобы запустить мой поток последовательно" и идти вперед и писать код, как это

stream.collect(Collectors.toCollection(() -> existingList))

в любом случае. Я бы рекомендовал этого не делать. Если вы управляете потоком, вы можете гарантировать, что он не будет работать параллельно. Я ожидаю, что появится стиль программирования, где потоки будут передаваться вместо коллекции. Если кто-то передаст вам поток, и вы используете этот код, он потерпит неудачу, если поток будет параллельным. Хуже того, кто-то может передать вам последовательный поток, и этот код будет работать нормально некоторое время, пройти все тесты и т. д. Затем, некоторое произвольное количество времени спустя, код в другом месте системы может измениться на использование параллельных потоков, которые вызовут код код, чтобы сломаться.

хорошо, тогда просто не забудьте позвонить sequential() на любом потоке перед использованием этот код:

stream.sequential().collect(Collectors.toCollection(() -> existingList))

конечно, вы будете помнить, чтобы сделать это каждый раз, не так ли? :-) Допустим, вы. Затем команда производительности будет задаваться вопросом, почему все их тщательно разработанные параллельные реализации не обеспечивают ускорения. И снова они проследят его до код код, который заставляет весь поток работать последовательно.

не делай этого.


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

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

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

обратите внимание, что это решение работает как для последовательных, так и параллельных потоков. Однако он не извлекает выгоды из параллелизма. Ссылка на метод передана в forEachOrdered всегда будет выполняться последовательно.


короткий ответ: нет (или должно быть). EDIT: да, это возможно (см. ответ ассилии ниже), но продолжайте читать. EDIT2: но СМ. ответ Стюарта Маркса еще по одной причине, почему вы все равно не должны этого делать!

более подробный ответ:

целью этих конструкций в Java 8 является введение некоторых понятий Функциональное Программирование к языку; в функциональном программировании, структуры данных обычно не изменяются, вместо этого новые создаются из старых с помощью преобразований, таких как map, filter, fold/reduce и многих других.

если вы должны измените старый список, просто соберите сопоставленные элементы в новый список:

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, как предложено)

чтобы узнать, почему изменение существующих списков в целом является плохой идеей и приводит к менее обслуживаемому коду-если вы не изменяете локальную переменную, а ваш алгоритм короткий и/или тривиальный, что выходит за рамки вопроса о ремонтопригодности кода-найдите хорошее введение в функциональное программирование (есть сотни) и начните чтение. Объяснение" предварительного просмотра " будет что-то вроде: это более математически обоснованно и проще рассуждать о том, чтобы не изменять данные (в большинстве частей вашей программы) и приводит к более высокому уровню и менее техническим (а также более дружественным к человеку, как только ваш мозг переходит от императивного мышления старого стиля) определениям логики программы.


Ерик Аллик уже дал очень веские причины, почему вы, скорее всего, не захотите собирать элементы потока в существующий список.

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

EDIT:, а как Стюарт Марки объясняет в своем ответе, вы должны никогда сделайте это, если потоки могут быть параллельными потоками-используйте на свой страх и риск...

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())
);

вот что обеспечивает эта парадигма функционального программирования.