Скопіюйте потік, щоб уникнути "потоку вже було запущено або закрито"


121

Я хотів би дублювати потік Java 8, щоб я міг з ним двічі мати справу. Я можу collectяк список і отримувати з цього нові потоки;

// doSomething() returns a stream
List<A> thing = doSomething().collect(toList());
thing.stream()... // do stuff
thing.stream()... // do other stuff

Але я думаю, що має бути більш ефективний / елегантний спосіб.

Чи є спосіб скопіювати потік, не перетворюючи його на колекцію?

Я фактично працюю з потоком Eithers, тому хочу обробити ліву проекцію в один бік, перш ніж рухатися на праву проекцію і мати справу з цим іншим способом. Такий собі подібний (який, поки що, я змушений використовувати toListтрюк).

List<Either<Pair<A, Throwable>, A>> results = doSomething().collect(toList());

Stream<Pair<A, Throwable>> failures = results.stream().flatMap(either -> either.left());
failures.forEach(failure -> ... );

Stream<A> successes = results.stream().flatMap(either -> either.right());
successes.forEach(success -> ... );

Не могли б ви детальніше зупинитися на "обробці одним способом" ... Ви споживаєте об'єкти? Картографування їх? partitionBy () і groupingBy () можуть отримати вас безпосередньо до списків 2+, але ви можете скористатися спочатку картографуванням або просто розщепленням у своєму forEach ().
AjahnCharles

У деяких випадках перетворення його на колекцію не може бути варіантом, якщо ми маємо справу з нескінченним потоком. Ви можете знайти альтернативу для запам'ятовування тут: dzone.com/articles/how-to-replay-java-streams
Мігель Гамбоа

Відповіді:


88

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

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

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

Якщо ви хочете неодноразово працювати з одними і тими ж даними, або зберігайте їх, або структуруйте свої операції як Споживачі та виконайте наступне:

stream()...stuff....forEach(e -> { consumerA(e); consumerB(e); });

Ви також можете заглянути в бібліотеку RxJava, оскільки її модель обробки краще піддається такому типу "потокової розв'язки".


1
Можливо, я не повинен був використовувати "ефективність", я начебто розбираюся, чому б я морочився потоками (а не зберігав нічого), якщо все, що я роблю, - це негайно зберігати дані ( toList), щоб мати можливість їх обробляти ( Eitherсправа бути прикладом)?
Тобі

11
Потоки є одночасно виразними та ефективними . Вони виразні тим, що дозволяють налаштувати складні сукупні операції без великої кількості випадкових деталей (наприклад, проміжних результатів) у способі читання коду. Вони також ефективні, оскільки вони (як правило) роблять один пропуск даних і не заповнюють контейнери з проміжними результатами. Ці два властивості разом роблять їх привабливою моделлю програмування для багатьох ситуацій. Звичайно, не всі моделі програмування відповідають усім проблемам; вам все-таки потрібно вирішити, чи використовуєте ви відповідний інструмент для роботи.
Брайан Гетц

1
Але неможливість повторного використання потоку викликає ситуації, коли розробник змушений зберігати проміжні результати (збирати), щоб обробити потік двома різними способами. Наслідок того, що потік генерується не один раз (якщо ви не збираєте його), здається зрозумілим - тому що в іншому випадку вам не знадобиться метод збирання.
Niall Connaughton

@NiallConnaughton Я не впевнений, що я хочу, щоб ця думка була. Якщо ви хочете двічі пройти її, хтось повинен її зберігати, або вам доведеться її регенерувати. Ви припускаєте, що бібліотека повинна зберігати її лише на випадок, якщо комусь потрібно двічі? Це було б нерозумно.
Брайан Гетц

Не пропонуючи, що бібліотека повинна буферувати її, але кажучи, що, маючи потоки як одноразові, це змушує людей, які хочуть повторно використовувати насіннєвий потік (тобто: спільна декларативна логіка, що використовується для його визначення), створити декілька похідних потоків або збирати насіннєвий потік або мати доступ до фабрики постачальників, яка створить дублікат насінного потоку. Обидва варіанти мають свої больові точки. У цій відповіді детальніше на тему: stackoverflow.com/a/28513908/114200 .
Niall Connaughton

73

Ви можете використовувати локальну змінну з a Supplierдля встановлення загальних частин потокового трубопроводу.

З http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ :

Повторне використання потоків

Потоки Java 8 не можуть бути використані повторно. Як тільки ви викликаєте будь-яку операцію терміналу, потік закривається:

Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> s.startsWith("a"));
stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

Calling `noneMatch` after `anyMatch` on the same stream results in the following exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
at 
java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at 
java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)

Для подолання цього обмеження нам потрібно створити новий ланцюжок потоків для кожної операції терміналу, яку ми хочемо виконати, наприклад, ми могли б створити постачальника потоку для побудови нового потоку з усіма вже створеними проміжними операціями:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

Кожен виклик get()створює новий потік, на якому ми економимо, щоб викликати потрібну операцію терміналу.


2
приємне та елегантне рішення. набагато більше Java8-ish, ніж найбільш схвалене рішення.
диланіато

Лише зауваження про використання, Supplierякщо Streamпобудований "дорого", ви платите ці витрати за кожен дзвінокSupplier.get() . тобто якщо запит до бази даних ... цей запит робиться щоразу
Жульєн

Здається, ви не можете слідувати цій схемі після mapTo, хоча використовуючи IntStream. Я виявив, що мені довелося перетворити його назад у Set<Integer>користувальний collect(Collectors.toSet())... і зробити пару операцій над цим. Я хотів, max()і якщо певне значення було встановлено як дві операції ...filter(d -> d == -1).count() == 1;
JGFMK

16

Використовуйте a Supplierдля створення потоку для кожної операції припинення.

Supplier<Stream<Integer>> streamSupplier = () -> list.stream();

Кожен раз, коли вам потрібен потік цієї колекції, використовуйте streamSupplier.get()для отримання нового потоку.

Приклади:

  1. streamSupplier.get().anyMatch(predicate);
  2. streamSupplier.get().allMatch(predicate2);

Оголошуємо вас, як ви першими вказали на Постачальників тут.
EnzoBnl

9

Ми реалізували duplicate()метод для потоків у jOOλ , бібліотеку з відкритим кодом, яку ми створили для поліпшення тестування інтеграції для jOOQ . По суті, ви можете просто написати:

Tuple2<Seq<A>, Seq<A>> duplicates = Seq.seq(doSomething()).duplicate();

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

Ось як працює алгоритм:

static <T> Tuple2<Seq<T>, Seq<T>> duplicate(Stream<T> stream) {
    final List<T> gap = new LinkedList<>();
    final Iterator<T> it = stream.iterator();

    @SuppressWarnings("unchecked")
    final Iterator<T>[] ahead = new Iterator[] { null };

    class Duplicate implements Iterator<T> {
        @Override
        public boolean hasNext() {
            if (ahead[0] == null || ahead[0] == this)
                return it.hasNext();

            return !gap.isEmpty();
        }

        @Override
        public T next() {
            if (ahead[0] == null)
                ahead[0] = this;

            if (ahead[0] == this) {
                T value = it.next();
                gap.offer(value);
                return value;
            }

            return gap.poll();
        }
    }

    return tuple(seq(new Duplicate()), seq(new Duplicate()));
}

Більше вихідного коду тут

Tuple2певно, як ваш Pairтип, тоді Seqяк Streamз деякими вдосконаленнями.


2
Це рішення не є безпечним для потоків: ви не можете передати один з потоків до іншого потоку. Я дійсно не бачу жодного сценарію, коли обидва потоку можна використовувати в однаковій кількості з однаковою швидкістю, і вам потрібні два різних потоку. Якщо ви хочете отримати два результати з одного потоку, було б набагато краще використовувати комбінуючі колектори (які у вас уже є в JOOL).
Тагір Валєєв

@TagirValeev: Ти маєш рацію щодо безпеки потоку, хороший пункт. Як це можна було зробити з об'єднанням колекторів?
Лукас Едер

1
Я маю на увазі, якщо хтось хоче використати той самий потік вдвічі, як цей Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList()); , краще це зробити Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. Використовуючи Collectors.mapping/reducingодну, можна виразити інші потокові операції як колектори та обробляючі елементи зовсім різним чином, створюючи єдиний результуючий кортеж. Так що взагалі ви можете багато разів споживати потік один раз без дублювання, і це буде паралельно.
Тагір Валєєв

2
У цьому випадку ви все одно зменшите один потік за іншим. Тож немає сенсу ускладнювати життя, вводячи складний ітератор, який все одно збиратиме весь потік до списку під кришкою. Ви можете просто зібрати до списку явно, а потім створити з нього два потоки, як повідомляє ОП (це однакова кількість рядків коду). Що ж, ви можете мати певне поліпшення лише в тому випадку, якщо перше зниження має коротке замикання, але це не випадок ОП.
Тагір Валєєв

1
@maaartinus: Дякую, хороший вказівник. Я створив проблему для еталону. Я використовував це для offer()/ poll()API, але ArrayDequeможе зробити саме те саме.
Лукас Едер

7

Ви можете створити потік бігунів (наприклад):

results.stream()
    .flatMap(either -> Stream.<Runnable> of(
            () -> failure(either.left()),
            () -> success(either.right())))
    .forEach(Runnable::run);

Де failureі successякі операції слід застосовувати. Однак це створить досить багато тимчасових об'єктів і може виявитися неефективнішим, ніж починати з колекції та потоково / ітератувати її двічі.


4

Ще один спосіб поводження з елементами кілька разів - це використовувати Stream.peek (Consumer) :

doSomething().stream()
.peek(either -> handleFailure(either.left()))
.foreach(either -> handleSuccess(either.right()));

peek(Consumer) можна прикувати стільки разів, скільки потрібно.

doSomething().stream()
.peek(element -> handleFoo(element.foo()))
.peek(element -> handleBar(element.bar()))
.peek(element -> handleBaz(element.baz()))
.foreach(element-> handleQux(element.qux()));

Здається, peek не повинен використовуватись для цього (див. Softwareengineering.stackexchange.com/a/308979/195787 )
HectorJ

2
@HectorJ Інший потік стосується модифікації елементів. Я припускав, що тут не робиться.
Мартін

2

cyclops-react , бібліотека, до якої я сприяю, має статичний метод, який дозволить вам дублювати Stream (і повертає кордон потоків jOOλ).

    Stream<Integer> stream = Stream.of(1,2,3);
    Tuple2<Stream<Integer>,Stream<Integer>> streams =  StreamUtils.duplicate(stream);

Дивіться коментарі. Існує штраф за виконання, який буде застосовано при використанні дубліката в існуючому потоці. Більш ефективною альтернативою було б використання Streamable: -

Існує також (ледачий) клас Streamable, який можна побудувати з потоку, відключення або масиву та повторно повторювати.

    Streamable<Integer> streamable = Streamable.of(1,2,3);
    streamable.stream().forEach(System.out::println);
    streamable.stream().forEach(System.out::println);

AsStreamable.synchronizedFromStream (stream) - може використовуватися для створення Streamable, який буде ліниво заповнювати його резервну колекцію таким чином, щоб їх можна було поділити по потоках. Streamable.fromStream (потік) не матиме жодних накладних синхронізацій.


2
І, звичайно, слід зазначити, що отримані потоки мають значні витрати на процесор / пам'ять і дуже низьку паралельну продуктивність. Також це рішення не є безпечним для потоків (ви не можете передати один із отриманих потоків в інший потік і обробити його безпечно паралельно). Це було б набагато ефективніше і безпечніше List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(як пропонує ОП). Також, будь ласка, розкрийте прямо у відповіді, що ви є автором циклоп-потоків. Прочитайте це .
Тагір Валєєв

Оновлено, щоб відобразити, що я автор. Також хороший момент для обговорення характеристик виконання кожного. Ваша оцінка, що викладена вище, досить важлива для StreamUtils.duplicate. StreamUtils.duplicate працює, завантажуючи дані з одного потоку в інший, використовуючи як центральний процесор, так і пам'ять (залежно від випадку використання). Однак для Streamable.of (1,2,3) новий потік створюється безпосередньо з масиву кожного разу, і характеристики продуктивності, що включають паралельну продуктивність, будуть такими ж, як і для звичайно створеного потоку.
Джон МакКлін

Крім того, існує клас AsStreamable, який дозволяє створювати екземпляр Streamable з потоку, але синхронізує доступ до колекції, що підтримує Streamable під час її створення (AsStreamable.synchronizedFromStream). Зробити його більш придатним для використання в різних потоках (якщо це те, що вам потрібно - я б уявив, що 99% часу Потоки створюються та використовуються повторно на одній нитці).
Джон МакКлін

Привіт, Тагіре, чи не слід також в коментарі повідомити, що ви автор бібліотеки, що конкурує?
Джон МакКлін

1
Коментарі - це не відповіді, і я не рекламую свою бібліотеку тут, оскільки в моїй бібліотеці немає функції дублювання потоку (просто тому, що я вважаю, що це марно), тому ми тут не змагаємось. Звичайно, коли я пропоную рішення, що стосується моєї бібліотеки, я завжди прямо кажу, що я автор.
Тагір Валєєв

0

Для цієї конкретної проблеми можна використовувати також розділення. Щось на зразок

     // Partition Eighters into left and right
     List<Either<Pair<A, Throwable>, A>> results = doSomething();
     Map<Boolean, Object> passingFailing = results.collect(Collectors.partitioningBy(s -> s.isLeft()));
     passingFailing.get(true) <- here will be all passing (left values)
     passingFailing.get(false) <- here will be all failing (right values)

0

Ми можемо використовувати Stream Builder під час читання або повторення потоку. Ось документ Stream Builder .

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.Builder.html

Використовуйте футляр

Скажімо, у нас є потік службовців, і нам потрібно використовувати цей потік для запису даних працівника у файл excel, а потім оновлення колекції / таблиці службовців [Це просто випадок використання, щоб показати використання Stream Builder]:

Stream.Builder<Employee> builder = Stream.builder();

employee.forEach( emp -> {
   //store employee data to excel file 
   // and use the same object to build the stream.
   builder.add(emp);
});

//Now this stream can be used to update the employee collection
Stream<Employee> newStream = builder.build();

0

У мене була подібна проблема, і я міг придумати три різні проміжні структури, з яких створити копію потоку: a List, масив та a Stream.Builder. Я написав невелику орієнтирову програму, яка запропонувала, що з точки зору продуктивностіList була на 30% повільніше, ніж інші два, які були досить схожими.

Єдиним недоліком перетворення масиву є те, що це складно, якщо ваш тип елемента є загальним типом (що в моєму випадку це було); тому я вважаю за краще використовувати aStream.Builder .

Я закінчив написати невелику функцію, яка створює Collector:

private static <T> Collector<T, Stream.Builder<T>, Stream<T>> copyCollector()
{
    return Collector.of(Stream::builder, Stream.Builder::add, (b1, b2) -> {
        b2.build().forEach(b1);
        return b1;
    }, Stream.Builder::build);
}

Тоді я можу зробити копію будь-якого потоку str, зробивши це, str.collect(copyCollector())що повністю відповідає ідіоматичному використанню потоків.

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