Skip to content

5. Der Typ Stream<T> aus Java 8

5.1. Beispiel-01 – die Klasse Stream

Die Operationen auf den Streams Observable weisen zahlreiche Gemeinsamkeiten mit den Streams Stream auf. Ein Unterschied besteht darin, dass ein Element eines Streams Stream erst verarbeitet werden kann, wenn der gesamte Stream Stream abgerufen wurde, während ein Element eines Streams Observable (beobachtet) werden, sobald es vorliegt, ohne darauf zu warten, dass der gesamte Datenstrom Observable vorliegt. Ein weiterer Unterschied besteht darin, dass nach dem Empfang von Stream dessen Werte genutzt werden, indem sie einzeln aus Stream abgerufen (pull) werden. Bei der Beobachtbaren ist dies anders. Sobald diese einen Wert ausgibt, wird dieser an ihren Abonnenten weitergeleitet (pushed).

Mehrere Klassen implementieren das Konzept von Stream. Wir stellen hier die Klasse Stream<T> vor:

Image

Die Klasse Stream verfügt über 39 Methoden. Wir werden einige davon vorstellen. Betrachten wir den folgenden Code:

  

package dvp.java8.streams;

import java.util.List;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple01 {
    public static void main(String[] args) {
        // Personenliste
        List<Personne> personnes = Personnes.get();
        // Anzeige 1
        personnes.stream().forEach(p -> {
            System.out.println(p);
        });
        System.out.println("----------------");
        // Anzeige 2
        personnes.stream().forEach(System.out::println);
    }
}
  • Zeile 11: Es wird eine Liste von Personen instanziiert;
  • Zeile 13: Aus dieser Liste wird ein Stream erstellt. Alle Sammlungen können auf diese Weise in einen Stream-Stream umgewandelt werden. Dadurch lassen sich alle Methoden dieser Klasse nutzen, mit denen die Elemente der Sammlung prägnanter verarbeitet werden können als mit Schleifen. Außerdem kann so die Parallelität bei der Verarbeitung der Elemente genutzt werden, sofern dies möglich ist;
  • Zeile 13: Die Methode [Stream.forEach] hat folgende Signatur:
 

Man sieht, dass der Parameter der Methode die in Abschnitt 4.4 vorgestellte Funktionsschnittstelle [Consumer<T>] ist – eine Schnittstelle, deren einzige Methode den Typ T verwendet und keinen Rückgabewert liefert.

  • Im Code:

        personnes.stream().forEach(p -> {
            System.out.println(p);
});
  • erzeugt [personnes.stream()] einen Strom von Elementen vom Typ [Personne], der die Methode [forEach] speist. Der Parameter p ist vom Typ [Personne], und die bereitgestellte Lambda-Funktion zeigt diese Person an;

Der obige Code lässt sich wie folgt vereinfachen (Zeile 18):


personnes.stream().forEach(System.out::println);

Anstatt den Wert einer Lambda-Funktion als Parameter zu übergeben, übergeben wir die Referenz einer vorhandenen Methode, in diesem Fall die Methode println der Klasse System.out. Natürlich muss diese Methode die richtige Signatur haben, in diesem Fall die Signatur der Methode [Consumer.accept]: void accept(T t). Wie bereits erwähnt, ist der Parameter der Methode [accept] vom Typ [Personne];

Wir erhalten folgende Ergebnisse:

1
2
3
4
5
6
7
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
----------------
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}

Sobald ein Stream verarbeitet wurde, steht er nicht mehr zur Verfügung. Er muss neu erstellt werden, wenn man ihn erneut nutzen möchte. Dies wird durch den folgenden Code [Exemple01b] veranschaulicht:


package dvp.java8.streams;

import java.util.stream.Stream;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple01b {
    public static void main(String[] args) {
        // Personenstrom
        Stream<Personne> personnes = Personnes.get().stream();
        // Anzeige 1
        personnes.forEach(p -> {
            System.out.println(p);
        });
        System.out.println("----------------");
        // Anzeige 2
        personnes.forEach(System.out::println);
    }
}
  • Zeile 11: Um den Code zu optimieren, wird beschlossen, den Stream nur einmal zu erstellen. Die dabei erzielten Ergebnisse lauten wie folgt:

{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
----------------
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.sourceStageSpliterator(Unknown Source)
    at java.util.stream.ReferencePipeline$Head.forEach(Unknown Source)
at dvp.java8.streams.Exemple02b.main(Exemple02b.java:18)

Jedes Mal, wenn man ein Stream nutzen möchte, muss es neu generiert werden, auch wenn es bereits zuvor generiert wurde.

5.2. Beispiel-02 – Parallele Verarbeitung der Elemente eines Streams

  

Betrachten wir den folgenden Code:


package dvp.java8.streams;

import java.util.List;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple02 {
    public static void main(String[] args) {
        // Personenliste
        List<Personne> personnes = Personnes.get();
        // Anzeige 1
        personnes.stream().forEach(Exemple02::affiche);
        System.out.println("-----------------");
        // Anzeige 2
        personnes.stream().parallel().forEach(Exemple02::affiche);
    }

    public static void affiche(Personne p) {
        System.out.printf("Personne %s sur thread %s%n", p, Thread.currentThread().getName());
    }
}
  • Zeilen 19–21: Die Methode [affiche] gibt die Zeichenkette jSON einer Person sowie den Namen des Ausführungsthreads, in dem die Anzeige erfolgt, auf der Konsole aus;
  • Zeile 13: Zeigt eine Liste von Personen an. Es ist zu beachten, dass der Parameter der Methode [forEach] die Referenz der vorherigen statischen Methode ist;
  • Zeile 16: Hier wird dasselbe getan, jedoch mit der Methode [parallel], die dafür sorgt, dass die Verarbeitung der Elemente des Streams parallel in mehreren Threads erfolgt. Nicht jede Verarbeitung kann parallel erfolgen. Hier muss davon ausgegangen werden, dass die Anzeigereihenfolge keine Rolle spielt, da bei einer parallelen Verarbeitung die Ausführungsreihenfolge der Threads nicht garantiert ist. Außerdem ist eine Syntax zu beachten, die sowohl bei Stream als auch bei Observable allgegenwärtig sein wird:
flux.m1(e1->...).m2(e2->..).m3(e3->...)...
  • (Fortsetzung)
    • flux erzeugt e1-Elemente, die in die Methode m1 eingespeist werden;
    • flux.m1 ist wiederum ein Strom von e2-Elementen, die in die Methode m2 einfließen;
    • flux.m1.m2 ist ein Strom von e3-Elementen, die in die Methode m3 einfließen;

Der Typ der Elemente e1, e2 und e3 kann sich im Laufe der Verarbeitung des ursprünglichen Datenstroms ändern.

Die Ausführung dieses Codes liefert das folgende Ergebnis:

1
2
3
4
5
6
7
Personne {"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"} sur thread main
Personne {"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"} sur thread main
Personne {"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"} sur thread main
-----------------
Personne {"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"} sur thread main
Personne {"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"} sur thread ForkJoinPool.commonPool-worker-1
Personne {"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"} sur thread ForkJoinPool.commonPool-worker-2

Man sieht, dass die parallele Ausführung (Zeilen 5–7) auf drei verschiedenen Threads erfolgte und die Reihenfolge der Elemente, wie sie in den Zeilen 1–3 angegeben ist, nicht beibehalten wurde. In diesem Dokument werden wir nicht näher auf die parallele Verarbeitung der Elemente eines Stream eingehen, da hierfür zunächst die Bedingungen erläutert werden müssten, die eine solche Verarbeitung ermöglichen. Dabei stellt man fest, dass nur wenige Verarbeitungsschritte parallel durchgeführt werden können. Einer der Schritte, der sich von Natur aus für die Parallelität eignet, ist die Summe der numerischen Elemente eines Datenstroms, die wir nun vorstellen.

5.3. Beispiel-03 – Parallele Verarbeitung der Elemente eines Streams

  

Betrachten wir den folgenden Code (Beispiel 03a):


package dvp.java8.streams;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class Exemple03a {
    public static void main(String[] args) {

        final long limite = 10_000_000L;
        // Anzahl der Prozessoren
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // Zahlenliste
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(i);
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // Summe der Zahlen – sequentielle Methode
        début = new Date().getTime();
        long somme = nombres.stream().reduce(0L, (s, i) -> s + i);
        System.out.printf("somme séquentielle : somme=%s, durée (ms)=%s%n", somme, new Date().getTime() - début);
    }
}
  • In Zeile 22 verwenden wir die Methode [reduce], deren Signatur wie folgt lautet:
  • Die Methode [reduce] arbeitet mit Elementen vom Typ T;
  • die Methode [reduce] wendet dieselbe Verarbeitung auf alle Elemente eines Datenstroms an: Der Anfangswert eines Akkumulators wird als erster Parameter übergeben. Als zweiter Parameter wird eine Methode übergeben, die die funktionale Schnittstelle [BinaryOperator] [2] instanziiert: Ausgehend von jedem Element und dem Akkumulator liefert diese Methode einen neuen Wert für den Akkumulator. Der Endwert des Akkumulators ist der von der Methode [reduce] zurückgegebene Wert. Der Code [3] verdeutlicht diesen Mechanismus. Die Methode [apply] ist die Methode der funktionalen Schnittstelle [BinaryOperator] [2];

Kehren wir zum Beispielcode zurück:

  • Zeile 12: Die Anzahl der von JVM erfassten Kerne wird angezeigt;
  • Zeilen 15–18: Es wird eine Liste mit 10 Millionen Zahlen erstellt;
  • Zeile 22: Die Summe dieser Zahlen wird sequenziell mit einem einzigen Thread berechnet;

Man erhält folgende Ergebnisse:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4336 ms
somme séquentielle : somme=49999995000000, durée (ms)=225

Ersetzen wir nun Zeile 22 des Codes durch die folgende (Beispiel 03b):


long somme = nombres.stream().parallel().reduce(0L, (s, i) -> s + i);

Es wird festgelegt, dass die Elemente des Streams mithilfe mehrerer Threads parallel verarbeitet werden sollen. Dies ist möglich, da die Reihenfolge der Summierung der Zahlen keine Rolle spielt. Man kann also einem Thread T1 n1 Zahlen zuweisen, einem Thread T2 n2 Zahlen, ... und schließlich die von diesen verschiedenen Threads gelieferten Summen addieren. Man erhält dann die folgenden Ergebnisse:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4332 ms
somme parallèle : somme=49999995000000, durée (ms)=184

Es gibt also praktisch keinen Leistungsgewinn. In den folgenden Beispielen wird dies häufig der Fall sein. Die Verwaltung der Threads ist selbst zeitaufwendig. Die von jedem Kern ausgeführte Operation muss ausreichend komplex sein, damit sich der Leistungsgewinn bemerkbar macht. Dies zeigt das folgende Beispiel (Beispiel03c):


package dvp.java8.streams;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.BinaryOperator;

public class Exemple03c {
    public static void main(String[] args) {

        final long limite = 10_000L;
        // Anzahl der Prozessoren
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // Zahlenliste
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(i);
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // Summe der Zahlen – sequentielle Methode
        début = new Date().getTime();
        BinaryOperator<Long> bo = (s, i) -> {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
            return s + i;
        };
        long somme = nombres.stream().reduce(0L, bo);
        System.out.printf("somme séquentielle : somme=%s, durée (ms)=%s%n", somme, new Date().getTime() - début);
    }
}
  • Zeile 30: Es wird erneut die Methode [reduce] verwendet, der als Parameter die Referenz der Methode aus den Zeilen 23–29 übergeben wird;
  • Zeile 28: Die Methode [bo] liefert die Summe ihrer beiden Parameter;
  • Zeilen 24–27: Der Thread wird künstlich 1 Millisekunde lang angehalten, um eine rechenintensive Aufgabe zu simulieren;

Man erhält dann folgende Ergebnisse:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000 nombres en 2 ms
somme séquentielle : somme=49995000, durée (ms)=13617

Ersetzt man nun Zeile 30 durch die folgende:


long somme = nombres.stream().parallel().reduce(0L, bo);

erhält man folgende Ergebnisse:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000 nombres en 2 ms
somme séquentielle : somme=49995000, durée (ms)=1598

Man sieht deutlich den Leistungsgewinn, der durch die parallele Ausführung der Summenberechnung erzielt wird. Bei der Verarbeitung von 8 Zahlen:

  • wartet der sequentielle Thread 8 Mal 1 Millisekunde, also 8 ms;
  • die 8 parallelen Threads warten gleichzeitig jeweils 1 Millisekunde (zur Vereinfachung gedanklich veranschaulicht), also insgesamt 1 Millisekunde für die 8 Zahlen;

Man kann also erwarten, dass die parallele Ausführung achtmal schneller ist als die sequenzielle. Das ist hier in etwa der Fall.

5.4. Beispiel-04 – Einen Stream filtern

  

Betrachten wir den folgenden Code:


package dvp.java8.streams;

import java.util.List;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple04 {
    public static void main(String[] args) {
        // Personenliste
        List<Personne> personnes = Personnes.get();
        // Anzeigen
        System.out.println("age < 28 ----------------------");
        personnes.stream().filter(p -> p.getAge() < 28).forEach(p -> {
            System.out.println(p);
        });
        System.out.println("poids < 50 ----------------------");
        personnes.stream().filter(p -> p.getPoids() < 50).forEach(p -> {
            System.out.println(p);
        });
        System.out.println("age < 28 ----------------------");
        personnes.stream().filter(p -> p.getAge() < 28).forEach(System.out::println);
        System.out.println("poids < 50 ----------------------");
        personnes.stream().filter(p -> p.getPoids() < 50).forEach(System.out::println);
    }
}
  • Zeile 14: Die Methode [Stream.filter] hat folgende Signatur:
 
  • Die Methode [filter] erwartet als Parameter eine Instanz der in Abschnitt 4.2 vorgestellten Funktionsschnittstelle [Predicate], deren einzige zu implementierende Methode wie folgt lautet: boolean test(T t);
  • Die Methode [filter] gibt die Elemente des Streams zurück, die die Bedingung Predicate erfüllen. Sie dient somit dazu, den Stream Stream zu filtern;

Betrachten wir den folgenden Code:


package dvp.java8.streams;

import java.util.List;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple04 {
    public static void main(String[] args) {
        // Personenliste
        List<Personne> personnes = Personnes.get();
        // Anzeigen
        System.out.println("age < 28 ----------------------");
        personnes.stream().filter(p -> p.getAge() < 28).forEach(p -> {
            System.out.println(p);
        });
        System.out.println("poids < 50 ----------------------");
        personnes.stream().filter(p -> p.getPoids() < 50).forEach(p -> {
            System.out.println(p);
        });
        System.out.println("age < 28 ----------------------");
        personnes.stream().filter(p -> p.getAge() < 28).forEach(System.out::println);
        System.out.println("poids < 50 ----------------------");
        personnes.stream().filter(p -> p.getPoids() < 50).forEach(System.out::println);
    }
}
  • Zeilen 14–16: zeigen Personen mit einem Alter <28 an;
  • Zeilen 18–20: zeigen Personen mit einem Gewicht <50 an;
  • Zeile 22: macht dasselbe wie die Zeilen 14–16, jedoch prägnanter;
  • Zeile 24: macht dasselbe wie die Zeilen 18–20, jedoch in einer prägnanteren Form;

Die Ergebnisse der Ausführung lauten wie folgt:

age < 28 ----------------------
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
poids < 50 ----------------------
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
age < 28 ----------------------
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
poids < 50 ----------------------
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}

5.5. Beispiel-05 – Erstellen eines Stream<T2> aus einem Stream<T1>

  

Betrachten wir den folgenden Code:


package dvp.java8.streams;

import java.util.List;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple05 {
  public static void main(String[] args) {
    // Personenliste
    List<Personne> personnes = Personnes.get();
    // Anzeigen
    System.out.println("Personne --> String ----------------------");
    personnes.stream().map(p -> p.getNom()).forEach(System.out::println);
    System.out.println("Personne --> Integer ----------------------");
    personnes.stream().map(p -> p.getAge()).forEach(System.out::println);
  }
}
  • In Zeile 13 hat die Methode [Stream.map] die folgende Signatur:
 

Der Parameter der Methode [Stream.map] ist eine Instanz der in Abschnitt 4.3 vorgestellten funktionalen Schnittstelle [Function], deren einzige zu implementierende Methode lautet: R apply(T t). Man sieht, dass die Funktion [apply] ausgehend von einem Typ T einen Typ R erzeugt. Die Methode [Stream.map] erzeugt also aus einem Strom vom Typ T einen Strom Stream vom Typ R (ein Strom vom Typ T bedeutet hier – in einer sprachlichen Ungenauigkeit, die wir beibehalten werden – einen Strom von Elementen vom Typ T).

Betrachten wir nun den Code des Beispiels:

  • Zeile 14: Von einer Person p wird nur der Name beibehalten. Man erhält somit einen Datenstrom vom Typ String;
  • Zeile 14: Von einer Person p wird nur der Name beibehalten. Man erhält somit einen Datenstrom vom Typ Integer;

Die erhaltenen Ergebnisse lauten wie folgt:

1
2
3
4
5
6
7
8
Personne --> String ----------------------
jean
marie
camille
Personne --> Integer ----------------------
20
10
30

5.6. Beispiel-06 – weitere Methoden der Klasse Stream<T>

  

Wir veranschaulichen einige der 39 Methoden der Klasse Stream mit dem folgenden Code:


package dvp.java8.streams;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.data.Personne;
import dvp.data.Personnes;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Exemple06 {

    // Mapper jSON
    static private ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) throws JsonProcessingException {
        // Personenliste
        List<Personne> personnes = Personnes.get();
        // alle Personen
        affiche("all", personnes);
        // die erste Person
        affiche("findFirst", personnes.stream().findFirst().get());
        // beliebige Person
        affiche("findAny", personnes.stream().findAny().get());
        // die Personen außer der ersten
        affiche("skip 1", personnes.stream().skip(1L).collect(Collectors.toList()));
        // die ersten beiden Personen
        affiche("limit 2", personnes.stream().limit(2L).collect(Collectors.toList()));
        // die Anzahl der Personen
        affiche("count", personnes.stream().count());
        // die älteste Person
        affiche("age max", personnes.stream().max(Comparator.comparingInt(Personne::getAge)).get());
        // die leichteste Person
        affiche("poids min", personnes.stream().min(Comparator.comparingDouble(Personne::getPoids)).get());
        // die letzte Person in alphabetischer Reihenfolge der Namen
        affiche("nom max", personnes.stream().max((p1, p2) -> p1.getNom().compareToIgnoreCase(p2.getNom())).get());
        // das Gesamtalter aller Personen
        affiche("âge total (reduce)", personnes.stream().map(p -> p.getAge()).reduce(0, (a1, a2) -> a1 + a2));
        // die Personen nach steigendem Alter
        affiche("personnes par âge croissant",
                personnes.stream().sorted(Comparator.comparingInt(Personne::getAge)).collect(Collectors.toList()));
        // Gibt es Personen, die älter als 100 Jahre sind?
        affiche("des personnes de + de 100 ans (anyMatch)", personnes.stream().anyMatch(p -> p.getAge() > 100));
        // Sind alle Personen höchstens 100 Jahre alt?
        affiche("des personnes de + de 100 ans (noneMatch)", personnes.stream().noneMatch(p -> p.getAge() > 100));
        // Sind alle Personen älter als 8 Jahre?
        affiche("des personnes de + de 8 ans (allMatch)", personnes.stream().allMatch(p -> p.getAge() > 8));
        // Die Personen werden nach Geschlecht gruppiert
        affiche("personnes regroupées par sexe", personnes.stream().collect(Collectors.groupingBy(p -> p.getSexe())));
        // Entfernen von doppelten Elementen aus einer Liste
        affiche("distinct", Stream.of(1, 2, 1).distinct().collect(Collectors.toList()));
        // Aus einem Stream<Stream<T>> wird ein Stream<T> gebildet
        affiche("flatMap", Stream.of(1, 2, 3).flatMap(i -> Stream.of(i, i + 10)).collect(Collectors.toList()));
        // Aus einem `Stream<Stream<Integer>>` wird ein `IntStream` gebildet, dessen Summe berechnet wird
        affiche("flatMapToInt", Stream.of(1, 2, 3).flatMapToInt(i -> IntStream.of(i, i + 10)).sum());
        // aus einem Stream<Stream<Integer>> wird ein DoubleStream gebildet und anschließend ein Array
        affiche("flatMapToDouble", Stream.of(1, 2, 3).flatMapToDouble(i -> DoubleStream.of(i, i * 1.2)).toArray());
        // Max-Wert eines Integer-Stroms
        affiche("reduce Integer::max", Stream.of(1, 10, 8).reduce(Integer::max).get());
        // Min-Wert eines Stroms von Double-Werten
        affiche("reduce Integer::min", Stream.of(1.5, 10.4, 8.9).reduce(Double::min).get());
        // Mittelwert eines Integer-Stroms
        affiche("IntStream average", IntStream.of(1, 10, 8).average().getAsDouble());
        // Statistiken eines Stroms von Ganzzahlen
        affiche("IntStream summaryStatistics", IntStream.of(1, 10, 8).summaryStatistics());
    }

    public static <T> void affiche(String message, T value) throws JsonProcessingException {
        System.out.println(String.format("%s ----", message));
        System.out.println(jsonMapper.writeValueAsString(value));
    }
}
  • Zeilen 72, 75: Geben die Zeichenkette jSON des zweiten Parameters der Methode aus;
  • Zeile 24: Zeigt die Zeichenfolge jSON für alle Personen an. Das Ergebnis sieht wie folgt aus:
all ----
[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]

5.6.1. [findFirst]


// die erste Person
affiche("findFirst", personnes.stream().findFirst().get());

Die Methode [findFirst] gibt das erste Element eines Datenstroms zurück, sofern vorhanden. Ihre Signatur lautet wie folgt:

Das Ergebnis ist vom Typ „Optional<T>“, einem in Java 8 eingeführten Typ:

Die Klasse Optional&lt;T&gt; ermöglicht eine unterschiedliche Handhabung von Zeigern vom Typ null. Eine Methode, die einen Typ T zurückgeben soll, der den Wert *null annehmen kann, kann entscheiden, einen Typ Optional&lt;T&gt; zurückzugeben. Mit der Methode [Optional<T>.isPresent()] lässt sich feststellen, ob die Methode einen Wert zurückgegeben hat oder nicht. Der folgende Code [Exemple06b] veranschaulicht einen Teil der Funktionsweise von Optional<T>*:


package dvp.java8.streams;

import java.util.Optional;

import com.fasterxml.jackson.core.JsonProcessingException;

public class Exemple06b {

    public static void main(String[] args) throws JsonProcessingException {
        // optional ohne Wert
        Optional<Integer> o1 = m1();
        System.out.println(o1.isPresent());
        affiche(o1);
        // optional mit Wert
        Optional<Integer> o2 = m2();
        System.out.println(o2.isPresent());
        affiche(o2);
    }

    private static void affiche(Optional<Integer> o1) {
        try {
            // Der Wert des optionalen Parameters wird abgerufen
            // löst eine Ausnahme aus, wenn kein Wert vorhanden ist
            System.out.println(o1.get());
        } catch (Throwable th) {
            System.out.printf("%s : %s%n", th.getClass().getName(), th.getMessage());
        }

    }

    public static Optional<Integer> m1() {
        // kein Wert
        return Optional.empty();
    }

    public static Optional<Integer> m2() {
        // ein Wert
        return Optional.of(10);
    }
}

Die erhaltenen Ergebnisse lauten wie folgt:


false
java.util.NoSuchElementException : No value present
true
10

Kehren wir zum Beispielcode für die Methode [findFirst] zurück:


// die erste Person
affiche("findFirst", personnes.stream().findFirst().get());
  • Zeile 2: Um den Code zu vereinfachen, wenden wir die Methode [get] auf das Ergebnis Optional<Personne> an, das von der Methode [findFirst] erzeugt wurde. Ein sauberer Code würde vorsehen, dass die Methode [Optional<Personne>.isPresent()] aufgerufen wird, bevor die Methode [get] aufgerufen wird;

Das Ergebnis sieht wie folgt aus:

findFirst ----
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}

5.6.2. [findAny]


// beliebige Person
affiche("findAny", personnes.stream().findAny().get());

Die Methode [findAny] hat folgende Signatur:

 

Die Methode [findAny] kann jedes beliebige Element des Datenstroms ausgeben. Bei Tests fällt auf, dass bei einer sequenziellen Ausführung das erste Element des Datenstroms ausgegeben wird, während bei einer parallelen Ausführung tatsächlich jedes beliebige Element ausgegeben werden kann. Dies wird durch den folgenden Code [Exemple06c] veranschaulicht:


package dvp.java8.streams;

import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple06c {

    // Mapper jSON
    static private ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) throws JsonProcessingException {
        // Personenliste
        List<Personne> personnes = Personnes.get();
        // alle Personen
        affiche("all", personnes);
        // beliebige Person
        affiche("findAny parallèle", personnes.stream().parallel().findAny().get());
        // beliebige Person
        affiche("findAny séquentiel", personnes.stream().findAny().get());
    }

    public static <T> void affiche(String message, T value) throws JsonProcessingException {
        System.out.println(String.format("%s ----", message));
        System.out.println(jsonMapper.writeValueAsString(value));
    }
}
  • Zeile 22: findAny, parallel ausgeführt;
  • Zeile 24: findAny wird sequenziell ausgeführt;

Die erzielten Ergebnisse lauten wie folgt:

1
2
3
4
5
6
all ----
[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]
findAny parallèle ----
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
findAny séquentiel ----
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
  • Zeile 4: Die parallele Ausführung hat Element 2 der Personenliste zurückgegeben. Es hätte auch ein anderes sein können;
  • Zeile 6: Bei der sequenziellen Ausführung wurde das erste Element der Personenliste zurückgegeben;

Die Verwendung der Methode [findAny] scheint nur bei der parallelen Verarbeitung eines Datenstroms sinnvoll zu sein.

5.6.3. [skip]


// Personen ohne die erste
affiche("skip 1", personnes.stream().skip(1L).collect(Collectors.toList()));

Die Methode [skip] hat folgende Signatur:

 

Die Methode [skip] ignoriert die ersten n Elemente eines Datenstroms. Wie in der obigen Dokumentation angegeben, bringt die parallele Ausführung dieser Methode nur geringe Leistungsgewinne und kann sogar zu Leistungseinbußen führen. Denn um die ersten n Elemente zu ignorieren, müssen sich die Threads abstimmen, was die durch die Parallelität erzielten Leistungsgewinne zunichte macht.

Die Methode [skip] gibt einen Datenstrom vom Typ Stream<Personne> zurück, der durch die Methode [collect] in den Typ List<Personne> umgewandelt wird. Die Signatur dieser Methode lautet wie folgt:

 

Die Methode [collect] akzeptiert als Parameter eine Instanz des Typs [Collector], deren Signatur komplex ist. Es gibt vordefinierte Implementierungen des Typs [Collector], die es in den meisten Fällen ermöglichen, eine eigene Implementierung zu vermeiden. Hier wird die Implementierung [Collectors.toList()] verwendet. [Collectors] ist eine Klasse mit zahlreichen statischen Methoden, die den Typ [Collector<T,A,R>] implementieren. Dort sollte man zunächst suchen, wenn man ein Stream in eine Standard-Java-Sammlung umwandeln möchte:

 

Einige dieser Methoden werden wir im weiteren Verlauf verwenden.

Die Ausführung liefert folgendes Ergebnis:

skip 1 ----
[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]

Das erste Element der Liste (jean) wurde weggelassen.

5.6.4. [limit]


// die ersten beiden Personen
affiche("limit 2", personnes.stream().limit(2L).collect(Collectors.toList()));

Die Methode [limit] hat folgende Signatur:

 

Die Methode [limit] ermöglicht es, nur die ersten n Elemente eines Datenstroms beizubehalten. Sie ist nicht für die parallele Verarbeitung geeignet.

Die Ausführung liefert folgendes Ergebnis:

limit 2 ----
[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]

5.6.5. [count]


// Anzahl der Personen
affiche("count", personnes.stream().count());

Die Methode [count] hat folgende Signatur:

 

Die Methode [count] gibt die Anzahl der Elemente eines Stream zurück. Die parallele Ausführung der Methode bringt keinen Leistungsgewinn, wie der folgende Code (Beispiel06d1) zeigt:


package dvp.java8.streams;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Stream;

public class Exemple06d1 {
    public static void main(String[] args) {

        final long limite = 10_000_000L;
        // Anzahl der Prozessoren
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // Zahlenliste
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(i);
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // Zählung der Zahlen – sequentielle Methode
        Stream<Long> sNombres = nombres.stream();
        début = new Date().getTime();
        long count = sNombres.count();
        System.out.printf("comptage séquentiel : compteur=%s, durée (ms)=%s%n", count, new Date().getTime() - début);
    }
}
  • Zeilen 11–22: Es wird ein Stream mit 10 Millionen Zahlen erstellt;
  • Zeilen 22–24: Zählung des Stream;

Die Ausführung liefert folgendes Ergebnis:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4407 ms
comptage séquentiel : compteur=10000000, durée (ms)=67

Ersetzt man Zeile 22 des Codes durch die folgende (Beispiel06d2):


Stream<Long> sNombres = nombres.stream().parallel();

erhält man folgende Ergebnisse:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4341 ms
comptage parallèle : compteur=10000000, durée (ms)=100

5.6.6. [max, min]


// die älteste Person
affiche("age max", personnes.stream().max(Comparator.comparingInt(Personne::getAge)).get());

Die Methode [max] hat folgende Signatur:

 

Die Methode [max] gibt den Maximalwert eines Stroms unter Verwendung des als Parameter übergebenen Vergleichers zurück. Comparator ist eine funktionale Schnittstelle, deren einzige zu implementierende Methode die folgende Signatur hat: int compare (T o1, T o2). Diese Methode muss -1 zurückgeben, wenn o1 < o2, 0, wenn o1.equals(o2), und +1, wenn o1 > o2. Die funktionale Schnittstelle Comparator verfügt über zahlreiche statische Standardmethoden, die die Schnittstelle Comparator für die gängigsten Fälle implementieren. So gilt für die Anweisung:


affiche("age max", personnes.stream().max(Comparator.comparingInt(Personne::getAge)).get());

verwenden wir die statische Methode [Comparator.comparingInt], deren Signatur wie folgt lautet:

 

Der Typ ToIntFunction ist eine funktionale Schnittstelle:

 

Die Methode [applyAsInt] der Funktionsschnittstelle ToIntFunction erzeugt aus einem Typ T einen Typ int. Kehren wir zu unserem Code zurück:


affiche("age max", personnes.stream().max(Comparator.comparingInt(Personne::getAge)).get());

Der effektive Parameter der Methode [Comparator.comparingInt] muss hier ein Lambda vom Typ „Personne --> int“ sein. Wir übergeben die Referenz der Methode [Personne.getAge], die genau diese Signatur aufweist. Am Ende erhalten wir die Person mit dem höchsten Alter. Wir erhalten einen Typ Optional<Personne>, aus dem wir den Wert mit der Methode [Optional.get] extrahieren. Wir erhalten folgendes Ergebnis:

age max ----
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}

Die parallele Berechnung von max bringt keine Leistungssteigerung, wie das folgende Beispiel zeigt: (Beispiel06e1):


package dvp.java8.streams;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.stream.Stream;

public class Exemple06e1 {
    public static void main(String[] args) {

        // Daten
        // final long limit = 100L;
        // final boolean verbose = true;
        final long limite = 10_000_000L;
        final boolean verbose = false;

        // Anzahl der Prozessoren
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // Zahlenliste
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(new Random().nextLong());
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // Maximalwert der Zahlen – sequentielle Methode
        Stream<Long> sNombres = nombres.stream();
        Comparator<Long> compLong = (l1, l2) -> {
            if (verbose) {
                // Thread
                System.out.printf("[%s]", Thread.currentThread().getName());
            }
            // Vergleich
            long v1 = l1.longValue();
            long v2 = l2.longValue();
            if (v1 < v2) {
                return -1;
            } else {
                if (v1 == v2) {
                    return 0;
                } else {
                    return +1;
                }
            }
        };
        début = new Date().getTime();
        // Maximale Länge = sNombres.max(Comparator.naturalOrder()).get();
        long max = sNombres.max(compLong).get();
        System.out.printf("%nmax séquentiel : max=%s, durée (ms)=%s%n", max, new Date().getTime() - début);
    }
}
  • Zeile 29: Es gibt einen Datenstrom von limite Zufallszahlen vom Typ Long;
  • Zeilen 30–47: Die Lambda-Variable compLong implementiert die Schnittstelle Comparator<Long>. Diese Schnittstelle wird normalerweise durch die Methode [Comparator.naturalOrder()] in Zeile 49 implementiert. Hier möchten wir jedoch den Ausführungsthread anzeigen (Zeilen 31–33). Daher implementieren wir die Schnittstelle selbst;
  • Zeile 50: Suche nach max;

Wir erhalten folgende Ergebnisse:

 

Ersetzt man nun Zeile 27 durch die folgende (Beispiel06e2):


Stream<Long> sNombres = nombres.stream().parallel();

erhält man folgende Ergebnisse:

 

Die parallele Ausführung war also langsamer. Wenn man mit verbose=false auf 10 Millionen Zahlen umstellt, erhält man folgende Ergebnisse:

1
2
3
4
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 3764 ms

max séquentiel : max=9223370471463514417, durée (ms)=53

für die sequentielle Ausführung:

1
2
3
4
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 3760 ms

max parallèle : max=9223365260999360873, durée (ms)=77

für die parallele Ausführung, die somit weiterhin langsamer ist.

Die Methode [Stream.min] wird analog verwendet:


// die leichteste Person
affiche("poids min", personnes.stream().min(Comparator.comparingDouble(Personne::getPoids)).get());

5.6.7. [reduce]


// Gesamtalter aller Personen
affiche("âge total (reduce)", personnes.stream().map(p -> p.getAge()).reduce(0, (a1, a2) -> a1 + a2));

Die Methode [reduce] wurde in Abschnitt 5.3 vorgestellt. In Zeile 2 oben werden die Alterswerte aller Personen addiert. Das Ergebnis lautet wie folgt:

âge total (reduce) ----
60

5.6.8. [sorted]


// Personen nach aufsteigendem Alter
affiche("personnes par âge croissant",
                personnes.stream().sorted(Comparator.comparingInt(Personne::getAge)).collect(Collectors.toList()));
// Personen in alphabetischer Reihenfolge der Namen
List<Personne> lPersonnes=personnes.stream().sorted((p1, p2) -> p1.getNom().compareTo(p2.getNom())).collect(Collectors.toList());
affiche("personnes par ordre alphabétique des noms", lPersonnes);

Die Methode [sorted] (Zeilen 3 und 5) hat folgende Signatur:

 

Die Methode [sorted] akzeptiert als Parameter den Typ [Comparator], der in Abschnitt 5.6.6 für die Methoden min und max beschrieben ist. Sie ermöglicht es, einen Stream in der Reihenfolge des als Parameter übergebenen Vergleichers zu sortieren. Wir haben gesehen, dass die Schnittstelle [Comparator] standardmäßig mehrere statische Methoden bereitstellt, die gängige Vergleicher implementieren, insbesondere für Zahlen und Zeichenketten. Hier verwenden wir die Methode [Comparator.comparingInt], die als Parameter einen Typ ToIntFunction akzeptiert, bei dem es sich um eine funktionale Schnittstelle der Methode [applyAsInt] mit folgender Signatur handelt: int applyAsInt(T t). Hier ist der tatsächliche Parameter, der in Zeile 3 an die Methode [Comparator.comparingInt] übergeben wird, die Referenz auf die Methode [Personne.age], die das Alter der Person angibt.

Die Schnittstelle [Comparator] bietet keine statischen Methoden zum Vergleichen von Zeichenketten. In Zeile 5 erstellen wir selbst ein Lambda, das die einzige Methode dieser Schnittstelle implementiert: int compare(T t1, T t2)


(p1, p2) -> p1.getNom().compareTo(p2.getNom())

Dieses Lambda vergleicht die Namen der Personen. Die Ergebnisse lauten wie folgt:

1
2
3
4
personnes par âge croissant ----
[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]
personnes par ordre alphabétique des noms ----
[{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"},{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]

Eine parallele Ausführung der Sortierung scheint nicht möglich zu sein, wie der folgende Code (Beispiel06f1) zeigt:


package dvp.java8.streams;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Exemple06f1 {
    // Mapper jSON
    static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) throws JsonProcessingException {

        // Daten
        final long limite = 100L;
        final boolean verbose = true;
//         endgültige lange Grenze = 10_000_000L;
//         final boolean verbose = false;

        // Anzahl der Prozessoren
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // Zahlenliste
        long début = new Date().getTime();
        List<Integer> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(new Random().nextInt(1000));
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // Sortierung der Zahlen – sequentielle Methode
        Stream<Integer> sNombres = nombres.stream();
        début = new Date().getTime();
        Comparator<Integer> compInt = (i1, i2) -> {
            if (verbose) {
                // Thread
                System.out.printf("[%s]", Thread.currentThread().getName());
            }
            // Vergleich
            int v1 = i1.intValue();
            int v2 = i2.intValue();
            if (v1 < v2) {
                return +1;
            } else {
                if (v1 == v2) {
                    return 0;
                } else {
                    return -1;
                }
            }
        };
        if (verbose) {
            affiche("nombres", sNombres.sorted(compInt).collect(Collectors.toList()));
        }
        System.out.printf("tri séquentiel : durée (ms)=%s%n", new Date().getTime() - début);
    }

    public static <T> void affiche(String message, T value) throws JsonProcessingException {
        System.out.println(String.format("%s ----", message));
        System.out.println(jsonMapper.writeValueAsString(value));
    }

}
  • Zeilen 30–36: Es wird ein Strom aus Zufallszahlen generiert;
  • Zeile 32: Das Lambda compInt (Zeilen 38–55) wird an die Methode [sorted] übergeben. Dieses Lambda sortiert die Zahlen in absteigender Reihenfolge und gibt den Thread aus, der es ausführt.

Die folgenden Ergebnisse werden erzielt:

 

Wenn man im obigen Code Zeile 36 durch die folgende ersetzt (Beispiel06f2):


        Stream<Integer> sNombres = nombres.stream().parallel();        

erhält man folgende Ergebnisse:

 

Erstaunlicherweise stellt man fest, dass die Sortierung des Zahlenstroms mit einem einzigen Thread durchgeführt wurde. Es gab keinerlei Parallelität. Oder habe ich hier etwas übersehen?

5.6.9. [anyMatch, noneMatch, allMatch]


// Gibt es Personen, die älter als 100 Jahre sind?
affiche("des personnes de + de 100 ans (anyMatch)", personnes.stream().anyMatch(p -> p.getAge() > 100));
// Sind alle Personen höchstens 100 Jahre alt?
affiche("des personnes de + de 100 ans (noneMatch)", personnes.stream().noneMatch(p -> p.getAge() > 100));
// Sind alle Personen älter als 8 Jahre?
affiche("des personnes de + de 8 ans (allMatch)", personnes.stream().allMatch(p -> p.getAge() > 8));

In den Zeilen 2, 4 und 6 haben die Methoden [anyMatch, noneMatch, allMatch] als Parameter einen Typ Predicate, der in Abschnitt 4.2 beschrieben ist. Sie führen also eine Filterung durch. Alle drei geben einen booleschen Wert zurück:

  • anyMatch gibt true zurück, wenn mindestens ein Element des Stream vorhanden ist, das den Filter erfüllt;
  • noneMatch ergibt true, wenn kein Element von Stream den Filter erfüllt;
  • allMatch ergibt true, wenn alle Elemente von Stream den Filter erfüllen;

Die folgenden Ergebnisse werden erzielt:

1
2
3
4
5
6
des personnes de + de 100 ans (anyMatch) ----
false
des personnes de + de 100 ans (noneMatch) ----
true
des personnes de + de 8 ans (allMatch) ----
true

5.6.10. [collect(Collectors.groupingBy)]


// Die Personen werden nach Geschlecht gruppiert
affiche("personnes regroupées par sexe", personnes.stream().collect(Collectors.groupingBy(p -> p.getSexe())));

Die Methode [collect] wurde in Abschnitt 5.6.3 vorgestellt. Ihr Parameter ist eine Implementierung der Schnittstelle [Collector]. Die Klasse [Collectors] bietet eine Reihe statischer Methoden, die die Schnittstelle [Collector] implementieren. Bisher haben wir die Methode [Collectors.toList()] verwendet. Hier verwenden wir die statische Methode [Collectors.groupingBy], die aus Stream ein Dictionary erstellt. Ihre Signatur lautet wie folgt:

 

Die Methode [groupingBy] erzeugt aus einem Typ Stream<T> einen Typ Map<K,List<T>>. Der Schlüssel K wird durch den Parameter der Methode [groupingBy] vom Typ Function<T,K> bereitgestellt, deren einzige Methode folgende Signatur hat: K apply(T t). Wenn man ein nach dem Geschlecht der Personen indiziertes Wörterbuch erstellen möchte, muss man eine Funktion bereitstellen, die das Geschlecht einer Person ermittelt. Hier übergeben wir als effektiven Parameter der Methode [groupingBy] die Referenz der Methode [Personne.getSexe]. Die erhaltenen Ergebnisse lauten wie folgt:

personnes regroupées par sexe ----
{"HOMME":[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}],"FEMME":[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]}

In Zeile 2 befindet sich die Zeichenkette jSON aus einem Wörterbuch, das durch zwei Schlüssel indiziert ist: HOMME und FEMME.

Die parallele Berechnung bringt keine Leistungssteigerung, wie das folgende Beispiel (Beispiel06g1) zeigt:


package dvp.java8.streams;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Exemple06g1 {

    // mppeur jSON
    static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) throws JsonProcessingException {

        // Daten
        final long limite = 100L;
        final boolean verbose = true;
//         Endgültige lange Grenze = 10_000_000L;
//         final boolean verbose = false;

        // Anzahl der Prozessoren
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // Zahlenliste
        long début = new Date().getTime();
        List<Integer> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(new Random().nextInt(1000));
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // Gruppierung der Zahlen nach Hundertern – sequentielle Methode
        Stream<Integer> sNombres = nombres.stream();
        Function<Integer, Integer> groupByCent = n -> {
            if (verbose) {
                System.out.printf("[%s]", Thread.currentThread().getName());
            }
            return n / 100;
        };
        début = new Date().getTime();
        // Map<Integer, List<Integer>> lNombres = sNombres.collect(Collectors.groupingBy(Zahl -> Zahl / 100));
        Map<Integer, List<Integer>> lNombres = sNombres.collect(Collectors.groupingBy(groupByCent));
        System.out.printf("%nregroupement séquentiel : durée (ms)=%s%n", new Date().getTime() - début);
        // Ergebnisse
        if (verbose) {
            affiche("nombres regroupés", lNombres);
        }
    }

    public static <T> void affiche(String message, T value) throws JsonProcessingException {
        System.out.println(String.format("%s ----", message));
        System.out.println(jsonMapper.writeValueAsString(value));
    }

}
  • Zeilen 23–38: Aufbau eines Datenstroms aus limite-Zahlen;
  • Zeile 47: Die Zahlen werden in Hundertergruppen zusammengefasst. Die Lambda-Funktion in den Zeilen 39–44 wird verwendet, um den Ausführungsthread anzeigen zu können;

Die Ausführungsergebnisse lauten wie folgt:

 

Wenn man im Code Zeile 38 durch die folgende Zeile ersetzt (Beispiel06g2):


Stream<Integer> sNombres = nombres.stream().parallel();            

erhält man folgende Ergebnisse:

 

Man sieht, dass die parallele Ausführung der Gruppierung die Leistung beeinträchtigt hat.

5.6.11. [distinct]


// Entfernen doppelter Elemente aus einer Liste
affiche("distinct", Stream.of(1, 2, 1).distinct().collect(Collectors.toList()));

Die Methode [distinct] hat folgende Signatur:

 

Sie dient dazu, Duplikate aus einem Datenstrom zu entfernen. Die Methode [Stream.of] (Zeile 2) hat folgende Signatur:

 

Sie ermöglicht es, ein Stream aus explizit angegebenen Werten zu erstellen. Die Ergebnisse der Ausführung lauten wie folgt:

distinct ----
[1,2]

5.6.12. [flatMap]


// aus einem Stream<Stream<T>> wird ein Stream<T> gebildet
affiche("flatMap", Stream.of(1, 2, 3).flatMap(i -> Stream.of(i, i + 10)).collect(Collectors.toList()));

Die Methode [flatMap] hat folgende Signatur:

 

Die Methode [flatMap] akzeptiert als Parameter eine Funktion, die:

  • als Parameter ein Element vom Typ T der Methode Stream akzeptiert;
  • als Ergebnis einen Datenstrom vom Typ Stream<R> liefert;

Hätte man anstelle der Methode [flatMap] die in Abschnitt 5.5 beschriebene Methode [map] verwendet worden wäre, wäre das Ergebnis ein Typ Stream<Stream<R>>, bei dem jedes Element vom Typ T des ursprünglichen Datenstroms ein Element vom Typ Stream<R> erzeugt hätte. Die Methode [flatMap] liefert hingegen einen Typ Stream<R>. Sie fasst die verschiedenen Ströme Stream<R> zu einem einzigen Strom zusammen. Dies zeigen die Ergebnisse der Ausführung des vorstehenden Codes:

flatMap ----
[1,11,2,12,3,13]

Es gibt spezielle Varianten von [flatMap]:


// Aus einem Stream<IntStream> wird ein IntStream gebildet, dessen Summe berechnet wird
affiche("flatMapToInt", Stream.of(1, 2, 3).flatMapToInt(i -> IntStream.of(i, i + 10)).sum());

Die Methode [flatMapToInt] hat folgende Signatur:

 

Die Methode [flatMapToInt] akzeptiert als Parameter eine Funktion, die als Ergebnis einen Typ IntStream liefert, der wie folgt aussieht:

 

IntStream ist ein Ableger von int. Dieser Typ ist dem Typ Stream<Integer> vorzuziehen, da bei seiner Verarbeitung das Boxing und Unboxing zwischen den Typen Integer und int vermieden wird. Diese Schnittstelle übernimmt zahlreiche Methoden des Typs Stream<T> und fügt weitere hinzu, darunter die oben genannte Methode [sum], die die Elemente des IntStream summiert.

Der folgende Code veranschaulicht die Verwendung der analogen Methode [flatMapToDouble]:


// aus einem Stream<DoubleStream> wird ein DoubleStream gebildet und anschließend ein Array
affiche("flatMapToDouble", Stream.of(1, 2, 3).flatMapToDouble(i -> DoubleStream.of(i, i * 1.2)).toArray());

Die Methode [DoubleStream.toArray] ermöglicht den Wechsel von einem Typ DoubleStream zu einem Typ double[].

Die Ergebnisse lauten für diese beiden Beispiele wie folgt:

1
2
3
4
flatMapToInt ----
42
flatMapToDouble ----
[1.0,1.2,2.0,2.4,3.0,3.5999999999999996]

Das folgende Beispiel zeigt die Leistungssteigerungen, die durch den Wechsel von einem Typ Stream<Long> zu einem Typ LongStream erzielt werden (Beispiel06i1):


package dvp.java8.streams;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class Exemple06i1 {
    public static void main(String[] args) {

        final long limite = 10_000_000L;
        // Anzahl der Prozessoren
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // Liste von Zahlen
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(i);
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // Summe der Zahlen – sequentielle Methode
        début = new Date().getTime();
        long somme = nombres.stream().reduce(0L, (s, i) -> s + i);
        System.out.printf("somme séquentielle du Stream<Integer> : somme=%s, durée (ms)=%s%n", somme, new Date().getTime() - début);
    }
}
  • Zeile 22: Berechnung der Summe eines Zahlenflusses vom Typ Long;

Es ergeben sich folgende Ergebnisse:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4537 ms
somme séquentielle du Stream<Integer> : somme=49999995000000, durée (ms)=226

Ersetzen wir nun Zeile 22 durch die folgende (Beispiel06i2):


long somme = nombres.stream().mapToLong(n -> n.longValue()).sum();

Mit der Methode Stream&lt;Integer&gt;.mapToLong erhalten wir einen Strom vom Typ *LongStream aus Elementen des primitiven Typs *long, den wir anschließend mit der Funktion *sum* summieren. Daraus ergeben sich folgende Ergebnisse:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4511 ms
somme séquentielle du LongStream : somme=49999995000000, durée (ms)=99

Der Leistungsgewinn ist deutlich.

5.6.13. Methoden für den Fluss primitiver Zahlen


// Maximalwert eines int-Stroms
affiche("IntStream max", IntStream.of(1, 10, 8).max());
// Minimum eines Double-Stroms
affiche("DoubleStream min", DoubleStream.of(1.5, 10.4, 8.9).min());
// Mittelwert eines Stroms von int
affiche("IntStream average", IntStream.of(1, 10, 8).average().getAsDouble());
// Statistiken eines int-Stroms
affiche("IntStream summaryStatistics", IntStream.of(1, 10, 8).summaryStatistics());

Die Datenströme für primitive Werte (int, long, double) bieten auf diese Typen zugeschnittene Methoden. Das Ergebnis der Ausführung des vorherigen Codes lautet wie folgt:

1
2
3
4
5
6
7
8
IntStream max ----
{"asInt":10,"present":true}
DoubleStream min ----
{"asDouble":1.5,"present":true}
IntStream average ----
6.333333333333333
IntStream summaryStatistics ----
{"count":3,"sum":19,"min":1,"max":10,"average":6.333333333333333}
  • Das Ergebnis von Zeile 2 des Codes ist ein Typ OptionalInt, der dem Typ Optional<Integer> ähnelt. Der in diesem Objekt gespeicherte Wert kann mit der Methode [getAsInt()] abgerufen werden. Das Vorhandensein eines Werts kann mit der Methode [isPresent()] geprüft werden. Zeile 2 der Ergebnisse bedeutet nicht, dass die Klasse [OptionalInt] Felder mit dem Namen [asInt, present] enthält. Standardmäßig verwendet die Bibliothek jSON alle öffentlichen Methoden getX und isY des zu serialisierenden Objekts in jSON. Und hier gibt es tatsächlich eine Methode [getAsInt] und eine weitere Methode [isPresent], ohne dass die Felder [asInt, present] selbst existieren;
  • Das Ergebnis von Zeile 4 des Codes ist ein Typ OptionalDouble, der dem Typ Optional<Double> ähnelt;
  • Das Ergebnis von Zeile 6 des Codes ist ein Typ OptionalDouble, dessen Wert mit der Methode [getAsDouble()] ermittelt werden kann. Die Methode [average] berechnet den Mittelwert des Zahlenflusses;
  • Das Ergebnis von Zeile 8 des Codes ist ein Typ IntSummaryStatistics, der wie folgt definiert ist:
 

Man sieht, dass das erhaltene Objekt IntSummaryStatistics verschiedene Informationen über den Zahlenfluss liefert, wie z. B. Anzahl der Werte, Summe, Maximalwert, Minimalwert und Mittelwert.