Як додати елементи потоку Java8 до наявного списку


155

Javadoc of Collector показує, як збирати елементи потоку в новий Список. Чи є однолінійний додаток, який додає результати до існуючого ArrayList?


1
Існує вже відповідь тут . Шукайте предмет "Додати до існуючого Collection"
Холгер

Відповіді:


198

ПРИМІТКА: Відповідь nosid показує, як додати до наявної колекції за допомогою forEachOrdered(). Це корисна та ефективна методика мутації існуючих колекцій. Моя відповідь стосується того, чому ви не повинні використовувати Collectorмутацію існуючої колекції.

Коротка відповідь - ні , принаймні, не в цілому, ви не повинні використовувати a 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))

Звичайно, ти пам’ятаєш робити це кожен раз, правда? :-) Скажімо, ви робите. Потім команда продуктивності буде задаватися питанням, чому всі їх ретельно продумані паралельні реалізації не забезпечують жодної швидкості. І знову вони простежать його до вашого коду, який змушує весь потік запускатись послідовно.

Не робіть цього.


Чудове пояснення! - дякую за уточнення цього. Я відредагую свою відповідь, щоб рекомендувати ніколи цього не робити з можливими паралельними потоками.
Лисий

3
Якщо питання полягає в тому, чи є однолінійка для додавання елементів потоку до наявного списку, то коротка відповідь - так . Дивіться мою відповідь. Однак я погоджуюся з вами, що використання Collectors.toCollection () у поєднанні з існуючим списком - це неправильний шлях.
ніс

Правда. Я думаю, що решта з нас думали про колекціонерів.
Стюарт відзначає

Чудова відповідь! Я дуже спокушаюся використовувати послідовне рішення, навіть якщо ви чітко не радите, оскільки, як зазначено, воно повинно добре працювати. Але той факт, що javadoc вимагає, щоб аргумент постачальника toCollectionметоду щоразу повертав нову та порожню колекцію, переконував мене в цьому не робити. Мені дуже хочеться розірвати контракт на javadoc основних класів Java.
зум

1
@AlexCurvers Якщо ви хочете, щоб у потоку були побічні ефекти, ви майже напевно хочете їх використовувати forEachOrdered. До побічних ефектів належить додавання елементів до вже наявної колекції, незалежно від того, чи в неї вже є елементи. Якщо ви хочете елементи в потоці поміщаються в нову колекції, використання collect(Collectors.toList())або toSet()або toCollection().
Стюарт Маркс

169

Наскільки я бачу, усі інші відповіді досі використовували колектор для додавання елементів до існуючого потоку. Однак існує коротше рішення, і воно працює як для послідовних, так і для паралельних потоків. Ви можете просто використовувати метод forEachOrряд у поєднанні з посиланням на метод.

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

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

Єдине обмеження полягає в тому, що джерело та ціль - це різні списки, тому що вам заборонено вносити зміни до джерела потоку, поки він обробляється.

Зауважте, що це рішення працює як для послідовних, так і для паралельних потоків. Однак це не має вигоди від одночасності. Посилання методу, передане для forEachOrряд , завжди буде виконуватися послідовно.


6
+1 Смішно, як так багато людей стверджують, що немає можливості, коли така є. Btw. Я включив forEach(existing::add)як можливість у відповідь два місяці тому . Я повинен був також додати forEachOrdered
Холгер

5
Чи є причина, яку ви використовували forEachOrderedзамість цього forEach?
члени звучання

6
@membersound: forEachOrderedпрацює як для послідовних, так і для паралельних потоків. Навпаки, forEachможе виконувати об'єкт переданої функції одночасно для паралельних потоків. У цьому випадку об'єкт функції повинен бути належним чином синхронізований, наприклад, за допомогою a Vector<Integer>.
носід

@BrianGoetz: Я мушу визнати, що документація на Stream.forEachOrdered трохи неточна. Тим НЕ менше, я не бачу якихось - або розумне тлумачення цієї специфікації , в якій тобто не відбувається, перш за , ніж відносини між будь-якими двома викликами target::add. Незалежно від того, з яких ниток викликаний метод, немає перегонів даних . Я б очікував, що ти це знаєш.
носід

Це найкорисніша відповідь, що стосується мене. Це фактично показує практичний спосіб вставити елементи до наявного списку із потоку, саме про це було задано питання (незважаючи на оманливе слово "збирати")
Wheezil

12

Коротка відповідь - ні (або не повинно бути). EDIT: Так, це можливо (див. Відповідь Асілія нижче), але читайте. EDIT2: але дивіться відповідь Стюарта Маркса з ще однієї причини, чому ви все одно не повинні цього робити!

Більш довга відповідь:

Метою цих конструкцій у Java 8 є введення в мову деяких понять функціонального програмування ; У функціональному програмуванні структури даних зазвичай не змінюються, натомість нові створюються зі старих за допомогою перетворень, таких як карта, фільтр, складання / зменшення та багато інших.

Якщо вам потрібно змінити старий список, просто збирайте відображені елементи у новий список:

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

а потім зробіть list.addAll(newList)- знову: якщо справді треба.

(або побудуйте новий список, що поєднує старий і новий, і присвоїть його назад listзмінній - це трохи більше в дусі FP, ніж addAll)

Щодо API: навіть якщо API дозволяє це (знову ж таки, дивіться відповідь assylias), ви повинні намагатися уникати цього незалежно, принаймні загалом. Найкраще не боротися з парадигмою (FP), а намагатися навчитися її, а не боротися з нею (навіть якщо Java, як правило, не є мовою FP), а вдаватися до «бруднішої» тактики лише в разі необхідності.

Справді довга відповідь: (тобто якщо ви доклали зусиль, щоб насправді знайти та прочитати вступ / книгу на ПЗ, як пропонується)

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


@assylias: логічно, це не було помилкою, тому що була або частина; все одно, додав примітку.
Ерік Каплун

1
Коротка відповідь правильна. Запропоновані однолінійні перемоги матимуть успіх у простих випадках, але в загальному випадку провалюються.
Стюарт відзначає

Більш довгий відповідь здебільшого правильний, але дизайн API в основному стосується паралелізму і менше стосується функціонального програмування. Хоча, звичайно, є багато речей щодо ПП, які піддаються паралелізму, тому ці дві концепції добре узгоджені.
Стюарт відзначає

@StuartMarks: Цікаво: в яких випадках розбивається рішення, подане у відповіді асилії? (і хороші моменти щодо паралелізму - я думаю, я занадто охоче захищав ФП)
Ерік Каплун

@ErikAllik Я додав відповідь, яка висвітлює це питання.
Стюарт відзначає

11

Ерік Аллік вже дав дуже вагомі причини, чому ви, швидше за все, не захочете збирати елементи потоку в існуючий Список.

У будь-якому випадку, ви можете використовувати наступний однолінійний, якщо вам справді потрібна ця функціональність.

Але як пояснює у своїй відповіді Стюарт Маркс , ви ніколи не повинні цього робити, якщо потоки можуть бути паралельними потоками - використовуйте на свій страх і ризик ...

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

ах, це ганьба: П
Ерік Каплун

2
Ця техніка жахливо вийде з ладу, якщо потік буде запущений паралельно.
Стюарт відзначає

1
Відповідальність постачальника колекцій повинна бути переконана, що вона не виходить з ладу - наприклад, надаючи одночасну колекцію.
Бальдер

2
Ні, цей код порушує вимогу toCollection (), тобто постачальник повертає нову, порожню колекцію відповідного типу. Навіть якщо місце призначення є безпечним для потоків, об'єднання, яке робиться для паралельного випадку, призведе до неправильних результатів.
Стюарт відзначає

1
@Balder Я додав відповідь, яка повинна це прояснити.
Стюарт відзначає

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

Я б об'єднав старий список і новий список як потоки і зберег би результати до списку призначення. Паралельно також працює добре.

Я буду використовувати приклад прийнятої відповіді, наданий Стюарт Маркс:

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]

Сподіваюся, це допомагає.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.