Javadoc of Collector показує, як збирати елементи потоку в новий Список. Чи є однолінійний додаток, який додає результати до існуючого ArrayList?
Javadoc of Collector показує, як збирати елементи потоку в новий Список. Чи є однолінійний додаток, який додає результати до існуючого ArrayList?
Відповіді:
ПРИМІТКА: Відповідь 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))
Звичайно, ти пам’ятаєш робити це кожен раз, правда? :-) Скажімо, ви робите. Потім команда продуктивності буде задаватися питанням, чому всі їх ретельно продумані паралельні реалізації не забезпечують жодної швидкості. І знову вони простежать його до вашого коду, який змушує весь потік запускатись послідовно.
Не робіть цього.
toCollection
методу щоразу повертав нову та порожню колекцію, переконував мене в цьому не робити. Мені дуже хочеться розірвати контракт на javadoc основних класів Java.
forEachOrdered
. До побічних ефектів належить додавання елементів до вже наявної колекції, незалежно від того, чи в неї вже є елементи. Якщо ви хочете елементи в потоці поміщаються в нову колекції, використання collect(Collectors.toList())
або toSet()
або toCollection()
.
Наскільки я бачу, усі інші відповіді досі використовували колектор для додавання елементів до існуючого потоку. Однак існує коротше рішення, і воно працює як для послідовних, так і для паралельних потоків. Ви можете просто використовувати метод forEachOrряд у поєднанні з посиланням на метод.
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
Єдине обмеження полягає в тому, що джерело та ціль - це різні списки, тому що вам заборонено вносити зміни до джерела потоку, поки він обробляється.
Зауважте, що це рішення працює як для послідовних, так і для паралельних потоків. Однак це не має вигоди від одночасності. Посилання методу, передане для forEachOrряд , завжди буде виконуватися послідовно.
forEach(existing::add)
як можливість у відповідь два місяці тому . Я повинен був також додати forEachOrdered
…
forEachOrdered
замість цього forEach
?
forEachOrdered
працює як для послідовних, так і для паралельних потоків. Навпаки, forEach
може виконувати об'єкт переданої функції одночасно для паралельних потоків. У цьому випадку об'єкт функції повинен бути належним чином синхронізований, наприклад, за допомогою a Vector<Integer>
.
target::add
. Незалежно від того, з яких ниток викликаний метод, немає перегонів даних . Я б очікував, що ти це знаєш.
Коротка відповідь - ні (або не повинно бути). 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), а вдаватися до «бруднішої» тактики лише в разі необхідності.
Справді довга відповідь: (тобто якщо ви доклали зусиль, щоб насправді знайти та прочитати вступ / книгу на ПЗ, як пропонується)
Щоб дізнатись, чому зміна існуючих списків взагалі є поганою ідеєю і призводить до менш рентабельного коду - якщо ви не змінюєте локальну змінну і ваш алгоритм є коротким та / або тривіальним, що виходить за рамки питання про збереження коду - знайдіть хороший вступ до функціонального програмування (їх сотні) та починайте читати. Пояснення "попереднього перегляду" буде чимось на зразок: це математично більш обґрунтовано і простіше міркувати про те, щоб не змінювати дані (в більшості частин вашої програми) і призводить до вищого рівня і менш технічного (а також більш дружнього до людини, коли ваш мозок переходи від імперативного мислення старого стилю) визначення логіки програми.
Ерік Аллік вже дав дуже вагомі причини, чому ви, швидше за все, не захочете збирати елементи потоку в існуючий Список.
У будь-якому випадку, ви можете використовувати наступний однолінійний, якщо вам справді потрібна ця функціональність.
Але як пояснює у своїй відповіді Стюарт Маркс , ви ніколи не повинні цього робити, якщо потоки можуть бути паралельними потоками - використовуйте на свій страх і ризик ...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
Вам просто потрібно вказати свій оригінальний список, який буде 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())
);
Ось що пропонує ця парадигма функціонального програмування.
targetList = sourceList.stream (). flatmap (Список :: потік) .collect (Collectors.toList ());
Я б об'єднав старий список і новий список як потоки і зберег би результати до списку призначення. Паралельно також працює добре.
Я буду використовувати приклад прийнятої відповіді, наданий Стюарт Маркс:
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]
Сподіваюся, це допомагає.
Collection
"