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

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

Ответ 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))

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

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

Ответ 2

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

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

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

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

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

Ответ 3

Короткий ответ - нет (или должен быть нет). РЕДАКТИРОВАТЬ: да, это возможно (см. ответ assylias ниже), но продолжайте читать. EDIT2:, но см. ответ Стюарта Маркса по еще одной причине, почему вы все еще не должны этого делать!

Более длинный ответ:

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

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

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

а затем do list.addAll(newList) - снова: если вы действительно должны.

(или создайте новый список, объединяющий старый и новый, и назначьте его обратно переменной list - это немного больше в духе FP, чем addAll)

Что касается API: даже если API позволяет это (опять же, см. ответ assylias), вы должны стараться избегать этого, независимо от того, по крайней мере, в целом. Лучше не бороться с парадигмой (FP) и пытаться ее изучить, а не бороться с ней (хотя Java вообще не является языком FP) и прибегать к "грязной" тактике, если это абсолютно необходимо.

Действительно длинный ответ: (т.е. если вы включите усилия по поиску и чтению вставки/версии FP, как было предложено)

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

Ответ 4

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

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

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

list.stream().collect(Collectors.toCollection(() -> myExistingList));

Ответ 5

Вам просто нужно отнести исходный список к тому, который возвращает 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())
);

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