Comment utiliser les Stream primitifs en Java pour optimiser le traitement des données numériques ?

Jusqu’à présent, tous les Stream que nous avons créés utilisaient l’interface Stream avec un type générique, comme Stream<String>, Stream<Integer>, et ainsi de suite. Pour les valeurs numériques, nous avons utilisé des classes enveloppes. Nous avons fait cela avec l’API Collections, donc cela devrait sembler naturel.

Java inclut en réalité d’autres classes de stream en plus de Stream que vous pouvez utiliser pour travailler avec certains types primitifs : int, double, et long. Voyons pourquoi c’est nécessaire. Supposons que nous voulions calculer la somme des nombres dans un stream fini :

Stream<Integer> stream = Stream.of(1, 2, 3);
System.out.println(stream.reduce(0, (s, n) -> s + n)); // 6

Pas mal. Ce n’était pas difficile d’écrire une réduction. Nous avons commencé l’accumulateur avec zéro. Nous avons ensuite ajouté chaque nombre à ce total courant au fur et à mesure qu’il apparaissait dans le strem. Il existe une autre façon de faire, montrée ici :

Stream<Integer> stream = Stream.of(1, 2, 3);
System.out.println(stream.mapToInt(x -> x).sum()); // 6

Cette fois, nous avons converti notre Stream<Integer> en IntStream et demandé à l’IntStream de calculer la somme pour nous. Un IntStream a beaucoup des mêmes méthodes intermédiaires et terminales qu’un Stream, mais inclut des méthodes spécialisées pour travailler avec des données numériques. Les stream primitifs savent comment effectuer automatiquement certaines opérations courantes.

Jusqu’ici, cela semble être une belle commodité, mais pas terriblement important. Maintenant, réfléchissez à comment vous calculeriez une moyenne. Vous devez diviser la somme par le nombre d’éléments. Le problème est que les stream ne permettent qu’un seul passage. Java reconnaît que le calcul d’une moyenne est une opération courante, et fournit une méthode pour calculer la moyenne sur les classes de stream pour les primitifs.

IntStream intStream = IntStream.of(1, 2, 3);
OptionalDouble avg = intStream.average();
System.out.println(avg.getAsDouble()); // 2.0

Non seulement il est possible de calculer la moyenne, mais c’est aussi facile à faire. Clairement, les stream primitifs sont importants. Nous allons examiner la création et l’utilisation de tels stream, y compris les optionnels et les interfaces fonctionnelles.

Créer des stream Primitifs

Voici les trois types de stream primitifs :

  • IntStream : Utilisé pour les types primitifs int, short, byte, et char
  • LongStream : Utilisé pour le type primitif long
  • DoubleStream : Utilisé pour les types primitifs double et float

Pourquoi chaque type primitif n’a-t-il pas son propre stream primitif ? Ces trois sont les plus courants, donc les concepteurs de l’API ont opté pour eux.

Lorsque vous voyez le mot stream, faites attention à la casse. Avec un S majuscule ou dans le code, Stream est le nom d’une classe qui contient un type Object. Avec un s minuscule, un stream est un concept qui peut être un Stream, DoubleStream, IntStream, ou LongStream.

Le Tableau 10.5 montre certaines des méthodes qui sont propres aux stream primitifs. Notez que nous n’incluons pas dans le tableau des méthodes comme empty() que vous connaissez déjà de l’interface Stream.

Méthodestream primitifDescription
OptionalDouble average()IntStream
LongStream
DoubleStream
Moyenne arithmétique des éléments
Stream<T> boxed()IntStream
LongStream
DoubleStream
Stream<T> où T est la classe enveloppe associée à la valeur primitive
OptionalInt max()IntStreamÉlément maximum du stream
OptionalLong max()LongStream 
OptionalDouble max()DoubleStream 
OptionalInt min()IntStreamÉlément minimum du stream
OptionalLong min()LongStream 
OptionalDouble min()DoubleStream 
IntStream range(int a, int b)IntStreamRenvoie un stream primitif de a (inclus) à b (exclus)
LongStream range(long a, long b)LongStream 
IntStream rangeClosed(int a, int b)IntStreamRenvoie un stream primitif de a (inclus) à b (inclus)
LongStream rangeClosed(long a, long b)LongStream 
int sum()IntStreamRenvoie la somme des éléments du stream
long sum()LongStream 
double sum()DoubleStream 
IntSummaryStatistics summaryStatistics()IntStreamRenvoie un objet contenant de nombreuses statistiques du stream comme la moyenne, min, max, etc.
LongSummaryStatistics summaryStatistics()LongStream 
DoubleSummaryStatistics summaryStatistics()DoubleStream 

Certaines des méthodes pour créer un stream primitif sont équivalentes à celles utilisées pour créer la source d’un Stream normal. Vous pouvez créer un stream vide avec ceci :

DoubleStream empty = DoubleStream.empty();

Une autre façon est d’utiliser la méthode factory of() à partir d’une seule valeur ou en utilisant la surcharge varargs.

DoubleStream oneValue = DoubleStream.of(3.14);
oneValue.forEach(System.out::println);
DoubleStream varargs = DoubleStream.of(1.0, 1.1, 1.2);
varargs.forEach(System.out::println);

Ce code génère la sortie suivante :

3.14
1.0
1.1
1.2

Vous pouvez également utiliser les deux méthodes pour créer des stream infinis, tout comme nous l’avons fait avec Stream.

var random = DoubleStream.generate(Math::random);
var fractions = DoubleStream.iterate(.5, d -> d / 2);
random.limit(3).forEach(System.out::println);
fractions.limit(3).forEach(System.out::println);

Puisque les stream sont infinis, nous avons ajouté une opération intermédiaire limit pour que la sortie n’imprime pas les valeurs indéfiniment. Le premier stream appelle une méthode statique sur Math pour obtenir un double aléatoire. Puisque les nombres sont aléatoires, votre sortie sera évidemment différente. Le second stream continue à créer des nombres plus petits, divisant la valeur précédente par deux à chaque fois. La sortie lorsque nous avons exécuté ce code était :

0.07890654781186413
0.28564363465842346
0.6311403511266134
0.5
0.25
0.125

La classe Random fournit une méthode pour obtenir des stream primitifs de nombres aléatoires directement. Par exemple, ints() génère un IntStream infini de primitifs.

Cela fonctionne de la même manière pour chaque type de stream primitif. Lorsqu’on traite des primitifs int ou long, il est courant de compter. Supposons que nous voulions un stream avec les nombres de 1 à 5. Nous pourrions écrire ceci en utilisant ce que nous avons expliqué jusqu’à présent :

IntStream count = IntStream.iterate(1, n -> n+1).limit(5);
count.forEach(System.out::print); // 12345

Ce code imprime bien les nombres 1-5. Cependant, c’est beaucoup de code pour faire quelque chose de si simple. Java fournit une méthode qui peut générer une plage de nombres.

IntStream range = IntStream.range(1, 6);
range.forEach(System.out::print); // 12345

C’est mieux. Si nous voulions les nombres 1-5, pourquoi avons-nous passé 1-6 ? Le premier paramètre de la méthode range() est inclusif, ce qui signifie qu’il inclut le nombre. Le second paramètre de la méthode range() est exclusif, ce qui signifie qu’il s’arrête juste avant ce nombre. Cependant, ce pourrait être plus clair. Nous voulons les nombres 1-5 inclus. Heureusement, il existe une autre méthode, rangeClosed(), qui est inclusive sur les deux paramètres.

IntStream rangeClosed = IntStream.rangeClosed(1, 5);
rangeClosed.forEach(System.out::print); // 12345

Encore mieux. Cette fois, nous avons exprimé que nous voulons une plage fermée ou une plage inclusive. Cette méthode correspond mieux à la façon dont nous exprimons une plage de nombres en français courant.

Mappage des stream

Une autre façon de créer un stream primitif est de faire un mappage à partir d’un autre type de stream. Le Tableau 10.6 montre qu’il existe une méthode pour mappage entre tous les types de stream.

Classe de stream sourcePour créer StreamPour créer DoubleStreamPour créer IntStreamPour créer LongStream
Stream<T>map()mapToDouble()mapToInt()mapToLong()
DoubleStreammapToObj()map()mapToInt()mapToLong()
IntStreammapToObj()mapToDouble()map()mapToLong()
LongStreammapToObj()mapToDouble()mapToInt()map()

Évidemment, les types doivent être compatibles pour que cela fonctionne. Java exige qu’une fonction de mappage soit fournie comme paramètre, par exemple :

Stream<String> objStream = Stream.of("pingouin", "poisson");
IntStream intStream = objStream.mapToInt(s -> s.length());

Cette fonction prend un Object, qui est un String dans ce cas. La fonction renvoie un int. Les mappages de fonctions sont intuitifs ici. Ils prennent le type source et renvoient le type cible. Dans cet exemple, le type de fonction réel est ToIntFunction. Le Tableau 10.7 montre les noms des fonctions de mappage. Comme vous pouvez le voir, ils font ce que vous pourriez attendre.

Classe de stream sourcePour créer StreamPour créer DoubleStreamPour créer IntStreamPour créer LongStream
Stream<T>Function<T,R>ToDoubleFunction<T>ToIntFunction<T>ToLongFunction<T>
DoubleStreamDoubleFunction<R>DoubleUnaryOperatorDoubleToIntFunctionDoubleToLongFunction
IntStreamIntFunction<R>IntToDoubleFunctionIntUnaryOperatorIntToLongFunction
LongStreamLongFunction<R>LongToDoubleFunctionLongToIntFunctionLongUnaryOperator

Pour le Tableau 10.6, mapper vers le même type avec lequel vous avez commencé s’appelle simplement map(). Lors du retour d’un stream d’objets, la méthode est mapToObj(). Au-delà de cela, c’est le nom du type primitif dans le nom de la méthode map.

Pour le Tableau 10.7, vous pouvez commencer par réfléchir aux types source et cible. Lorsque le type cible est un objet, vous supprimez le To du nom. Lorsque le mappage est vers le même type avec lequel vous avez commencé, vous utilisez un opérateur unaire au lieu d’une fonction pour les stream primitifs.

Utilisation de flatMap()

Nous pouvons utiliser cette approche sur les stream primitifs également. Cela fonctionne de la même manière que sur un Stream normal, sauf que le nom de la méthode est différent. Voici un exemple :

var entierListe = new ArrayList<Integer>();
IntStream ints = entierListe.stream()
    .flatMapToInt(x -> IntStream.of(x));
DoubleStream doubles = entierListe.stream()
    .flatMapToDouble(x -> DoubleStream.of(x));
LongStream longs = entierListe.stream()
    .flatMapToLong(x -> LongStream.of(x));

De plus, vous pouvez créer un Stream à partir d’un stream primitif. Ces méthodes montrent deux façons de le faire :

private static Stream<Integer> mapping(IntStream stream) {
    return stream.mapToObj(x -> x);
}
private static Stream<Integer> boxing(IntStream stream) {
    return stream.boxed();
}

La première utilise la méthode mapToObj() que nous avons vue précédemment. La seconde est plus concise. Elle ne nécessite pas de fonction de mappage car tout ce qu’elle fait est d’autoboxer chaque primitif vers l’objet wrapper correspondant. La méthode boxed() existe sur les trois types de stream primitifs.

Utilisation d’Optional avec les stream Primitifs

Plus tôt dans ce chapitre, nous avons écrit une méthode pour calculer la moyenne d’un int[] et promis une meilleure façon plus tard. Maintenant que vous connaissez les stream primitifs, vous pouvez calculer la moyenne en une ligne.

var stream = IntStream.rangeClosed(1,10);
OptionalDouble optional = stream.average();

Le type de retour n’est pas l’Optional auquel vous êtes habitué. C’est un nouveau type appelé OptionalDouble. Puisque nous avons un type séparé, vous pourriez vous demander : pourquoi ne pas simplement utiliser Optional<Double> ? La différence est que OptionalDouble est pour un primitif, et Optional<Double> est pour la classe enveloppe Double. Travailler avec la classe optional primitive ressemble à travailler avec la classe Optional elle-même.

optional.ifPresent(System.out::println); // 5.5
System.out.println(optional.getAsDouble()); // 5.5
System.out.println(optional.orElseGet(() -> Double.NaN)); // 5.5

La seule différence notable est que nous avons appelé getAsDouble() plutôt que get(). Cela indique clairement que nous travaillons avec un primitif. De plus, orElseGet() prend un DoubleSupplier au lieu d’un Supplier.

Comme pour les stream primitifs, il existe trois classes spécifiques aux types pour les primitifs. Le Tableau 10.8 montre les différences mineures entre les trois. C’est vraiment facile à retenir puisque le nom primitif est le seul changement. Comme vous devriez vous rappeler de la section sur les opérations terminales, un certain nombre de méthodes de stream renvoient un optional, comme min() ou findAny(). Chacune renvoie le type optional correspondant. Les implémentations de stream primitifs ajoutent également deux nouvelles méthodes que vous devez connaître. La méthode sum() ne renvoie pas un optional. Si vous essayez d’additionner un stream vide, vous obtenez simplement zéro. La méthode average() renvoie toujours un OptionalDouble puisqu’une moyenne peut potentiellement avoir des données fractionnaires pour n’importe quel type.

 OptionalDoubleOptionalIntOptionalLong
Obtention comme primitifgetAsDouble()getAsInt()getAsLong()
Type de paramètre orElseGet()DoubleSupplierIntSupplierLongSupplier
Type de retour de max() et min()OptionalDoubleOptionalIntOptionalLong
Type de retour de sum()doubleintlong
Type de retour de average()OptionalDoubleOptionalDoubleOptionalDouble

Essayons un exemple pour nous assurer que vous comprenez ceci :

LongStream longs = LongStream.of(5, 10);
long sum = longs.sum();
System.out.println(sum); // 15
DoubleStream doubles = DoubleStream.generate(() -> Math.PI);
OptionalDouble min = doubles.min(); // s'exécute indéfiniment

La ligne 1 crée un stream de primitifs long avec deux éléments. La ligne 2 montre que nous n’utilisons pas un optional pour calculer une somme. La ligne 4 crée un stream infini de primitifs double. La ligne 5 est là pour vous rappeler qu’une question sur du code qui s’exécute indéfiniment peut apparaître avec les stream primitifs également.

Statistiques Récapitulatives

Vous avez appris suffisamment pour être capable d’obtenir la valeur maximale à partir d’un stream de primitifs. Si le stream est vide, nous voulons lancer une exception.

private static int max(IntStream ints) {
    OptionalInt optional = ints.max();
    return optional.orElseThrow(RuntimeException::new);
}

Cela devrait être familier maintenant. Nous avons obtenu un OptionalInt parce que nous avons un IntStream. Si l’optional contient une valeur, nous la renvoyons. Sinon, nous lançons une nouvelle RuntimeException.

Maintenant, nous voulons changer la méthode pour prendre un IntStream et renvoyer une plage. La plage est la valeur minimale soustraite de la valeur maximale. Oh oh. min() et max() sont des opérations terminales, ce qui signifie qu’elles consomment le stream lorsqu’elles sont exécutées. Nous ne pouvons pas exécuter deux opérations terminales sur le même stream. Heureusement, c’est un problème courant, et les stream primitifs le résolvent pour nous avec les statistiques récapitulatives. Une statistique est juste un grand mot pour un nombre qui a été calculé à partir de données.

private static int range(IntStream ints) {
    IntSummaryStatistics stats = ints.summaryStatistics();
    if (stats.getCount() == 0) throw new RuntimeException();
    return stats.getMax()-stats.getMin();
}

Ici, nous avons demandé à Java d’effectuer de nombreux calculs sur le stream. Les statistiques récapitulatives incluent ce qui suit :

  • getCount() : Renvoie un long représentant le nombre de valeurs.
  • getAverage() : Renvoie un double représentant la moyenne. Si le stream est vide, renvoie 0.
  • getSum() : Renvoie la somme comme un double pour DoubleSummaryStream et long pour IntSummaryStream et LongSummaryStream.
  • getMin() : Renvoie le plus petit nombre (minimum) comme un double, int, ou long, selon le type du stream. Si le stream est vide, renvoie la plus grande valeur numérique basée sur le type.
  • getMax() : Renvoie le plus grand nombre (maximum) comme un double, int, ou long selon le type du stream. Si le stream est vide, renvoie la plus petite valeur numérique basée sur le type.