Suche
Transformationen gefällig ? – findFirst – gib mir ein Optional

Core Methods in Java 8 Streams, Teil 2

Sven Ruppert
©Shutterstock.com/ilolab

„Finde ein Element aus dem Stream das folgenden Kriterien entspricht.“ Was auf SQL-Ebene meist schnell und kompakt als Menge definiert ist, kann auf der imperativen Seite zu einigem Aufwand führen. Aber können uns Streams da nicht doch helfen?

Immer wieder gibt es eine Menge von Elementen, deren Reihenfolge nicht definiert, die Anzahl unbestimmt, aber genau ein Element dieser Menge mit bestimmten Eigenschaften zu entnehmen ist. Was auf der Datenbank dank SQL kein Problem in den meisten Fällen darstellt, kann auf der imperativen Seite schon mal zu einem längeren Stück Quelltext führen. Dank Streams und der Methode findFirst() können viele Konstrukte nun mit JDK-Bordmitteln einfach und kompakt gelöst werden.

findFirst – gib mir ein Optional

Die Methode findFirst() liefert das erste Element aus dem Stream. Auf den ersten Blick eine triviale Methode, so mehr erfreut man sich beim zweiten Blick. Der Rückgabewert ist ein Optional (Listing 1), in Falle eine leeren Streams ein leeres Optional.

    final List<String> demoValues =
        Arrays.asList("AB","AAB","AAAB","AAAAB","AAAAAB");

    final Optional<String> first = demoValues.stream().findFirst();


Optional? Was ist das nun schon wieder? Optionals sind seit dem JDK8 im Sprachumfang enthalten. Die Idee dahinter ist nichts anderes als das Null-Object-Pattern in Verbindung mit Servicemethoden. Da es sich um eine neue Klasse handelt, schauen wir uns diese kurz an.

Optional – ja / nein / vielleicht ?

Das ursprüngliche Null-Object-Pattern kann man kurz wie folgt erklären: Der Rückgabewert einer Methode ist vom Typ List<X>. Ist das Ergebnis eine leere Menge, liefert die Methode eine leere Liste. Der nachfolgende Code kann entweder mit isEmpty() diesen Zustand erfragen, oder die verarbeitende Schleife erzeugt ein ebenfalls leeres Ergebnis. Soweit ist alles in Ordnung. Ist der Rückgabewert allerdings eine einzelne Instanz vom Typ X dann liefern viele Methoden leider ein null zurück. Die Folge dessen ist, dass immer wieder Codeteile mit folgenden if-else Blöcken versehen werden müssen (Listing 2).

    final Integer x = methodX();
    if(x == null){
        //do something
    } else{
        //do something else
    }


Abhilfe kann sein, dass man die Klasse mit einem NULL-Element ausstattet. Seit JDK8 ist das nicht mehr notwendig. Das JDK liefert nun eine Lösung. Das Optional kann man sich als Holder vorstellen, der auf das Vorhandensein von dem inkludierten Value-Element befragt werden kann. Da es nun zum Sprachumfang gehört, ist es auch schon an mancher Stelle eingebaut worden. So z.B. in den Streams. Um selbst Instanzen der Klasse Optional zu erzeugen gibt es zwei Methoden. Die eine ist of(<T>), sie erwartet immer eine Referenz ungleich null. Die andere Methode ist ofNullable(<T>). Bei der zweiten Version kann auch ein null übergeben werden. Null-Values werden übrigens alle auf eine gemeinsame Referenz zurückgeführt. In der Implementierung von Optional ist diese definiert durch:

private static final Optional<?> EMPTY = new Optional<>();

    //how to create an Optional
    final Optional<String> optionalA = Optional.of("A");
    final Optional<String> optionalB1 = Optional.ofNullable("B");
    final Optional<String> optionalB2 = Optional.ofNullable(null);


Die Klasse Optional liefert einem zusätzlich folgende Methoden (Listing 4), die nun kurz erklärt werden.

    System.out.println("optionalB2.isPresent() = " 
        + optionalB2.isPresent());

    optionalA.ifPresent(System.out::println); // result = 'A'
        optionalB2.ifPresent(System.out::println);// no output

    demoValues.stream()
        .forEach(v -> {
            Optional.ofNullable(v)
            .filter(o -> o.contains("AAA"))
            .ifPresent(System.out::println);
        });

    demoValues.stream()
        .forEach(v -> {
            Optional.ofNullable(v)
            .map(o->o.concat("_X"))
            .filter(f->f.contains("AAA"))
            .ifPresent(System.out::println);
        });
    //method addX returned Optional<String>
    final Optional<Optional<String>> map = optionalA.map(Part04::addX);
    final Optional<String> flatMap = optionalA.flatMap(Part04::addX);

    demoValues.stream()
        .forEach(v -> {
            Optional.ofNullable(v)
            .flatMap(Part04::addX)
            .filter(f -> f.contains("AAA"))
            .ifPresent(System.out::println);
        });


    System.out.println(optionalB2.orElse("noop"));

    try {

        optionalB2.orElseThrow(NullPointerException::new));
    } catch (NullPointerException e) {
        e.printStackTrace();
    }


Die Methode isPresent liefert true wenn ein Value ungleich null enthalten ist. Die Methode ifPresent(<lambda>); hingegen führt das übergebene Lambda aus, wenn ein Value ungleich null vorhanden ist. Mit filter(<Predicate>) werden nur die Optionals die dem definierten Filterkriterium entsprechen zurückgegeben, alle anderen werden als empty zurückgegeben. Die Methode map entspricht der map-Methode von den Streams, wobei das Ergebnis in einem Optional verpackt zurückgeliefert wird. Aus diesem Grunde gibt es auch die Methode flatMap() die das Ergebnis ohne umschließendes Optional liefert. Mit orElse können alternative Werte geliefert werden wenn das Value null ist. Bei der Methode orElseThrow wird eine Exception geworfen wenn das Value null ist.

findFirst – was ist das erste Element ?

Mit der Methode findFirst() wird einem der erste Treffer als Optional aus dem definierten Werteraum basierend auf dem Streaminhalt geliefert. Das bedeutet aber auch, dass es nicht zwingend das erste aus der Reihenfolge der Eingangswerte sein muss. Listing 5 liefert das erste Element aus dem Stream („AB“,“AAB“,“AAAB“,“AAAAB“,“AAAAAB“) zurück, das eine Zeichenfolge „AAA“ enthält. In unserem Beispiel also immer  „AAAB“. Was aber passiert wenn man das als ParallelStream definiert (Listing 6)? Es kann passieren, dass irgendein Value aus der Werteliste, welches dem Kriterium entspricht, geliefert wird, da der Stream parallel abgearbeitet wird.

    final String value = demoValues
        .stream()
        .filter(o -> o.contains("AAA"))
        .findFirst().orElse("noop ");
    System.out.println("value = " + value);


    for(int i=0; i<10;i++){
        final String valueParallel = demoValues
            .parallelStream()
            .filter(o -> o.contains("AAA"))
            .findFirst().orElse("noop ");
        System.out.println("value ("+i+") = " + valueParallel);
    }


Fazit

Die Methode findFirst() gehört zu den „terminalen„ Methoden. Das bedeutet, dass nach dem Aufruf von findFirst() keine weiteren Streamoperationen durchgeführt werden können. Der Stream wird terminiert. Mit dem Einsatz von findFirst() können recht komplexe Muster abgebildet werden, um einzelne Exemplare aus dem Stream zu erhalten. Da es sich bei den Streams um Pipelines handelt, werden auch immer nur so viele Elemente prozessiert, wie zum Finden dieses einen Elementes notwendig sind. Im Vergleich zu den Ausdrücken in der herkömmlichen Notation, sind die Ausdrücke mittels Streams meist um einiges kompakter. Der Einsatz von findFirst() eignet sich überall dort, wo eine deklarative mengenorientierte Beschreibung der einzelnen Entität nicht angewendet werden kann und demnach ein imperativer Weg notwendig ist.

Die Quelltexte zu diesem Text sind unter [1] zu finden. Wer umfangreichere Beispiele zu diesem Thema sehen möchte, dem empfehle einen Blick auf [2].

Aufmacherbild: Streams of light abstract Cool waves background von Shutterstock / Urheberrecht: ilolab

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.