Чи можете ви розділити потік на два потоки?


146

У мене є набір даних, представлений потоком Java 8:

Stream<T> stream = ...;

Я бачу, як відфільтрувати його, щоб отримати випадкову підмножину - наприклад

Random r = new Random();
PrimitiveIterator.OfInt coin = r.ints(0, 2).iterator();   
Stream<T> heads = stream.filter((x) -> (coin.nextInt() == 0));

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

(heads, tails) = stream.[some kind of split based on filter]

Дякую за будь-яке розуміння.


Відповідь Марка набагато корисніша, ніж відповідь Луї, але мушу сказати, що Луїс більше пов'язаний з початковим запитанням. Питання скоріше орієнтоване на можливість перетворення Streamв декілька Streams без проміжного перетворення , хоча я думаю, що люди, які дійшли до цього питання, насправді шукають спосіб досягти цього незалежно від такого обмеження, що є відповіддю Марка. Це може бути пов’язано з тим, що питання в заголовку не таке, як у описі .
devildelta

Відповіді:


9

Не зовсім. Ви не можете отримати два Streams з одного; це не має сенсу - як би ви повторювали один, не створюючи іншого одночасно? Потоком можна керувати лише один раз.

Однак, якщо ви хочете скинути їх у список чи щось таке, ви можете зробити

stream.forEach((x) -> ((x == 0) ? heads : tails).add(x));

65
Чому це не має сенсу? Оскільки потік - трубопровід, немає ніяких причин, щоб він не міг створити двох виробників вихідного потоку, я міг бачити, як цим керує колектор, який забезпечує два потоки.
Бретт Райан

36
Не безпечно для ниток. Погана порада намагатися додати безпосередньо до колекції, тому у нас є stream.collect(...)заздалегідь визначений безпечний потік Collectors, який працює добре навіть у колекціях, що не захищені ниткою (без синхронізованих записів блокування). Найкраща відповідь від @MarkJeronimus.
YoYo

1
@JoD Це безпечно для ниток, якщо голови та хвости захищені ниткою. Крім того, якщо припустити використання непаралельних потоків, лише порядок не гарантується, тому вони є безпечними для потоків. Програміст вирішує проблеми з одночасністю, тому ця відповідь цілком підходить, якщо колекції є безпечними для потоків.
Ніколя

1
@Nixon це не підходить за наявності кращого рішення, яке ми маємо тут. Наявність такого коду може призвести до поганого прецеденту, що змусить інших використовувати його неправильно. Навіть якщо паралельні потоки не використовуються, це лише один крок. Хороші практики кодування вимагають від нас не підтримувати стан під час потокових операцій. Наступне, що ми робимо - це кодування в такій рамці, як іскра Apache, і ті ж практики справді призведуть до несподіваних результатів. Це було творче рішення, я даю це, я, можливо, написав сам себе не так давно.
YoYo

1
@JoD Це не найкраще рішення, це фактично більш неефективне. Ця думка в кінцевому підсумку закінчується висновком, що всі колекції за замовчуванням повинні бути безпечними для запобігання ненавмисних наслідків, що просто неправильно.
Ніколя

301

Для цього може бути використаний колектор .

  • Для двох категорій використовуйте Collectors.partitioningBy()заводські.

Це створить " Mapвід" Booleanдо Listта додасть елементи в той чи інший список на основі Predicate.

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

Крім того, немає необхідності в ітераторі, навіть у прикладі, який ви надали лише для голов.

  • Двійкове розщеплення виглядає приблизно так:
Random r = new Random();

Map<Boolean, List<String>> groups = stream
    .collect(Collectors.partitioningBy(x -> r.nextBoolean()));

System.out.println(groups.get(false).size());
System.out.println(groups.get(true).size());
  • Для інших категорій використовуйте Collectors.groupingBy()фабрику.
Map<Object, List<String>> groups = stream
    .collect(Collectors.groupingBy(x -> r.nextInt(3)));
System.out.println(groups.get(0).size());
System.out.println(groups.get(1).size());
System.out.println(groups.get(2).size());

Якщо потоків немає Stream, але подібний один із примітивних потоків IntStream, то цей .collect(Collectors)метод недоступний. Вам доведеться це зробити ручним способом без фабрики колекціонерів. Його реалізація виглядає приблизно так:

[Приклад 2.0 з 2020-04-16]

    IntStream    intStream = IntStream.iterate(0, i -> i + 1).limit(100000).parallel();
    IntPredicate predicate = ignored -> r.nextBoolean();

    Map<Boolean, List<Integer>> groups = intStream.collect(
            () -> Map.of(false, new ArrayList<>(100000),
                         true , new ArrayList<>(100000)),
            (map, value) -> map.get(predicate.test(value)).add(value),
            (map1, map2) -> {
                map1.get(false).addAll(map2.get(false));
                map1.get(true ).addAll(map2.get(true ));
            });

У цьому прикладі я ініціалізую ArrayLists з повним розміром початкової колекції (якщо це взагалі відомо). Це запобігає подіям зміни розміру навіть у гіршому випадку, але може потенційно збільшити простір 2 * N * T (N = початкова кількість елементів, T = кількість потоків). Щоб випромінювати простір для швидкості, ви можете залишити його поза межами або використати найкращу освічену здогадку, як-от очікувану найбільшу кількість елементів в одній секції (як правило, трохи більше N / 2 для збалансованого поділу).

Я сподіваюся, що я нікого не ображу, використовуючи метод Java 9. Для версії Java 8 подивіться історію редагування.


2
Гарний. Однак останнє рішення для IntStream не буде безпечним для потоків у випадку паралельного потоку. Рішення набагато простіше, ніж ви думаєте, це ... stream.boxed().collect(...);! Це зробить так, як рекламується: перетворити примітив IntStreamу коробку Stream<Integer>.
YoYo

32
Це має бути прийнятою відповіддю, оскільки вона безпосередньо вирішує питання ОП.
ejel

27
Я хочу, щоб переповнення стека дозволило громаді перекрити обрану відповідь, якщо буде знайдена краща.
GuiSim

Я не впевнений, що це відповідає на питання. Питання вимагає розділити потік на потоки - не Списки.
АлікЕльзін-кілака

1
Функція акумулятора зайво багатослівна. Замість (map, x) -> { boolean partition = p.test(x); List<Integer> list = map.get(partition); list.add(x); }вас можна просто використовувати (map, x) -> map.get(p.test(x)).add(x). Далі я не бачу жодної причини, чому collectоперація не повинна бути безпечною для потоків. Він працює точно так, як це повинно працювати, і дуже близько до того, як Collectors.partitioningBy(p)би працював. Але я б IntPredicateзамість того, Predicate<Integer>щоб не використовувати boxed(), щоб уникнути боксу двічі.
Холгер

21

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

class PredicateSplitterConsumer<T> implements Consumer<T>
{
  private Predicate<T> predicate;
  private Consumer<T>  positiveConsumer;
  private Consumer<T>  negativeConsumer;

  public PredicateSplitterConsumer(Predicate<T> predicate, Consumer<T> positive, Consumer<T> negative)
  {
    this.predicate = predicate;
    this.positiveConsumer = positive;
    this.negativeConsumer = negative;
  }

  @Override
  public void accept(T t)
  {
    if (predicate.test(t))
    {
      positiveConsumer.accept(t);
    }
    else
    {
      negativeConsumer.accept(t);
    }
  }
}

Тепер реалізація вашого коду може бути приблизно такою:

personsArray.forEach(
        new PredicateSplitterConsumer<>(
            person -> person.getDateOfBirth().isPresent(),
            person -> System.out.println(person.getName()),
            person -> System.out.println(person.getName() + " does not have Date of birth")));

20

На жаль, те, що ви просите, прямо нахмуриться в JavaDoc of Stream :

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

Ви можете обійти це за допомогою peek інших методів, якщо справді бажаєте такого типу поведінки. У цьому випадку те, що ви повинні зробити, це замість того, щоб намагатися створити резервне копіювання двох потоків з одного оригінального джерела потоку за допомогою розгалужувального фільтра, ви б дублювали свій потік і фільтрували кожен дублікат відповідним чином.

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


6
Формулювання javadoc не виключає розподілу на декілька потоків, якщо один елемент потоку йде лише в одному з них
Thorbjørn Ravn Andersen

2
@ ThorbjørnRavnAndersen Я не впевнений, що дублювання елемента потоку є головною перешкодою для роздвоєного потоку. Основне питання полягає в тому, що операція форкінгу - це по суті термінальна операція, тому, коли ви вирішили розщедритися, ви в основному створюєте якусь колекцію. Наприклад, я можу написати метод, List<Stream> forkStream(Stream s)але отримані потоки принаймні частково будуть підкріплені колекціями, а не безпосередньо базовим потоком, на відміну від того, що говорити, filterщо не є операцією кінцевого потоку.
Тревор Фріман

7
Це одна з причин, чому я вважаю, що потоки Java трохи напівзмінені порівняно з github.com/ReactiveX/RxJava/wiki, оскільки суть потоку полягає в застосуванні операцій над потенційно нескінченним набором елементів, а операції в реальному світі часто вимагають розщеплення , дублювання та об'єднання потоків.
Усман Ісмаїл

8

Це проти загального механізму Потоку. Скажіть, ви можете розділити Stream S0 на Sa і Sb так, як хотіли. Виконуючи будь-яку операцію терміналу, скажімо count(), на Sa обов’язково "споживають" всі елементи в S0. Тому Sb втратив джерело даних.

Раніше Stream мав tee()метод, який я думаю, що дублює потік на два. Зараз його видалено.

У потоці є метод peek (), проте ви можете використовувати його для досягнення своїх вимог.


1
peekсаме те, що було раніше tee.
Луї Вассерман

5

не зовсім так, але ви, можливо, зможете виконати те, що вам потрібно, посилаючись Collectors.groupingBy(). ви створюєте нову колекцію, а потім можете створювати потоки для цієї нової колекції.


2

Це була найменш погана відповідь, яку я міг придумати.

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

public class Test {

    public static <T, L, R> Pair<L, R> splitStream(Stream<T> inputStream, Predicate<T> predicate,
            Function<Stream<T>, L> trueStreamProcessor, Function<Stream<T>, R> falseStreamProcessor) {

        Map<Boolean, List<T>> partitioned = inputStream.collect(Collectors.partitioningBy(predicate));
        L trueResult = trueStreamProcessor.apply(partitioned.get(Boolean.TRUE).stream());
        R falseResult = falseStreamProcessor.apply(partitioned.get(Boolean.FALSE).stream());

        return new ImmutablePair<L, R>(trueResult, falseResult);
    }

    public static void main(String[] args) {

        Stream<Integer> stream = Stream.iterate(0, n -> n + 1).limit(10);

        Pair<List<Integer>, String> results = splitStream(stream,
                n -> n > 5,
                s -> s.filter(n -> n % 2 == 0).collect(Collectors.toList()),
                s -> s.map(n -> n.toString()).collect(Collectors.joining("|")));

        System.out.println(results);
    }

}

Це займає потік цілих чисел і розбиває їх на 5. Для тих, що перевищують 5, він фільтрує лише парні числа і вносить їх у список. Для решти він приєднує їх до |.

Виходи:

 ([6, 8],0|1|2|3|4|5)

Це не ідеально, оскільки він збирає все в посередницькі колекції, що порушують потік (і має занадто багато аргументів!)


1

Я наткнувся на це питання, шукаючи спосіб відфільтрувати певні елементи з потоку та записати їх як помилки. Тож мені не дуже потрібно було розділяти потік настільки, щоб приєднати передчасно закінчувальну дію до присудка з ненав’язливим синтаксисом. Ось що я придумав:

public class MyProcess {
    /* Return a Predicate that performs a bail-out action on non-matching items. */
    private static <T> Predicate<T> withAltAction(Predicate<T> pred, Consumer<T> altAction) {
    return x -> {
        if (pred.test(x)) {
            return true;
        }
        altAction.accept(x);
        return false;
    };

    /* Example usage in non-trivial pipeline */
    public void processItems(Stream<Item> stream) {
        stream.filter(Objects::nonNull)
              .peek(this::logItem)
              .map(Item::getSubItems)
              .filter(withAltAction(SubItem::isValid,
                                    i -> logError(i, "Invalid")))
              .peek(this::logSubItem)
              .filter(withAltAction(i -> i.size() > 10,
                                    i -> logError(i, "Too large")))
              .map(SubItem::toDisplayItem)
              .forEach(this::display);
    }
}

0

Коротша версія, яка використовує Lombok

import java.util.function.Consumer;
import java.util.function.Predicate;

import lombok.RequiredArgsConstructor;

/**
 * Forks a Stream using a Predicate into postive and negative outcomes.
 */
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PROTECTED)
public class StreamForkerUtil<T> implements Consumer<T> {
    Predicate<T> predicate;
    Consumer<T> positiveConsumer;
    Consumer<T> negativeConsumer;

    @Override
    public void accept(T t) {
        (predicate.test(t) ? positiveConsumer : negativeConsumer).accept(t);
    }
}

-3

Як щодо:

Supplier<Stream<Integer>> randomIntsStreamSupplier =
    () -> (new Random()).ints(0, 2).boxed();

Stream<Integer> tails =
    randomIntsStreamSupplier.get().filter(x->x.equals(0));
Stream<Integer> heads =
    randomIntsStreamSupplier.get().filter(x->x.equals(1));

1
Оскільки постачальник викликається двічі, ви отримаєте дві різні випадкові колекції. Я думаю, що розум ОП розбиває шанси від евен у тій же створеній послідовності
usr-local-ΕΨΗΕΛΩΝ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.