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

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

117 голосов | спросил codefx 31 MaramMon, 31 Mar 2014 08:40:32 +04002014-03-31T08:40:32+04:0008 2014, 08:40:32

4 ответа


0

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

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

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

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

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

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

ответил Stuart Marks 31 MaramMon, 31 Mar 2014 11:40:23 +04002014-03-31T11:40:23+04:0011 2014, 11:40:23
0

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

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

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

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

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

ответил nosid 31 MaramMon, 31 Mar 2014 10:53:52 +04002014-03-31T10:53:52+04:0010 2014, 10:53:52
0

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

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

РЕДАКТИРОВАТЬ . Но, как объясняет Стюарт Маркс , никогда не делайте этого, если потоки могут быть параллельными потоками - используйте на свой страх и риск ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));
ответил Balder 31 MaramMon, 31 Mar 2014 10:20:51 +04002014-03-31T10:20:51+04:0010 2014, 10:20:51
0

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

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

ответил Aman Agnihotri 31 MaramMon, 31 Mar 2014 08:51:50 +04002014-03-31T08:51:50+04:0008 2014, 08:51:50

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132