Finde das erste Element nach Prädikat

504

Ich habe gerade angefangen, mit Java 8 Lambdas zu spielen, und ich versuche, einige der Dinge, die ich gewohnt bin, in funktionale Sprachen zu implementieren.

Beispielsweise haben die meisten funktionalen Sprachen eine Suchfunktion, die mit Sequenzen arbeitet, oder Listen, die das erste Element zurückgeben, für das das Prädikat ist true. Der einzige Weg, dies in Java 8 zu erreichen, ist:

lst.stream()
    .filter(x -> x > 5)
    .findFirst()

Dies scheint mir jedoch ineffizient zu sein, da der Filter die gesamte Liste scannt, zumindest nach meinem Verständnis (was falsch sein könnte). Gibt es einen besseren Weg?

Siki
quelle
53
Es ist nicht ineffizient, die Java 8 Stream-Implementierung wird nur schleppend ausgewertet, sodass der Filter nur auf den Terminalbetrieb angewendet wird. Gleiche Frage hier: stackoverflow.com/questions/21219667/stream-and-lazy-evaluation
Marek Gregor
1
Cool. Das hatte ich gehofft. Sonst wäre es ein großer Design-Flop gewesen.
Siki
2
Wenn Sie wirklich prüfen möchten, ob die Liste überhaupt ein solches Element enthält (nicht das erste von möglicherweise mehreren herausgreifen), kann .findAny () theoretisch in einer Paralleleneinstellung effizienter sein und diese Absicht natürlich klarer kommunizieren.
Joachim Lous
Im Vergleich zu einem einfachen forEach-Zyklus würden dadurch viele Objekte auf dem Heap und Dutzende dynamischer Methodenaufrufe erstellt. Während dies in Ihren Leistungstests möglicherweise nicht immer das Endergebnis beeinflusst, macht es an den Hotspots einen Unterschied, auf die triviale Verwendung von Stream und ähnlichen Schwergewichtskonstrukten zu verzichten.
Agoston Horvath

Antworten:

720

Nein, der Filter scannt nicht den gesamten Stream. Es ist eine Zwischenoperation, die einen Lazy Stream zurückgibt (tatsächlich geben alle Zwischenoperationen einen Lazy Stream zurück). Um Sie zu überzeugen, können Sie einfach den folgenden Test durchführen:

List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);
int a = list.stream()
            .peek(num -> System.out.println("will filter " + num))
            .filter(x -> x > 5)
            .findFirst()
            .get();
System.out.println(a);

Welche Ausgänge:

will filter 1
will filter 10
10

Sie sehen, dass nur die beiden ersten Elemente des Streams tatsächlich verarbeitet werden.

So können Sie mit Ihrem Ansatz gehen, der vollkommen in Ordnung ist.

Alexis C.
quelle
37
Als Hinweis habe ich get();hier verwendet , weil ich weiß, welche Werte ich in die Stream-Pipeline einspeise und daher ein Ergebnis ergibt. In der Praxis sollten Sie nicht verwenden get();, aber orElse()/ orElseGet()/ orElseThrow()(für einen aussagekräftigeren Fehler anstelle eines NSEE), da Sie möglicherweise nicht wissen, ob die auf die Stream-Pipeline angewendeten Operationen zu einem Element führen.
Alexis C.
31
.findFirst().orElse(null);zum Beispiel
Gondy
20
Verwenden Sie nicht orElse null. Das sollte ein Anti-Muster sein. Es ist alles in der Option enthalten. Warum sollten Sie also eine NPE riskieren? Ich denke, der Umgang mit Optional ist der bessere Weg. Testen Sie das Optionale einfach mit isPresent (), bevor Sie es verwenden.
BeJay
@ BeJay Ich verstehe nicht. Was soll ich anstelle von verwenden orElse?
John Henckel
3
@ JohnHenckel Ich denke, was BeJay bedeutet, ist, dass Sie es als OptionalTyp belassen sollten, was .findFirstzurückkehrt. Eine der Verwendungsmöglichkeiten von Optional besteht darin, Entwicklern zu helfen, sich nicht mit nullSeg zu befassen, anstatt zu prüfen myObject != null, ob Sie myOptional.isPresent()andere Teile der optionalen Benutzeroberfläche überprüfen oder verwenden können. Hat das klarer gemacht?
AMTerp
102

Dies scheint mir jedoch ineffizient zu sein, da der Filter die gesamte Liste scannt

Nein, wird es nicht - es wird "brechen", sobald das erste Element gefunden wird, das das Prädikat erfüllt. Sie können mehr über Faulheit im Stream-Paket javadoc lesen , insbesondere (Hervorhebung von mir):

Viele Stream-Vorgänge wie Filtern, Zuordnen oder Entfernen von Duplikaten können träge implementiert werden, wodurch Optimierungsmöglichkeiten entstehen. Beispiel: "Finde den ersten String mit drei aufeinanderfolgenden Vokalen" muss nicht alle Eingabe-Strings untersuchen. Stream-Operationen werden in Zwischenoperationen (Stream-produzierende Operationen) und Terminal-Operationen (Wert- oder Nebeneffekt-produzierende Operationen) unterteilt. Zwischenoperationen sind immer faul.

was hast du?
quelle
5
Diese Antwort war für mich informativer und erklärt das Warum, nicht nur das Wie. Ich nie neue Zwischenoperationen sind immer faul; Java-Streams überraschen mich immer wieder.
Kevinarpe
30
return dataSource.getParkingLots()
                 .stream()
                 .filter(parkingLot -> Objects.equals(parkingLot.getId(), id))
                 .findFirst()
                 .orElse(null);

Ich musste nur ein Objekt aus einer Liste von Objekten herausfiltern. Also habe ich das benutzt, hoffe es hilft.

CodeShadow
quelle
BESSER: Da wir nach einem booleschen Rückgabewert suchen, können wir dies besser tun, indem wir null check hinzufügen: return dataSource.getParkingLots (). Stream (). Filter (parkLot -> Objects.equals (parkLot.getId (), id)) .findFirst (). orElse (null)! = null;
Shreedhar Bhat
1
@shreedharbhat Das solltest du nicht tun müssen .orElse(null) != null. Stattdessen nutzen die die optionale API .isPresentdh .findFirst().isPresent().
AMTerp
@shreedharbhat Zunächst suchte OP nicht nach einem booleschen Rückgabewert. Zweitens wäre es sauberer zu schreiben.stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))
AjaxLeung
13

Verwenden Sie diese Option zusätzlich zu Alexis Cs Antwort: Wenn Sie mit einer Array-Liste arbeiten, in der Sie nicht sicher sind, ob das gesuchte Element vorhanden ist.

Integer a = list.stream()
                .peek(num -> System.out.println("will filter " + num))
                .filter(x -> x > 5)
                .findFirst()
                .orElse(null);

Dann können Sie einfach überprüfen, ob a ist null.

Ifesinachi Bryan
quelle
1
Sie sollten Ihr Beispiel korrigieren. Sie können einem einfachen int keine Null zuweisen. stackoverflow.com/questions/2254435/can-an-int-be-null-in-java
RubioRic
Ich habe deinen Beitrag bearbeitet. 0 (Null) kann ein gültiges Ergebnis sein, wenn Sie in einer Liste von Ganzzahlen suchen. Der Variablentyp wurde durch eine Ganzzahl und der Standardwert durch Null ersetzt.
RubioRic
0

import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

// Stream is ~30 times slower for same operation...
public class StreamPerfTest {

    int iterations = 100;
    List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);


    // 55 ms
    @Test
    public void stream() {

        for (int i = 0; i < iterations; i++) {
            Optional<Integer> result = list.stream()
                    .filter(x -> x > 5)
                    .findFirst();

            System.out.println(result.orElse(null));
        }
    }

    // 2 ms
    @Test
    public void loop() {

        for (int i = 0; i < iterations; i++) {
            Integer result = null;
            for (Integer walk : list) {
                if (walk > 5) {
                    result = walk;
                    break;
                }
            }
            System.out.println(result);
        }
    }
}
Illusionen
quelle
0

Verbesserte Einzeiler -Antwort: Wenn Sie nach einem booleschen Rückgabewert suchen, können wir dies besser tun, indem wir isPresent hinzufügen:

return dataSource.getParkingLots().stream().filter(parkingLot -> Objects.equals(parkingLot.getId(), id)).findFirst().isPresent();
Shreedhar Bhat
quelle
Wenn Sie einen booleschen Rückgabewert wünschen, sollten Sie anyMatch
AjaxLeung