Kopieren Sie einen Stream, um zu vermeiden, dass der Stream bereits bearbeitet oder geschlossen wurde.


120

Ich möchte einen Java 8-Stream duplizieren, damit ich zweimal damit umgehen kann. Ich kann collectals Liste und neue Streams daraus bekommen;

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

Aber ich denke, es sollte einen effizienteren / eleganteren Weg geben.

Gibt es eine Möglichkeit, den Stream zu kopieren, ohne ihn in eine Sammlung umzuwandeln?

Ich arbeite tatsächlich mit einem Stream von Eithers, möchte also die linke Projektion auf eine Weise verarbeiten, bevor ich auf die rechte Projektion übergehe und auf eine andere Weise damit umgehe. Ein bisschen wie dieses (mit dem ich bisher gezwungen bin, den toListTrick anzuwenden ).

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 -> ... );

Könnten Sie näher auf "Einweg verarbeiten" eingehen ... verbrauchen Sie die Objekte? Mapping sie? partitionBy () und groupingBy () können Sie direkt zu 2+ Listen führen, aber Sie können davon profitieren, zuerst zuzuordnen oder nur eine Entscheidungsgabel in forEach () zu haben.
AjahnCharles

In einigen Fällen kann die Umwandlung in eine Sammlung keine Option sein, wenn es sich um einen unendlichen Stream handelt. Eine Alternative zum Auswendiglernen
Miguel Gamboa

Antworten:


88

Ich denke, Ihre Annahme über Effizienz ist etwas rückwärts. Sie erhalten diese enorme Amortisation, wenn Sie die Daten nur einmal verwenden, da Sie sie nicht speichern müssen. Streams bieten Ihnen leistungsstarke "Loop Fusion" -Optimierungen, mit denen Sie die gesamten Daten effizient durch die Pipeline fließen lassen können.

Wenn Sie dieselben Daten wiederverwenden möchten, müssen Sie sie per Definition entweder zweimal (deterministisch) generieren oder speichern. Wenn es sich bereits in einer Sammlung befindet, großartig; dann ist es billig, es zweimal zu wiederholen.

Wir haben im Design mit "gegabelten Streams" experimentiert. Was wir fanden, war, dass die Unterstützung echte Kosten hatte; es belastete den allgemeinen Fall (einmalige Verwendung) auf Kosten des ungewöhnlichen Falls. Das große Problem bestand darin, "was passiert, wenn die beiden Pipelines nicht mit der gleichen Rate Daten verbrauchen". Jetzt sind Sie sowieso wieder beim Puffern. Dies war ein Merkmal, das offensichtlich nicht das Gewicht hatte.

Wenn Sie dieselben Daten wiederholt bearbeiten möchten, speichern Sie sie entweder oder strukturieren Sie Ihre Vorgänge als Verbraucher und gehen Sie wie folgt vor:

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

Sie können sich auch die RxJava-Bibliothek ansehen, da sich das Verarbeitungsmodell besser für diese Art von "Stream Forking" eignet.


1
Vielleicht hätte ich "Effizienz" nicht verwenden sollen, ich verstehe, warum ich mich mit Streams beschäftigen (und nichts speichern) sollte, wenn ich nur sofort die Daten ( toList) speichere , um sie verarbeiten zu können (der EitherFall) als Beispiel)?
Toby

11
Streams sind sowohl ausdrucksstark als auch effizient . Sie sind insofern ausdrucksstark, als Sie komplexe Aggregatoperationen ohne viele zufällige Details (z. B. Zwischenergebnisse) beim Lesen des Codes einrichten können. Sie sind auch insofern effizient, als sie (im Allgemeinen) die Daten einmalig weitergeben und keine Zwischenergebniscontainer füllen. Diese beiden Eigenschaften zusammen machen sie zu einem attraktiven Programmiermodell für viele Situationen. Natürlich passen nicht alle Programmiermodelle zu allen Problemen. Sie müssen sich noch entscheiden, ob Sie ein geeignetes Tool für den Job verwenden.
Brian Goetz

1
Die Unfähigkeit, einen Stream wiederzuverwenden, führt jedoch zu Situationen, in denen der Entwickler gezwungen ist, Zwischenergebnisse (Sammeln) zu speichern, um einen Stream auf zwei verschiedene Arten zu verarbeiten. Die Implikation, dass der Stream mehr als einmal generiert wird (es sei denn, Sie erfassen ihn), scheint klar zu sein - da Sie sonst keine Erfassungsmethode benötigen würden.
Niall Connaughton

@ NiallConnaughton Ich bin nicht sicher, ob Ihr Punkt ist. Wenn Sie es zweimal durchlaufen möchten, muss es jemand speichern oder neu generieren. Schlagen Sie vor, dass die Bibliothek sie puffern sollte, falls jemand sie zweimal benötigt? Das wäre dumm.
Brian Goetz

Dies bedeutet nicht, dass die Bibliothek es puffern soll, sondern dass Personen, die einen Seed-Stream wiederverwenden möchten (dh die zur Definition verwendete deklarative Logik gemeinsam nutzen möchten), mehrere abgeleitete Streams erstellen müssen, um sie zu sammeln den Seed-Stream oder Zugriff auf eine Provider-Factory, die ein Duplikat des Seed-Streams erstellt. Beide Optionen haben ihre Schwachstellen. Diese Antwort enthält viel mehr Details zum Thema: stackoverflow.com/a/28513908/114200 .
Niall Connaughton

73

Sie können eine lokale Variable mit a verwenden Supplier, um allgemeine Teile der Stream-Pipeline einzurichten.

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

Streams wiederverwenden

Java 8-Streams können nicht wiederverwendet werden. Sobald Sie eine Terminaloperation aufrufen, wird der Stream geschlossen:

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)

Um diese Einschränkung zu überwinden, müssen wir für jede Terminaloperation, die wir ausführen möchten, eine neue Stream-Kette erstellen, z. B. könnten wir einen Stream-Lieferanten erstellen, um einen neuen Stream mit allen bereits eingerichteten Zwischenoperationen zu erstellen:

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

Jeder Aufruf zum get()Erstellen eines neuen Streams, in dem wir gespeichert sind, um die gewünschte Terminaloperation aufzurufen.


2
schöne und elegante Lösung. viel mehr Java8-ish als die am besten bewertete Lösung.
Dylaniato

Nur ein Hinweis zur Verwendung, Supplierwenn das Streammit einer "kostspieligen" Weise gebaut wird, zahlen Sie diese Kosten für jeden Anruf anSupplier.get() . dh wenn eine Datenbankabfrage ... diese Abfrage jedes Mal durchgeführt wird
Julien

Sie können diesem Muster nach einer mapTo nicht folgen, obwohl Sie einen IntStream verwenden. Ich stellte fest, dass ich es wieder in ein Set<Integer>using konvertieren musste collect(Collectors.toSet())... und ein paar Operationen daran durchführen musste. Ich wollte max()und wenn ein bestimmter Wert als zwei Operationen festgelegt wurde ...filter(d -> d == -1).count() == 1;
JGFMK

16

Verwenden Sie a Supplier, um den Stream für jede Beendigungsoperation zu erstellen.

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

Wenn Sie einen Stream dieser Sammlung benötigen streamSupplier.get(), können Sie einen neuen Stream abrufen.

Beispiele:

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

Stimmen Sie zu, da Sie als erster auf Lieferanten hingewiesen haben.
EnzoBnl

9

Wir haben eine duplicate()Methode für Streams in jOOλ implementiert , einer Open Source-Bibliothek, die wir erstellt haben, um die Integrationstests für jOOQ zu verbessern . Im Wesentlichen können Sie einfach schreiben:

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

Intern gibt es einen Puffer, in dem alle Werte gespeichert sind, die von einem Stream verbraucht wurden, aber nicht von dem anderen. Das ist wahrscheinlich so effizient wie es nur geht, wenn Ihre beiden Streams ungefähr gleich schnell verbraucht werden und wenn Sie mit dem Mangel an Thread-Sicherheit leben können .

So funktioniert der Algorithmus:

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()));
}

Mehr Quellcode hier

Tuple2ist wahrscheinlich wie Ihr Pairwährend Typ, Seqist Streammit einigen Verbesserungen.


2
Diese Lösung ist nicht threadsicher: Sie können keinen der Streams an einen anderen Thread übergeben. Ich sehe wirklich kein Szenario, in dem beide Streams in einem Thread gleich schnell konsumiert werden können und Sie tatsächlich zwei unterschiedliche Streams benötigen. Wenn Sie zwei Ergebnisse aus demselben Stream erstellen möchten, ist es viel besser, kombinierte Kollektoren zu verwenden (die Sie bereits in JOOL haben).
Tagir Valeev

@TagirValeev: Sie haben Recht mit der Thread-Sicherheit, guter Punkt. Wie könnte dies mit der Kombination von Sammlern geschehen?
Lukas Eder

1
Ich meine, wenn jemand denselben Stream zweimal so verwenden möchte Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());, ist es besser Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. Die Verwendung von Collectors.mapping/reducingone kann andere Stream-Operationen als Kollektoren und Prozesselemente auf ganz andere Weise ausdrücken und ein einzelnes resultierendes Tupel erzeugen. Im Allgemeinen können Sie also viele Dinge tun, indem Sie den Stream einmal ohne Duplizierung verbrauchen, und er ist parallel.
Tagir Valeev

2
In diesem Fall reduzieren Sie immer noch einen Stream nach dem anderen. Es macht also keinen Sinn, das Leben schwieriger zu machen, indem man den weichen Iterator einführt, der sowieso den gesamten Stream auf der Liste unter der Haube sammelt. Sie können einfach explizit in der Liste sammeln und dann zwei Streams daraus erstellen, wie OP sagt (es ist die gleiche Anzahl von Codezeilen). Nun, Sie können möglicherweise nur eine gewisse Verbesserung erzielen, wenn die erste Reduzierung ein Kurzschluss ist, aber dies ist nicht der OP-Fall.
Tagir Valeev

1
@maaartinus: Danke, guter Zeiger. Ich habe ein Problem für den Benchmark erstellt. Ich habe es für die offer()/ poll()API verwendet, aber es ArrayDequekönnte genauso sein.
Lukas Eder

7

Sie können beispielsweise einen Stream mit ausführbaren Dateien erstellen:

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

Wo failureund wo successsind die Operationen anzuwenden? Dies erstellt jedoch einige temporäre Objekte und ist möglicherweise nicht effizienter, als von einer Sammlung auszugehen und sie zweimal zu streamen / zu iterieren.


4

Eine andere Möglichkeit, die Elemente mehrmals zu behandeln, ist die Verwendung von Stream.peek (Consumer) :

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

peek(Consumer) kann so oft wie nötig verkettet werden.

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

Es scheint, dass Peek dafür nicht verwendet werden soll (siehe softwareengineering.stackexchange.com/a/308979/195787 )
HectorJ

2
@HectorJ Im anderen Thread geht es um das Ändern von Elementen. Ich nahm an, dass das hier nicht gemacht wird.
Martin

2

cyclops-react , eine Bibliothek, zu der ich beitrage, verfügt über eine statische Methode, mit der Sie einen Stream duplizieren können (und ein jOOλ-Tupel von Streams zurückgibt).

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

Siehe Kommentare, es gibt Leistungseinbußen, die auftreten, wenn Duplikate in einem vorhandenen Stream verwendet werden. Eine leistungsfähigere Alternative wäre die Verwendung von Streamable:

Es gibt auch eine (faule) Streamable-Klasse, die aus einem Stream, Iterable oder Array erstellt und mehrmals wiedergegeben werden kann.

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

AsStreamable.synchronizedFromStream (Stream) - kann verwendet werden, um ein Streamable zu erstellen, das die Backing-Sammlung träge auf eine Weise auffüllt, die für mehrere Threads freigegeben werden kann. Streamable.fromStream (Stream) verursacht keinen Synchronisationsaufwand.


2
Und natürlich sollte beachtet werden, dass die resultierenden Streams einen erheblichen CPU- / Speicher-Overhead und eine sehr schlechte parallele Leistung aufweisen. Auch diese Lösung ist nicht threadsicher (Sie können einen der resultierenden Streams nicht an einen anderen Thread übergeben und ihn sicher parallel verarbeiten). Es wäre viel performanter und sicherer List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(wie OP vorschlägt). Bitte geben Sie in der Antwort auch ausdrücklich an, dass Sie der Autor von Cyclop-Streams sind. Lesen Sie dies .
Tagir Valeev

Aktualisiert, um zu reflektieren, dass ich der Autor bin. Auch ein guter Punkt, um die Leistungsmerkmale der einzelnen zu diskutieren. Ihre obige Einschätzung ist für StreamUtils.duplicate ziemlich genau richtig. StreamUtils.duplicate puffert Daten von einem Stream zum anderen und verursacht sowohl CPU- als auch Speicher-Overhead (je nach Anwendungsfall). Für Streamable.of (1,2,3) wird jedoch jedes Mal ein neuer Stream direkt aus dem Array erstellt, und die Leistungsmerkmale, einschließlich der parallelen Leistung, sind dieselben wie für normal erstellte Streams.
John McClean

Es gibt auch eine AsStreamable-Klasse, die das Erstellen einer Streamable-Instanz aus einem Stream ermöglicht, aber den Zugriff auf die Sammlung synchronisiert, die das Streamable beim Erstellen unterstützt (AsStreamable.synchronizedFromStream). Es eignet sich besser für die Verwendung über Threads hinweg (wenn Sie dies benötigen - ich würde mir vorstellen, dass 99% der Zeit Streams erstellt und auf demselben Thread wiederverwendet werden).
John McClean

Hallo Tagir, sollten Sie in Ihrem Kommentar nicht auch angeben, dass Sie Autor einer konkurrierenden Bibliothek sind?
John McClean

1
Kommentare sind keine Antworten und ich mache hier keine Werbung für meine Bibliothek, da meine Bibliothek keine Funktion zum Duplizieren des Streams hat (nur weil ich denke, dass dies nutzlos ist), sodass wir hier nicht konkurrieren. Wenn ich eine Lösung für meine Bibliothek vorschlage, sage ich natürlich immer ausdrücklich, dass ich der Autor bin.
Tagir Valeev

0

Für dieses spezielle Problem können Sie auch die Partitionierung verwenden. Etwas wie

     // 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

Wir können Stream Builder zum Zeitpunkt des Lesens oder Iterierens eines Streams verwenden. Hier ist das Dokument von Stream Builder .

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

Anwendungsfall

Angenommen, wir haben einen Mitarbeiter-Stream und müssen diesen Stream verwenden, um Mitarbeiterdaten in eine Excel-Datei zu schreiben und dann die Mitarbeitersammlung / -tabelle zu aktualisieren. [Dies ist nur ein Anwendungsfall, um die Verwendung von Stream Builder zu zeigen]:

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

Ich hatte ein ähnliches Problem und konnte mir drei verschiedene Zwischenstrukturen vorstellen, aus denen ich eine Kopie des Streams erstellen konnte: a List, ein Array und a Stream.Builder. Ich habe ein kleines Benchmark-Programm geschrieben, das darauf hinwies, dass es aus Sicht der Leistung Listetwa 30% langsamer war als die beiden anderen, die ziemlich ähnlich waren.

Der einzige Nachteil beim Konvertieren in ein Array besteht darin, dass es schwierig ist, wenn Ihr Elementtyp ein generischer Typ ist (was in meinem Fall der Fall war). deshalb bevorzuge ich a Stream.Builder.

Am Ende habe ich eine kleine Funktion geschrieben, die Folgendes erzeugt 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);
}

Ich kann dann eine Kopie jedes Streams erstellen, strindem ich str.collect(copyCollector())dies tue, was der idiomatischen Verwendung von Streams entspricht.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.