Скопируйте поток, чтобы избежать «поток уже обработан или закрыт»


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
Возможно, мне не стоило использовать «эффективность», я как бы понимаю, зачем мне беспокоиться о потоках (и ничего не хранить), если все, что я делаю, это немедленно сохраняю data ( toList), чтобы иметь возможность их обработать ( Eitherслучай являясь примером)?
Toby

11
Стримы одновременно выразительны и эффективны . Они выразительны тем, что позволяют настраивать сложные агрегатные операции без множества случайных деталей (например, промежуточных результатов) при чтении кода. Они также эффективны в том смысле, что (как правило) выполняют один проход данных и не заполняют контейнеры промежуточных результатов. Эти два свойства вместе делают их привлекательной моделью программирования для многих ситуаций. Конечно, не все модели программирования подходят для всех задач; вам все равно нужно решить, используете ли вы подходящий инструмент для работы.
Брайан Гетц,

1
Но невозможность повторно использовать поток вызывает ситуации, когда разработчик вынужден сохранять промежуточные результаты (сбор), чтобы обрабатывать поток двумя разными способами. Подразумевается, что поток создается более одного раза (если вы его не собираете), кажется очевидным, потому что в противном случае вам не понадобился бы метод сбора.
Найл

@NiallConnaughton Я не уверен, что хочу вашу точку зрения. Если вы хотите пройти его дважды, кто-то должен его сохранить, или вам придется его регенерировать. Вы предлагаете, чтобы библиотека буферизовала его на случай, если кому-то понадобится дважды? Это было бы глупо.
Брайан Гетц

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

73

Вы можете использовать локальную переменную с, 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

Каждый вызов to get()создает новый поток, в котором мы сохраняемся, чтобы вызвать желаемую операцию терминала.


2
красивое и элегантное решение. гораздо больше java8-ish, чем решение, получившее наибольшее количество голосов.
dylaniato 02

Просто обратите внимание на использование, 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
В этом случае вы все равно будете уменьшать один поток за другим. Так что нет смысла усложнять жизнь введением сложного итератора, который в любом случае будет собирать весь поток в список под капотом. Вы можете просто собрать в список явно, а затем создать из него два потока, как сообщает OP (это такое же количество строк кода). Что ж, у вас может быть только некоторое улучшение, если первое сокращение связано с коротким замыканием, но это не случай OP.
Тагир Валеев

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 , библиотека, в которую я работаю, имеет статический метод, который позволит вам дублировать поток (и возвращает кортеж потоков jOOλ).

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

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

Существует также (ленивый) класс Streamable, который можно создать из Stream, Iterable или Array и многократно воспроизводить.

    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 (stream) не повлечет за собой никаких накладных расходов на синхронизацию.


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

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

Кроме того, существует класс AsStreamable, который позволяет создавать экземпляр Streamable из Stream, но синхронизирует доступ к коллекции, поддерживающей 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% медленнее, чем два других, которые были довольно похожи.

Единственный недостаток преобразования в массив состоит в том, что это сложно, если ваш тип элемента является универсальным типом (что в моем случае было); поэтому я предпочитаю использовать Stream.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.