Hinzufügen von zwei Java 8-Streams oder eines zusätzlichen Elements zu einem Stream

168

Ich kann Streams oder zusätzliche Elemente wie folgt hinzufügen:

Stream stream = Stream.concat(stream1, Stream.concat(stream2, Stream.of(element));

Und ich kann im Laufe der Zeit neue Dinge hinzufügen:

Stream stream = Stream.concat(
                       Stream.concat(
                              stream1.filter(x -> x!=0), stream2)
                              .filter(x -> x!=1),
                                  Stream.of(element))
                                  .filter(x -> x!=2);

Aber das ist hässlich, weil concates statisch ist. Wenn concateine Instanzmethode wäre, wären die obigen Beispiele viel einfacher zu lesen:

 Stream stream = stream1.concat(stream2).concat(element);

Und

 Stream stream = stream1
                 .filter(x -> x!=0)
                 .concat(stream2)
                 .filter(x -> x!=1)
                 .concat(element)
                 .filter(x -> x!=2);

Meine Frage ist:

1) Gibt es einen guten Grund, warum concatstatisch ist? Oder fehlt mir eine äquivalente Instanzmethode?

2) Gibt es auf jeden Fall einen besseren Weg, dies zu tun?

MarcG
quelle
4
Es sieht so aus , als ob die Dinge nicht immer so waren , aber ich kann den Grund dafür einfach nicht finden.
Edwin Dalorzo

Antworten:

126

Wenn Sie statische Importe für Stream.concat und Stream.of hinzufügen , könnte das erste Beispiel wie folgt geschrieben werden:

Stream<Foo> stream = concat(stream1, concat(stream2, of(element)));

Das Importieren statischer Methoden mit generischen Namen kann zu Code führen, der schwer zu lesen und zu warten ist ( Namespace-Verschmutzung ). Daher ist es möglicherweise besser, eigene statische Methoden mit aussagekräftigeren Namen zu erstellen . Zur Demonstration werde ich mich jedoch an diesen Namen halten.

public static <T> Stream<T> concat(Stream<? extends T> lhs, Stream<? extends T> rhs) {
    return Stream.concat(lhs, rhs);
}
public static <T> Stream<T> concat(Stream<? extends T> lhs, T rhs) {
    return Stream.concat(lhs, Stream.of(rhs));
}

Mit diesen beiden statischen Methoden (optional in Kombination mit statischen Importen) könnten die beiden Beispiele wie folgt geschrieben werden:

Stream<Foo> stream = concat(stream1, concat(stream2, element));

Stream<Foo> stream = concat(
                         concat(stream1.filter(x -> x!=0), stream2).filter(x -> x!=1),
                         element)
                     .filter(x -> x!=2);

Der Code ist jetzt deutlich kürzer. Ich stimme jedoch zu, dass sich die Lesbarkeit nicht verbessert hat. Ich habe also eine andere Lösung.


In vielen Situationen können Collectors verwendet werden, um die Funktionalität von Streams zu erweitern. Mit den beiden Sammlern unten könnten die beiden Beispiele wie folgt geschrieben werden:

Stream<Foo> stream = stream1.collect(concat(stream2)).collect(concat(element));

Stream<Foo> stream = stream1
                     .filter(x -> x!=0)
                     .collect(concat(stream2))
                     .filter(x -> x!=1)
                     .collect(concat(element))
                     .filter(x -> x!=2);

Der einzige Unterschied zwischen Ihrer gewünschten Syntax und der obigen Syntax besteht darin, dass Sie concat (...) durch collect (concat (...)) ersetzen müssen . Die beiden statischen Methoden können wie folgt implementiert werden (optional in Kombination mit statischen Importen verwendet):

private static <T,A,R,S> Collector<T,?,S> combine(Collector<T,A,R> collector, Function<? super R, ? extends S> function) {
    return Collector.of(
        collector.supplier(),
        collector.accumulator(),
        collector.combiner(),
        collector.finisher().andThen(function));
}
public static <T> Collector<T,?,Stream<T>> concat(Stream<? extends T> other) {
    return combine(Collectors.toList(),
        list -> Stream.concat(list.stream(), other));
}
public static <T> Collector<T,?,Stream<T>> concat(T element) {
    return concat(Stream.of(element));
}

Natürlich gibt es bei dieser Lösung einen Nachteil, der erwähnt werden sollte. Das Sammeln ist eine letzte Operation, die alle Elemente des Streams verbraucht. Darüber hinaus erstellt der Collector Concat bei jeder Verwendung in der Kette eine Zwischen- ArrayList . Beide Vorgänge können erhebliche Auswirkungen auf das Verhalten Ihres Programms haben. Wenn jedoch die Lesbarkeit wichtiger ist als die Leistung , ist dies möglicherweise immer noch ein sehr hilfreicher Ansatz.

nosid
quelle
1
Ich finde den concatSammler nicht gut lesbar. Es scheint seltsam, eine statische Einzelparameter-Methode wie diese zu haben und sie auch collectfür die Verkettung zu verwenden.
Didier L
@nosid, vielleicht eine leicht orthogonale Frage zu diesem Thread, aber warum behauptest du It's a bad idea to import static methods with names? Ich bin wirklich interessiert - ich finde, dass der Code dadurch prägnanter und lesbarer wird, und viele Leute, die ich gefragt hatte, dachten dasselbe. Möchten Sie einige Beispiele nennen, warum das im Allgemeinen schlecht ist?
Quantum
1
@ Quantum: Was bedeutet das compare(reverse(getType(42)), of(6 * 9).hashCode())? Beachten Sie, dass ich nicht gesagt habe, dass statische Importe eine schlechte Idee sind, aber statische Importe für generische Namen wie ofund concat.
Nosid
1
@nosid: Wird nicht in jeder modernen IDE über jeder Aussage schweben, um die Bedeutung schnell zu verraten? Ich denke jedenfalls, dass dies bestenfalls eine persönliche Präferenzerklärung sein könnte, da ich immer noch keinen technischen Grund sehe, warum statische Importe für "generische" Namen schlecht sind - es sei denn, Sie verwenden Notepad oder VI (M) für die Programmierung in diesem Fall Sie haben größere Probleme.
Quantum
Ich werde nicht sagen, dass das Scala SDK besser ist, aber ... hoppla, ich habe es gesagt.
Eirirlar
165

Leider ist diese Antwort wahrscheinlich wenig oder gar nicht hilfreich, aber ich habe eine forensische Analyse der Java Lambda-Mailingliste durchgeführt, um festzustellen, ob ich die Ursache für dieses Design finden konnte. Das habe ich herausgefunden.

Am Anfang gab es eine Instanzmethode für Stream.concat (Stream)

In der Mailingliste kann ich deutlich sehen, dass die Methode ursprünglich als Instanzmethode implementiert wurde, wie Sie in diesem Thread von Paul Sandoz über die Concat-Operation lesen können.

Darin diskutieren sie die Probleme, die sich aus den Fällen ergeben könnten, in denen der Stream unendlich sein könnte, und welche Verkettung in diesen Fällen bedeuten würde, aber ich denke nicht, dass dies der Grund für die Änderung war.

In diesem anderen Thread sehen Sie, dass einige frühe Benutzer des JDK 8 das Verhalten der Methode concat instance bei Verwendung mit Nullargumenten in Frage stellten.

Dieser andere Thread zeigt jedoch, dass das Design der Concat-Methode diskutiert wurde.

Refactored to Streams.concat (Stream, Stream)

Aber ohne jede Erklärung wurden die Methoden plötzlich in statische Methoden geändert, wie Sie in diesem Thread über das Kombinieren von Streams sehen können . Dies ist vielleicht der einzige Mail-Thread, der ein wenig Licht auf diese Änderung wirft, aber es war mir nicht klar genug, um den Grund für das Refactoring zu bestimmen. Aber wir können sehen, dass sie ein Commit durchgeführt haben, in dem sie vorgeschlagen haben, die concatMethode aus Streamund in die Helferklasse zu verschieben Streams.

Refactored to Stream.concat (Stream, Stream)

Später wurde es wieder von Streamsnach verschobenStream , aber noch einmal keine Erklärung dafür.

Unterm Strich ist der Grund für das Design für mich nicht ganz klar und ich konnte keine gute Erklärung finden. Ich denke, Sie könnten die Frage immer noch in der Mailingliste stellen.

Einige Alternativen zur Stream-Verkettung

In diesem anderen Thread von Michael Hixson werden andere Möglichkeiten zum Kombinieren / Konzentrieren von Streams erörtert

  1. Um zwei Streams zu kombinieren, sollte ich Folgendes tun:

    Stream.concat(s1, s2)

    nicht das:

    Stream.of(s1, s2).flatMap(x -> x)

    ... richtig?

  2. Um mehr als zwei Streams zu kombinieren, sollte ich Folgendes tun:

    Stream.of(s1, s2, s3, ...).flatMap(x -> x)

    nicht das:

    Stream.of(s1, s2, s3, ...).reduce(Stream.empty(), Stream::concat)

    ... richtig?

Edwin Dalorzo
quelle
6
+1 Gute Forschung. Und ich werde dies als meine Stream.concat verwenden, die varargs nimmt:public static <T> Stream<T> concat(Stream<T>... streams) { return Stream.of(streams).reduce(Stream.empty(), Stream::concat);}
MarcG
1
Heute habe ich meine eigene Concat-Version geschrieben und kurz danach finanziere ich dieses Thema. Die Signatur ist etwas anders, aber dank dieser ist sie allgemeiner;) Sie können beispielsweise Stream <Integer> und Stream <Double> zu Stream <Number> zusammenführen. @SafeVarargs private static <T> Stream<T> concat(Stream<? extends T>... streams) { return Stream.of(streams).reduce(Stream.empty(),Stream::concat).map(Function.identity());}
Kant
@kant Warum brauchst du eine Function.identity()Karte? Immerhin gibt es das gleiche Argument zurück, das es erhalten hat. Dies sollte im resultierenden Stream keine Auswirkung haben. Vermisse ich etwas
Edwin Dalorzo
1
Haben Sie versucht, es in Ihre IDE einzugeben? Ohne .map (identity ()) wird ein Kompilierungsfehler angezeigt. Ich möchte Stream <T> zurückgeben, aber Anweisung: return Stream.of(streams).reduce(Stream.empty(),Stream::concat)Gibt Stream <zurück? erweitert T>. (Someting <T> ist der Subtyp von Something <? erweitert T>, nicht in die andere Richtung, sodass es nicht gewirkt werden konnte.) Zusätzliche .map(identity())Besetzung <? erweitert T> bis <T>. Dies geschieht dank einer Mischung aus Java 8-Zieltypen von Methodenargumenten und Rückgabetypen sowie der Signatur der map () -Methode. Eigentlich ist es Function. <T> identity ().
Kant
1
@kant Ich sehe nicht viel Sinn darin ? extends T, da Sie die Capture-Konvertierung verwenden können . Hier ist auf jeden Fall mein Kerncode-Snippet. Lassen Sie uns die Diskussion im Kern fortsetzen.
Edwin Dalorzo
12

Meine StreamEx- Bibliothek erweitert die Funktionalität der Stream-API. Insbesondere bietet es Methoden wie Anhängen und Voranstellen, die dieses Problem lösen (intern verwenden sie concat). Diese Methoden können entweder einen anderen Stream oder eine andere Sammlung oder ein anderes Varargs-Array akzeptieren. Mit meiner Bibliothek kann Ihr Problem auf diese Weise gelöst werden (beachten Sie, dass es x != 0für nicht-primitive Streams seltsam aussieht):

Stream<Integer> stream = StreamEx.of(stream1)
             .filter(x -> !x.equals(0))
             .append(stream2)
             .filter(x -> !x.equals(1))
             .append(element)
             .filter(x -> !x.equals(2));

Übrigens gibt es auch eine Verknüpfung für Ihre filterOperation:

Stream<Integer> stream = StreamEx.of(stream1).without(0)
                                 .append(stream2).without(1)
                                 .append(element).without(2);
Tagir Valeev
quelle
9

Mach einfach:

Stream.of(stream1, stream2, Stream.of(element)).flatMap(identity());

Wo identity()ist ein statischer Import von Function.identity().

Das Verketten mehrerer Streams zu einem Stream entspricht dem Reduzieren eines Streams.

Leider gibt es aus irgendeinem Grund keine flatten()Methode Stream, sodass Sie sie flatMap()mit der Identitätsfunktion verwenden müssen.

ihr Mann
quelle
1

Wenn es Ihnen nichts ausmacht, Bibliotheken von Drittanbietern zu verwenden, verfügt cyclops-react über einen erweiterten Stream-Typ, mit dem Sie genau das über die Operatoren append / prepend ausführen können.

Einzelne Werte, Arrays, Iterables, Streams oder Reactive-Streams Publisher können als Instanzmethoden angehängt und vorangestellt werden.

Stream stream = ReactiveSeq.of(1,2)
                           .filter(x -> x!=0)
                           .append(ReactiveSeq.of(3,4))
                           .filter(x -> x!=1)
                           .append(5)
                           .filter(x -> x!=2);

[Offenlegung Ich bin der Hauptentwickler von Cyclops-React]

John McClean
quelle
1

Letztendlich bin ich nicht daran interessiert, Streams zu kombinieren, sondern das kombinierte Ergebnis der Verarbeitung jedes Elements all dieser Streams zu erhalten.

Während sich das Kombinieren von Streams als umständlich erweisen kann (daher dieser Thread), ist das Kombinieren ihrer Verarbeitungsergebnisse ziemlich einfach.

Der zu lösende Schlüssel besteht darin, einen eigenen Kollektor zu erstellen und sicherzustellen, dass die Lieferantenfunktion für den neuen Kollektor jedes Mal dieselbe Sammlung zurückgibt ( keine neue ). Der folgende Code veranschaulicht diesen Ansatz.

package scratchpad;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class CombineStreams {
    public CombineStreams() {
        super();
    }

    public static void main(String[] args) {
        List<String> resultList = new ArrayList<>();
        Collector<String, List<String>, List<String>> collector = Collector.of(
                () -> resultList,
                (list, item) -> {
                    list.add(item);
                },
                (llist, rlist) -> {
                    llist.addAll(rlist);
                    return llist;
                }
        );
        String searchString = "Wil";

        System.out.println("After processing first stream\n"
                + createFirstStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

        System.out.println("After processing second stream\n"
                + createSecondStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

        System.out.println("After processing third stream\n"
                + createThirdStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

    }

    private static Stream<String> createFirstStream() {
        return Arrays.asList(
                "William Shakespeare",
                "Emily Dickinson",
                "H. P. Lovecraft",
                "Arthur Conan Doyle",
                "Leo Tolstoy",
                "Edgar Allan Poe",
                "Robert Ervin Howard",
                "Rabindranath Tagore",
                "Rudyard Kipling",
                "Seneca",
                "John Donne",
                "Sarah Williams",
                "Oscar Wilde",
                "Catullus",
                "Alfred Tennyson",
                "William Blake",
                "Charles Dickens",
                "John Keats",
                "Theodor Herzl"
        ).stream();
    }

    private static Stream<String> createSecondStream() {
        return Arrays.asList(
                "Percy Bysshe Shelley",
                "Ernest Hemingway",
                "Barack Obama",
                "Anton Chekhov",
                "Henry Wadsworth Longfellow",
                "Arthur Schopenhauer",
                "Jacob De Haas",
                "George Gordon Byron",
                "Jack London",
                "Robert Frost",
                "Abraham Lincoln",
                "O. Henry",
                "Ovid",
                "Robert Louis Stevenson",
                "John Masefield",
                "James Joyce",
                "Clark Ashton Smith",
                "Aristotle",
                "William Wordsworth",
                "Jane Austen"
        ).stream();
    }

    private static Stream<String> createThirdStream() {
        return Arrays.asList(
                "Niccolò Machiavelli",
                "Lewis Carroll",
                "Robert Burns",
                "Edgar Rice Burroughs",
                "Plato",
                "John Milton",
                "Ralph Waldo Emerson",
                "Margaret Thatcher",
                "Sylvie d'Avigdor",
                "Marcus Tullius Cicero",
                "Banjo Paterson",
                "Woodrow Wilson",
                "Walt Whitman",
                "Theodore Roosevelt",
                "Agatha Christie",
                "Ambrose Bierce",
                "Nikola Tesla",
                "Franz Kafka"
        ).stream();
    }
}
Legna
quelle
0

Wie wäre es mit einer eigenen Concat-Methode?

public static Stream<T> concat(Stream<? extends T> a, 
                               Stream<? extends T> b, 
                               Stream<? extends T> args)
{
    Stream<T> concatenated = Stream.concat(a, b);
    for (Stream<T> stream : args)
    {
        concatenated = Stream.concat(concatenated, stream);
    }
    return concatenated;
}

Dies macht zumindest Ihr erstes Beispiel viel lesbarer.

Felix S.
quelle
1
Seien Sie vorsichtig, wenn Sie Streams aus wiederholter Verkettung erstellen. Der Zugriff auf ein Element eines stark verketteten Streams kann zu tiefen Aufrufketten oder sogar zu StackOverflowError führen.
Legna