Wann sollte ich Streams verwenden?

99

Ich bin gerade auf eine Frage gestoßen, als ich a Listund seine stream()Methode verwendet habe. Obwohl ich weiß, wie man sie benutzt, bin ich mir nicht ganz sicher, wann ich sie benutzen soll.

Zum Beispiel habe ich eine Liste, die verschiedene Pfade zu verschiedenen Orten enthält. Jetzt möchte ich prüfen, ob ein einzelner, angegebener Pfad einen der in der Liste angegebenen Pfade enthält. Ich möchte eine zurückgeben, booleanbasierend darauf, ob die Bedingung erfüllt ist oder nicht.

Dies ist natürlich an sich keine schwere Aufgabe. Aber ich frage mich, ob ich Streams oder eine for (-each) -Schleife verwenden soll.

Die Liste

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

Beispiel - Stream

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

Beispiel - Für jede Schleife

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

Beachten Sie, dass der pathParameter immer klein geschrieben ist .

Meine erste Vermutung ist, dass der Ansatz für jeden Ansatz schneller ist, da die Schleife sofort zurückkehren würde, wenn die Bedingung erfüllt ist. Während der Stream immer noch alle Listeneinträge durchläuft, um die Filterung abzuschließen.

Ist meine Annahme richtig? Wenn ja, warum (oder besser wann ) würde ich stream()dann verwenden?

mcuenez
quelle
11
Streams sind ausdrucksvoller und lesbarer als herkömmliche For-Loops. In letzterem Fall müssen Sie vorsichtig sein mit den Eigenschaften von Wenn-Dann und Bedingungen usw. Der Stream-Ausdruck ist sehr klar: Konvertieren Sie Dateinamen in Kleinbuchstaben, filtern Sie dann nach etwas und zählen, sammeln usw. Das Ergebnis: eine sehr iterative Ausdruck des Rechenflusses.
Jean-Baptiste Yunès
12
Hier besteht keine Notwendigkeit new String[]{…}. Verwenden Sie einfachArrays.asList("my/path/one", "my/path/two")
Holger
4
Wenn Ihre Quelle eine String[]ist, müssen Sie nicht anrufen Arrays.asList. Sie können einfach mit über das Array streamen Arrays.stream(array). Übrigens habe ich Schwierigkeiten, den Zweck des isExcludedTests insgesamt zu verstehen . Ist es wirklich interessant, ob ein Element von EXCLUDE_PATHSbuchstäblich irgendwo im Pfad enthalten ist? Dh isExcluded("my/path/one/foo/bar/baz")wird zurückkehren true, sowie isExcluded("foo/bar/baz/my/path/one/")...
Holger
3
Großartig, ich war mir der Arrays.streamMethode nicht bewusst , danke, dass Sie darauf hingewiesen haben. In der Tat scheint das Beispiel, das ich gepostet habe, für andere als mich ziemlich nutzlos zu sein. Ich bin mir des Verhaltens der isExcludedMethode bewusst , aber es ist wirklich nur etwas, das ich für mich selbst brauche, um Ihre Frage zu beantworten: Ja , es ist aus Gründen interessant, die ich nicht erwähnen möchte, da es nicht in den Anwendungsbereich passt der ursprünglichen Frage.
Mcuenez
1
Warum wird das toLowerCaseauf die Konstante angewendet, die bereits in Kleinbuchstaben geschrieben ist? Sollte es nicht auf das pathArgument angewendet werden ?
Sebastian Redl

Antworten:

78

Ihre Annahme ist richtig. Ihre Stream-Implementierung ist langsamer als die for-Schleife.

Diese Stream-Nutzung sollte jedoch so schnell sein wie die for-Schleife:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

Dies durchläuft die Elemente, wendet sie an String::toLowerCaseund filtert sie einzeln auf die Elemente und endet beim ersten übereinstimmenden Element .

Beide collect()& anyMatch()sind Terminaloperationen. anyMatch()Wird jedoch beim ersten gefundenen Element beendet, während collect()alle Elemente verarbeitet werden müssen.

Stefan Pries
quelle
2
Genial, wusste nichts findFirst()in Kombination mit filter(). Anscheinend nicht weiß so gut, wie ich dachte, wie man Streams benutzt.
Mcuenez
4
Es gibt einige wirklich interessante Blog-Artikel und Präsentationen im Web über die Leistung von Stream-APIs, die ich sehr hilfreich fand, um zu verstehen, wie dieses Zeug unter der Haube funktioniert. Ich kann definitiv empfehlen, nur ein bisschen zu recherchieren, wenn Sie daran interessiert sind.
Stefan Pries
Nach Ihrer Bearbeitung sollte Ihre Antwort akzeptiert werden, da Sie meine Frage auch in den Kommentaren der anderen Antwort beantwortet haben. Ich möchte @ rvit34 jedoch etwas Anerkennung für die Veröffentlichung des Codes geben :-)
mcuenez
34

Die Entscheidung, ob Streams verwendet werden sollen oder nicht, sollte nicht von Leistungsaspekten abhängen, sondern von der Lesbarkeit. Wenn es wirklich um Leistung geht, gibt es andere Überlegungen.

Bei Ihrem .filter(path::contains).collect(Collectors.toList()).size() > 0Ansatz verarbeiten Sie alle Elemente und sammeln sie in einem temporären Element List, bevor Sie die Größe vergleichen. Dies ist jedoch für einen Stream, der aus zwei Elementen besteht, kaum von Bedeutung.

Die Verwendung .map(String::toLowerCase).anyMatch(path::contains)kann CPU-Zyklen und Speicher sparen, wenn Sie eine wesentlich größere Anzahl von Elementen haben. Dies konvertiert jedoch jedes Stringin seine Kleinbuchstaben-Darstellung, bis eine Übereinstimmung gefunden wird. Offensichtlich hat die Verwendung einen Sinn

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

stattdessen. Sie müssen die Konvertierung in Kleinbuchstaben also nicht bei jedem Aufruf von wiederholen isExcluded. Wenn die Anzahl der Elemente EXCLUDE_PATHSoder die Länge der Zeichenfolgen sehr groß wird, können Sie die Verwendung in Betracht ziehen

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

Wenn Sie eine Zeichenfolge als Regex-Muster mit dem LITERALFlag kompilieren , verhält sie sich wie normale Zeichenfolgenoperationen, kann jedoch einige Zeit für die Vorbereitung der Engine aufwenden, z. B. mithilfe des Boyer Moore-Algorithmus, um den tatsächlichen Vergleich effizienter zu gestalten.

Dies zahlt sich natürlich nur aus, wenn genügend nachfolgende Tests vorhanden sind, um die für die Vorbereitung aufgewendete Zeit zu kompensieren. Die Feststellung, ob dies der Fall sein wird, ist neben der ersten Frage, ob dieser Vorgang jemals leistungskritisch sein wird, eine der tatsächlichen Leistungsüberlegungen. Nicht die Frage, ob Streams oder forLoops verwendet werden sollen.

Übrigens behalten die obigen Codebeispiele die Logik Ihres ursprünglichen Codes bei, was für mich fragwürdig erscheint. Ihre isExcludedMethode gibt zurück true, wenn der angegebene Pfad eines der Elemente in der Liste enthält, also gibt sie truefür /some/prefix/to/my/path/onesowie my/path/one/and/some/suffixoder sogar zurück /some/prefix/to/my/path/one/and/some/suffix.

Sogar dummy/path/onerouswird als Erfüllung der Kriterien angesehen, da es containsdie Zeichenfolge ist my/path/one

Holger
quelle
Gute Einblicke in mögliche Leistungsoptimierungen, danke. Zum letzten Teil Ihrer Antwort: Wenn meine Antwort auf Ihren Kommentar nicht zufriedenstellend war, betrachten Sie meinen Beispielcode als bloße Hilfe für andere, um zu verstehen, was ich frage - und nicht als tatsächlichen Code. Sie können die Frage auch jederzeit bearbeiten, wenn Sie ein besseres Beispiel vor Augen haben.
Mcuenez
3
Ich nehme Ihren Kommentar an, dass dieser Vorgang genau das ist, was Sie wirklich wollen, sodass Sie ihn nicht ändern müssen. Ich werde nur den letzten Abschnitt für zukünftige Leser behalten, damit sie wissen, dass dies keine typische Operation ist, aber auch, dass sie bereits besprochen wurde und keine weiteren Kommentare benötigt…
Holger
Tatsächlich sind Streams perfekt für die Speicheroptimierung geeignet, wenn die Menge des Arbeitsspeichers das Serverlimit überschreitet
ColacX
21

Ja. Du hast recht. Ihr Stream-Ansatz hat einen gewissen Overhead. Sie können jedoch eine solche Konstruktion verwenden:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

Der Hauptgrund für die Verwendung von Streams ist, dass sie Ihren Code einfacher und leichter lesbar machen.

rvit34
quelle
3
Ist anyMatcheine Abkürzung für filter(...).findFirst().isPresent()?
Mcuenez
6
Ja, so ist es! Das ist sogar besser als mein erster Vorschlag.
Stefan Pries
8

Das Ziel von Streams in Java ist es, die Komplexität des Schreibens von parallelem Code zu vereinfachen. Es ist inspiriert von funktionaler Programmierung. Der serielle Stream dient nur dazu, den Code sauberer zu machen.

Wenn wir Leistung wollen, sollten wir parallelStream verwenden, das für entwickelt wurde. Die serielle ist im Allgemeinen langsamer.

Es gibt einen guten Artikel zu lesen über , und Leistung . ForLoopStreamParallelStream

In Ihrem Code können wir Beendigungsmethoden verwenden, um die Suche bei der ersten Übereinstimmung zu stoppen. (anyMatch ...)

Paulo Ricardo Almeida
quelle
5
Beachten Sie, dass bei kleinen Streams und in einigen anderen Fällen ein paralleler Stream aufgrund der Startkosten langsamer sein kann. Und wenn Sie eine geordnete Terminaloperation anstelle einer ungeordneten parallelisierbaren haben, wird die Resynchronisation am Ende durchgeführt.
CAD97
0

Wie andere schon viele gute Punkte erwähnt haben, möchte ich nur die träge Bewertung in der Stream-Bewertung erwähnen . Wenn wir map()einen Stream mit Kleinbuchstaben erstellen, erstellen wir nicht sofort den gesamten Stream, sondern der Stream ist träge aufgebaut , weshalb die Leistung der herkömmlichen for-Schleife entsprechen sollte. Es wird kein vollständiger Scanvorgang durchgeführt map()und anyMatch()gleichzeitig ausgeführt. Sobald anyMatch()true zurückgegeben wird, wird es kurzgeschlossen.

Kaicheng Hu
quelle