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

157

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

codefx
источник
1
Существует уже ответ здесь . Ищите пункт «Добавить к существующему Collection»
Хольгер,

Ответы:

198

ПРИМЕЧАНИЕ: ответ 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))

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

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

Стюарт Маркс
источник
Отличное объяснение! - спасибо за разъяснение этого. Я отредактирую свой ответ, чтобы рекомендовать никогда не делать это с возможными параллельными потоками.
Балдер
3
Если вопрос заключается в том, что для добавления элементов потока в существующий список существует однострочник, то короткий ответ - да . Смотри мой ответ. Однако я согласен с вами, что использование Collectors.toCollection () в сочетании с существующим списком является неправильным способом.
Носид
Правда. Я думаю, что все мы думали о коллекционерах.
Стюарт Маркс
Отличный ответ! Мне очень хочется использовать последовательное решение, даже если вы явно не советуете, потому что, как указано, оно должно работать хорошо. Но тот факт, что Javadoc требует, чтобы аргумент поставщика toCollectionметода возвращал новую и пустую коллекцию каждый раз, убеждает меня не делать этого. Я действительно хочу разорвать контракт Javadoc на основные классы Java.
увеличить
1
@AlexCurvers Если вы хотите, чтобы поток имел побочные эффекты, вы почти наверняка захотите использовать forEachOrdered. Побочные эффекты включают добавление элементов в существующую коллекцию независимо от того, есть ли у нее уже элементы. Если вы хотите поместить элементы потока в новую коллекцию, используйте collect(Collectors.toList())или toSet()или toCollection().
Стюарт Маркс
169

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

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

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

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

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

nosid
источник
6
+1 Забавно, как много людей утверждают, что нет возможности, когда она есть. Btw. Я включил forEach(existing::add)в качестве возможности в ответ два месяца назад . Я должен был также добавить forEachOrdered...
Хольгер
5
Есть ли какая-то причина, которую вы использовали forEachOrderedвместо forEach?
membersound
6
@membersound: forEachOrderedработает как для последовательных, так и для параллельных потоков. Напротив, forEachможет выполнять переданный объект функции одновременно для параллельных потоков. В этом случае объект функции должен быть правильно синхронизирован, например, с помощью Vector<Integer>.
nosid
@BrianGoetz: Я должен признать, что документация Stream.forEachOrdered немного неточна. Тем не менее, я не вижу разумного толкования этой спецификации , в которой нет никаких отношений « до и между» между любыми двумя вызовами target::add. Независимо от того, из каких потоков вызывается метод, данных не происходит . Я бы ожидал, что вы это знаете.
Носид
Это самый полезный ответ, насколько я понимаю. На самом деле он показывает практический способ вставки элементов в существующий список из потока, что и было задано вопросом (несмотря на вводящее в заблуждение слово «собирать»)
Wheezil
12

Краткий ответ - нет (или должно быть нет). РЕДАКТИРОВАТЬ: да, это возможно (см. Ответ Ассилии ниже), но продолжайте читать. РЕДАКТИРОВАТЬ 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, как это было предложено)

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

Эрик Каплун
источник
@assylias: логически, это не было неправильно, потому что была часть или часть; Во всяком случае, добавил примечание.
Эрик Каплун
1
Краткий ответ правильный. Предложенные однострочники будут успешными в простых случаях, но в общем случае потерпят неудачу.
Стюарт Маркс
Более длинный ответ в основном правильный, но дизайн API в основном связан с параллелизмом, а не с функциональным программированием. Хотя, конечно, есть много вещей в FP, которые поддаются параллелизму, поэтому эти две концепции хорошо согласованы.
Стюарт Маркс
@StuartMarks: Интересно: в каких случаях выходит из строя решение, представленное в ответе Ассилии? (и хорошие моменты о параллелизме - я полагаю, я слишком сильно хотел пропагандировать ФП)
Эрик Каплун
@ErikAllik Я добавил ответ, который охватывает эту проблему.
Стюарт Маркс
11

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

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

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

list.stream().collect(Collectors.toCollection(() -> myExistingList));
Бальдр
источник
ааа, это позор: P
Эрик Каплун
2
Эта техника ужасно провалится, если поток будет работать параллельно.
Стюарт Маркс
1
Поставщик сбора должен убедиться, что он не потерпит неудачу - например, путем предоставления одновременного сбора.
Балдер
2
Нет, этот код нарушает требование toCollection (), которое заключается в том, что поставщик возвращает новую пустую коллекцию соответствующего типа. Даже если назначение является поточно-ориентированным, слияние, которое выполняется для параллельного случая, приведет к неверным результатам.
Стюарт Маркс
1
@ Балдер Я добавил ответ, который должен прояснить это.
Стюарт Маркс
4

Вы просто должны сослаться на свой первоначальный список, чтобы быть тем, который 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())
);

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

Аман Агнихотри
источник
Я хотел сказать, как добавить / собрать в существующий список, а не просто переназначить.
codefx
1
Ну, технически, вы не можете делать такого рода вещи в парадигме функционального программирования, о которой пойдут потоки. В функциональном программировании состояние не изменяется, вместо этого создаются новые состояния в постоянных структурах данных, что делает его безопасным для целей параллелизма и более функциональным. Подход, который я упомянул, это то, что вы можете сделать, или вы можете прибегнуть к объектно-ориентированному подходу старого стиля, где вы перебираете каждый элемент и сохраняете или удаляете элементы по своему усмотрению.
Аман Агнихотри
0

targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());

А.С. Ранджан
источник
0

Я бы объединял старый список и новый список в виде потоков и сохранял результаты в списке назначения. Параллельно работает тоже хорошо.

Я буду использовать пример принятого ответа, который дал Стюарт Маркс:

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]

Надеюсь, поможет.

Никос Стайс
источник