it-swarm.com.ru

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

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

129
codefx

ПРИМЕЧАНИЕ: ответ nosid показывает, как добавить существующую коллекцию с помощью forEachOrdered(). Это полезный и эффективный метод для изменения существующих коллекций. В моем ответе объясняется, почему вы не должны использовать Collector для изменения существующей коллекции.

Краткий ответ: нет , по крайней мере, вообще, вы не должны использовать Collector для изменения существующей коллекции.

Причина в том, что коллекторы предназначены для поддержки параллелизма даже над коллекциями, которые не являются поточно-ориентированными. Они делают так, чтобы каждый поток работал независимо со своей собственной коллекцией промежуточных результатов. Каждый поток получает свою собственную коллекцию, вызывая функцию Collector.supplier(), которая требуется для возврата новой коллекции каждый раз.

Эти коллекции промежуточных результатов затем объединяются, снова в потоке-ограниченном режиме, пока не будет единой коллекции результатов. Это окончательный результат операции 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(), который говорит, что результаты не определены, если исходная коллекция была изменена во время операции. В этом случае ArrayList.addAll() выполняет операцию копирования массива, поэтому он в конечном итоге дублирует себя, что, как я полагаю, является своего рода ожиданием. (Обратите внимание, что другие реализации List могут иметь совершенно другое поведение.) В любом случае, это объясняет странные результаты и дублированные элементы в месте назначения.

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

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

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

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

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

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

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

150
Stuart Marks

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

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

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

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

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

143
nosid

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

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

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

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

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

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

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

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

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

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

11
Erik Allik

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

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

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

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

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

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

4
Aman Agnihotri