Suche
Transformationen gefällig ? - forEach / match / find

Core Methods in Java 8 Streams

Sven Ruppert
©Shutterstock.com/ilolab

In dem letzten Artikel „JDK8: Streams first touch“ wurde gezeigt wie Streams mit Daten versorgt werden und wie die Daten wieder einem Stream entnommen werden können. Aber wie wird in einem Stream mit den Daten gearbeitet? Welche Basis-Methoden stehen einem zur Verfügung?

Nachdem wir uns im letzten Artikel damit beschäftigt haben, wie die Daten in die Streams gelangen und wie sie wieder zu entnehmen sind, werden wir uns jetzt mit der Datentransformation beschäftigen. Es stehen einem unter anderem die drei Basis-Methoden forEach, match und find zur Verfügung mit denen man schnell und einfach die ersten Versuche unternehmen kann.

forEach – ein Lambda für jedes Element

Die Methode forEach(<lambda>) macht eigentlich genau das, was man vermutet. Es wendet das als Argument übergebene Lambda auf jedes einzelne Element des Streams an. Diese Methode ist  auch bei Iterable, List, Map und einigen anderen Klassen/Interfaces zu finden, was erfreulicherweise zu kürzeren sprachlichen Konstrukten führt. Listing 1 zeigt die Iteration in Pre-JDK8-Notation, Listing 2 unter der Verwendung von forEach(<lambda>). Dort sind eine lange und zwei kurze Versionen zu sehen. Die lange Version verwendet die volle Notation inkl. der geschwungenen Klammern. Die beiden kurzen Versionen verwenden die Notation um auf statische Methoden zuzugreifen.

    final List<Pair> generateDemoValues = new 
        PairListGenerator(){}.generateDemoValues();

    //pre JDK8
    for (final Pair generateDemoValue : generateDemoValues) {
        System.out.println(generateDemoValue);
    }


    //long version
    generateDemoValues.stream().forEach(v -> { System.out.println(v) });

    //short version - seriel
    generateDemoValues.stream().forEach(System.out::println);

    //short version - parallel
    generateDemoValues.parallelStream().forEach(System.out::println);


Im Listing 2 ist ein kleiner aber feiner Unterschied in den beiden kurzen Versionen zu sehen. Die erste Definition geht auf einen seriellen, die andere auf einen parallelen Stream zurück. Bei beiden ist sichergestellt, dass die übergebene Methoden-Referenz nur einmal auf jedes Element angewendet wird. Nicht sichergestellt ist jedoch die Reihenfolge, in der die einzelnen Elemente abgearbeitet werden wenn es sich um parallele Streams handelt. Ist die Reihenfolge von Bedeutung, muss die Methode forEachOrdered(<lambda>) verwendet werden. Hier wird bei der Abarbeitung die Reihenfolge, die in der Datenquelle vorliegt, eingehalten.  Das sollte nur verwendet werden wenn es zwingend notwendig ist. Auf den ersten Blick ist aber nicht ersichtlich, dass ein NotNullCheck durchgeführt wird und im Falle von null mit einer NullPointerException quittiert wird. Jedes Element wird durch die Consumers accept Methode konsumiert (Listing 3).

    //class - ReferencePipeline    
    @Override
    public void forEach(Consumer<? super P_OUT> action) {
        evaluate(ForEachOps.makeRef(action, false));
    }

    //class - AbstractPipeline
    final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
        assert getOutputShape() == terminalOp.inputShape();
        if (linkedOrConsumed)
            throw new IllegalStateException(MSG_STREAM_LINKED);
        linkedOrConsumed = true;

        return isParallel()
            ? terminalOp.evaluateParallel(this, 
                sourceSpliterator(terminalOp.getOpFlags()))
            : terminalOp.evaluateSequential(this,
                sourceSpliterator(terminalOp.getOpFlags()));
    }

    //class - ForEachOps
    public static <T> TerminalOp<T, Void> 
        makeRef(Consumer<? super T> action, boolean ordered) {

        Objects.requireNonNull(action); //throws NPE
        return new ForEachOp.OfRef<> (action, ordered);
    }

    //class – ForEachOps. OfRef
    static final class OfRef<T> extends ForEachOp<T> {
        final Consumer<? super T> consumer;
        //…

        @Override
        public void accept(T t) {
            consumer.accept(t);
        }
    }


Bei der Verwendung von forEach(<lambda>) ist allerdings folgendes zu beachten. Durch die Methode accept im Consumer wird das Element konsumiert. Das bedeutet, dass forEach(<lambda>) nur einmal auf einen Stream angewendet werden kann. Man spricht hier in diesem Zusammenhang auch von einer terminalen Operation. Sind mehr als eine Operation auf dem Element anzuwenden, kann diese natürlich innerhalb des übergebenden Lambdas geschehen.

Das Argument der forEach(<lambda>)-Methode kann jedoch wiederverwendet werden, indem man eine Instanz vorhält und diese dann mehrenden Streams übergibt (Listing 4).

Ebenfalls nicht gestattet ist die Manipulation von umgebenden Variablen. Wie das erfolgen kann, sehen wir uns im Zusammenhang mit der Methode map und reduce an. Der größte Unterschied zu einer for-Schleife ist allerdings, dass nicht vorzeitig unterbrochen werden kann, weder mit break noch mit return.

    final Consumer<? super Pair> consumer = System.out::println;
    generateDemoValues.stream().forEachOrdered(consumer);
    generateDemoValues.parallelStream().forEachOrdered(consumer);


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

[ header = map und filter ]

map – Transformationen gefällig?

Die Methode map(<lambda>) erzeugt einen neuen Stream bestehend aus der Summe aller Transformationen der Elemente des Quell-Streams. Auch hier ist das Argument wieder ein Lambda. Das bedeutet, dass der Zielstream bis auf die funktionale Kopplung nichts mit dem Quellstream gemeinsam haben muss (Listing 5). In dem folgenden Beispiel wird aus einem Stream<Pair> ein Stream<DemoElement>, um anschließend in einen Stream<String> gemapped zu werden.

Die Methode map(<lambda>) kann beliebig oft hintereinander angewendet werden, da das Ergebnis immer wieder ein neuer Stream ist.

    //map from Point to DemoElements
    final Stream<DemoElement> demoElementStream = 
        generateDemoValues.stream().map(v -> {
            final String value = v.getValue();
            final DemoElement d = new DemoElement();
            d.setDatum(new Date());
            d.setValue(Base64.getEncoder()
            .encodeToString(value.getBytes()));
            return d;
        });

    final Stream<String> stringStream = 
        demoElementStream.map(v -> v.getValue());

    final Stream<String> stringStreamShort = 
        demoElementStream.map(DemoElement::getValue);

    //map from Point to DemoElements to Strings
    final List<String> stringList = generateDemoValues.stream()
        .map(v -> {
            final String value = v.getValue();
            final DemoElement d = new DemoElement();
            d.setDatum(new Date());
            d.setValue(Base64.getEncoder()
            .encodeToString(value.getBytes()));
            return d;
        })
        .map(DemoElement::getValue)
        .collect(Collectors.toList());


filter – wer darf es sein?

Wie die Methode map(<lambda>), erzeugt die Methode filter(<Lambda>) ebenfalls einen neuen Stream. Aus der Menge der Quellelemente werden die für die weiteren Schritte benötigten Elemente herausgefiltert (Listing 6). Die Methode filter(<Lambda>) kann mehrfach hintereinander angewendet werden, wobei mit jedem weiteren Aufruf die Menge weiter gefiltert wird. Es findet somit einer weitere Reduktion statt. Die Methode filter(<Lambda>) kann in beliebiger Kombination angewendet werden. Z.B. map -> filter -> map –> filter -> filter.

    final Stream<Pair> filteredPairStream =
        generateDemoValues.stream()
            .filter(v -> v.getId() % 2 == 0);


Fazit

Schon diese wenigen Basis-Methoden ermöglichen einem nach sehr kurzer Einarbeitung Streams recht effizient und effektiv einzusetzen. Zur Übung kann ich empfehlen bestehende Quelltexte in Konstrukte mit Streams umzuformen. Dabei wird sich zeigen, dass mit dieser Transformation eine starke Code-Reduktion einhergeht. An so mancher Stelle kann man dank der Streams auch Teilaufgaben parallelisieren, was zu einer höheren Auslastung der vorhandenen modernen CPU-Architekturen führt. Der Umbau wird sich lohnen!

In den nachfolgenden Artikeln werden wir uns mit der Funktionsvielfalt der Streams aus dem Bereich der Transformationen im Detail beschäftigen und anhand von praktischen Beispielen erläutern.

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.