Suche
Transformationen gefällig ? – Streams und seine kleinen Helfer

Streams: Core Methods Teil 4

Sven Ruppert
© Shutterstock.com/ilolab

Die Streams-API hat schon einige Service-Methoden an Board um einem das Leben als Entwickler leichter zu machen. Welche gibt es? Wie kann man sie einsetzen?

Bei der Verwendung von Streams findet man viele grundlegende Funktionen die in der API schon enthalten sind und einem die Entwicklung von Basic-Utilitys ersparen. Diese werden wir uns nun ansehen und anhand kleiner Beispiele deren Verwendung zeigen.

limit / skip (vor Build 116 – substream) – bitte nicht alles

Streams können undefiniert lang sein. Das bedeutet, dass im Extremfall ein Stream kein Ende hat. Es ist also manchmal sinnvoll, Streams nur bis zu einer bestimmten Länge abzuarbeiten, oder nur eine bestimmte Menge von Ergebnissen zu sammeln, da der Rest nicht mehr für die nachfolgende Logik zu verwenden ist. Die Methode limit(count) ist genau dafür gedacht. Das nachfolgende Beispiel zeigt einmal wie die initiale Menge reduziert werden kann und wie die Menge der Ergebnisse beschränkt werden kann (Listing 1). Es werden also immer ab der Stelle, an der die Methode limit(count) aufgerufen wird, die restlichen Schritte auf das angegebene Limit begrenzt.

final List <Integer> demoValues
        = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

        //limit the input -> [1, 2, 3, 4]
        System.out.println(demoValues
            .stream().limit(4)
                .collect(Collectors.toList()));

        //limit the result -> [5, 6, 7, 8]
        System.out.println(demoValues
            .stream().filter((v)- >v > 4)
            .limit(4)
            .collect(Collectors.toList()));

Mit der Methode skip(count) verhält es sich ein klein wenig anders. Hier ist auch eine Begrenzung des Streams vorhanden, jedoch handelt es sich hier um eine absolute Grenze. Der Counter gibt an, wie viele Elemente übersprungen werden. Das Ende ist allerdings offen. Die Begrenzung findet somit zu Beginn statt, indem n Elemente einfach übersprungen und nicht verarbeitet werden (Listing 2). Die Methode substream(counter) kann auch wieder mehrfach und an verschiedenen Stellen in dem Gesamtkonstrukt vorkommen.

//jumping over the first 4 elements -> [5, 6, 7, 8, 9, 10]
    System.out.println(demoValues
        .stream().skip(4)
        .collect(Collectors.toList()));

distinct – alles nur einmal bitte

Aus dem Bereich SQL kennt man den Befehl distinct, um eine Menge von Werten auf jeweils nur ein Exemplar eines Wertes zu reduzieren. Also das Erzeugen einer unique-Menge. Genau dasselbe erledigt die Methode distinct() (Listing 3). Die Implementierung selbst arbeitet in der Klasse DistinctOps auf einer ConcurentHashMap da diese Operation auch für parallele Streams entwickelt worden ist. Die Distinct-Menge ist dann das KeySet der HashMap. Das auschlaggebende Element ist die hashCode– und equals-Implementierung der Elemente die dort in die unique-Menge überführt werden sollen. An dieser Stelle kann man das Verhalten und die Performance der distinct Operation beeinflussen.

// [77, 79, 81, 95, 43, 10, 53, 48, 
    //     74, 68, 60, 86, 83, 24, 57, 28, 8,
    //  85, 70, 66, 20, 14, 97, 73, 22, 
    //    36, 40, 39, 32, 19, 41, 67, 25, 88]
    final Random random = new Random();
    System.out.println(
        Stream.generate(() -> random.nextInt(100))
            .limit(40)
            .distinct()
            .collect(Collectors.toList())
    );

min / max – ganz klein – ganz groß

Die Methoden min(<Comparator>) und max(<Comparator>) liefern aus der Menge der Werte im Stream das Minimum bzw. das Maximum (Listing 4). Dieser Wert wird mittels Comparator ermittelt. Das hat zur Folge, dass alle Elemente durchlaufen werden müssen. Es kann also nicht auf unendlichen Streams ausgeführt werden. Die Definition des Comparators lässt dementsprechend verschiede Interpretationen zu, was ein Minimum und was ein Maximum ist. Gleichzeitig ist die Implementierung des Comparators eines der bestimmenden Glieder bei der Performance, da es auf alle Elemente angewendet wird. Es ist auf jedem Fall schneller als das Sortieren der Elemente mit anschließendem findFirst(), da die Komplexität von min/max bei O(n) liegt und die Komplexität des Sortierens bei O(n log n).

//find the maximum
    System.out.println(demoValues
        .stream().max(Integer::compareTo));

    //find the BUG 😉
    System.out.println(demoValues
        .stream().min((v1, v2) -> Integer.compare(v2, v1)));

allMatch, anyMatch, noneMatch, count

Die Methoden allMatch(<Predicate>), anyMatch(<Predicate>), noneMatch(<Predicate>) liefern ein boolean zurück.

• allMatch wenn die definierte Bedingung bei genau allen Elementen zutrifft,
• anyMatch wenn einige Elemente der Bedingung entsprechen (mind. 2)
• noneMatch wenn kein einziges Element der Bedingung entspricht

Sieht man sich die Laufzeit der einzelnen Methoden an, so kann man feststellen, dass noneMatch(<Predicate>) auf den gesamten Wertevorrat angewendet werden muss. anyMatch(<Predicate>)und allMatch(<Predicate>) hingegen brechen ab sobald das Ergebnis ableitbar ist. In unserem Fall bricht allMatch(<Predicate>) nach genau einem Vergleich ab, da dieser schon nicht passt. (ungerade Zahl) nach anyMatch(<Predicate>) bricht nach genau zwei erfolgreichen Treffern ab, da die Bedingung „any“ erfüllt ist (Listing 6).

// true, some are matching
    System.out.println("anyMatch " + demoValues.stream()
        .map((e) -> {
            System.out.println("e = " + e);
            return e;
        })
        .anyMatch((v) -> v % 2 == 0));

    //false, not all are matching
    System.out.println("allMatch " + demoValues.stream()
        .map((e) -> {
            System.out.println("e = " + e);
            return e;
        })
        .allMatch((v) -> v % 2 == 0));

    //false, not all are NOT matching
    System.out.println("noneMatch " + demoValues.stream() 
        .map((e) -> {
            System.out.println("e = " + e);
            return e;
        })
        .noneMatch((v) -> v % 2 == 0));
    e = 1
    e = 2
    anyMatch true
    e = 1
    allMatch false
    e = 1
    e = 2
    noneMatch false
    e = 1
    e = 2
    e = 3
    e = 4
    e = 5
    e = 6
    e = 7
    e = 8
    e = 9
    e = 10

Nun fehlt nur noch die Methode count(). Diese ist schnell erklärt, da sie die Anzahl der Elemente zurückliefert die bis zu diesem Schritt in der Verarbeitung des Streams gekommen sind (Listing 7).

//5 matching the filter, 2,4,6,8,10
    System.out.println("count " + demoValues.stream()
        .map((e) -> {
            System.out.println("e = " + e);
            return e;
        })
        .filter((v) -> v % 2 == 0)
        .count());

parallel() / sequential () – umschalten wenn nötig

Die beiden letzten Methoden, die wir uns hier ansehen werden, sind parallel() und sequential(). Die Methoden die wiederum einen Stream zurückliefern, können so explizit in eine serielle oder parallele Version geschaltet werden. Sollte eine nachfolgende Operation nicht parallel durchführbar sein, dann kann das mit dem Methodenaufruf seriell() geschehen. Es kann bei jedem Stream einzeln entschieden werden ob parallel oder seriell gearbeitet wird (Listing 8).

System.out.println(demoValues.stream()  //seriell
          .map((m1) -> m1)
          .parallel()    
          .map((m2) -> m2)
          .sequential() //seriell
          .collect(Collectors.toList()));

Fazit

Die Methoden, die zur Verfügung stehen, sind sehr breit in der Funktionalität. Viele Dinge sind out-of-the-box dabei. Das führt zu sehr kompakten Code ohne dass selber grundlegende Util-Methoden implementiert werden müssen.

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].

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.