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


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

5 110

5 ответов:

Примечание: ответ носида показывает, как добавить в существующую коллекцию с помощью 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);

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

обратите внимание, что это решение работает как для последовательных, так и параллельных потоков. Однако он не выигрывает от параллелизма. Ссылка на метод передается в 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 intro/книги, как это предлагается)

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

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

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

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

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