Conception par Objet et Programmation Java

Ce document présente les concepts fondamentaux des streams en Java, destinés aux étudiants en programmation orientée objet souhaitant comprendre la programmation fonctionnelle et la manipulation des flux de données avec l'API Stream. Introduction à la programmation fonctionnelle La programmation fonctionnelle conçoit les programmes comme des fonctions mathématiques composables.

D'après le document Conception par Objet et Programmation Java

Cet article a été rédigé automatiquement à partir du document source, puis vérifié avant publication.

Document source

Ce document présente les concepts fondamentaux des streams en Java, destinés aux étudiants en programmation orientée objet souhaitant comprendre la programmation fonctionnelle et la manipulation des flux de données avec l'API Stream.

Introduction à la programmation fonctionnelle

La programmation fonctionnelle conçoit les programmes comme des fonctions mathématiques composables. Contrairement aux programmes impératifs qui s'organisent en instructions produisant des effets de bord, les programmes fonctionnels sont constitués d'expressions dont la valeur est le résultat final.

Un programme fonctionnel est une expression E représentant l'algorithme et ses entrées. Cette expression est soumise à des règles de réécriture appelées réductions, qui remplacent des parties du programme par d'autres selon des règles définies. Ce processus se répète jusqu'à obtenir une expression irréductible, appelée forme normale (fn) de E, qui constitue la sortie du programme.

Définition d’un Stream

Un Stream<T> décrit une séquence d’éléments de type T, supportant des opérations agrégées en séquence ou en parallèle. L’API Stream fournit des classes permettant d’effectuer des opérations de style fonctionnel sur des flux de valeurs.

Il est important de ne pas considérer les streams comme une alternative aux collections, mais plutôt comme un moyen de traitement associé. Un stream n’est pas un moyen de stockage : il transmet des données provenant d’une source (collection, tableau, générateur, canal d’E/S, etc.) à travers une file d’opérations.

Un stream est fonctionnel par nature : une opération sur un stream produit un résultat sans modifier la source. Par exemple, un filtrage crée un nouveau stream avec les éléments filtrés sans supprimer quoi que ce soit de la source.

Les streams ont un mode d’exécution paresseux (lazy). Les opérations sont de deux types :

  • Intermédiaires : renvoient un nouveau stream (ex. : filtrage) et sont évaluées paresseusement.
  • Terminales : déclenchent l’exécution du pipeline, produisent un résultat et ferment le stream.

Un stream est un consommateur : ses éléments ne peuvent être visités qu’une seule fois. Pour parcourir à nouveau la source, il faut créer un nouveau stream.

Obtention d’un Stream

Plusieurs méthodes permettent de créer un stream :

  • Depuis une collection avec stream() ou parallelStream().
  • Depuis un tableau avec Arrays.stream(Object[]).
  • Via des méthodes statiques de l’interface Stream<T> comme Stream.of(Object[]), IntStream.range(int,int) ou Stream.iterate(Object, UnaryOperator).
  • Lors de la lecture d’un fichier avec BufferedReader.lines().

Opérations sur les Streams

L’intérêt principal d’un stream réside dans la possibilité d’enchaîner plusieurs opérations complexes en une seule instruction, appelée pipeline. Ce pipeline est composé de plusieurs opérations intermédiaires suivies d’une seule opération terminale.

Opérations intermédiaires

Chaque opération intermédiaire retourne un nouveau stream, permettant ainsi l’enchaînement.

Principales opérations intermédiaires :

  • Stream<T> filter(Predicate<T>) : filtre les éléments selon un prédicat.
  • Stream<T> sorted() et Stream<T> sorted(Comparator<T>) : trie les éléments.
  • Stream<T> distinct() : élimine les doublons.
  • Stream<T> limit(long maxSize) : limite le nombre d’éléments.
  • Stream<T> peek(Consumer<T>) : exécute une action sur chaque élément.
  • Stream<T> skip(long n) : ignore les n premiers éléments.
  • <R> Stream<R> map(Function<T, R>) : transforme les éléments.
  • <R> Stream<R> flatMap(Function<T, Stream<R>>) : transforme et aplatit les éléments.

Opérations terminales

Une opération terminale déclenche l’exécution du pipeline, consomme le flux d’entrée, produit un résultat et ferme le stream.

Principales opérations terminales :

Booléennes

  • allMatch(Predicate<T>) : vrai si tous les éléments satisfont le prédicat.
  • anyMatch(Predicate<T>) : vrai si au moins un élément satisfait le prédicat.
  • noneMatch(Predicate<T>) : vrai si aucun élément ne satisfait le prédicat.

Numériques

  • long count() : retourne le nombre d’éléments.
  • sum() : retourne la somme des éléments (pour int, double ou long).

Simples

  • Optional<T> findAny() : retourne un élément quelconque.
  • Optional<T> findFirst() : retourne le premier élément.
  • Optional<T> max(Comparator<T>) et Optional<T> min(Comparator<T>) : retournent respectivement le maximum et le minimum selon le comparateur.

Complexes

  • Collect : transforme les éléments du stream en une autre structure (List, Set, Map).
  • Reduce : combine tous les éléments du stream en un seul résultat.

Exemple d’utilisation d’un Stream

Supposons une liste de nombres entiers, et on souhaite filtrer les nombres pairs, les doubler, puis afficher chacun :

List<Integer> nombres = Arrays.asList(1, 2, 3, 4, 5);
nombres.stream()
       .filter(n -> n % 2 == 0)
       .map(n -> n * 2)
       .forEach(System.out::println);

Ce code crée un stream à partir de la liste, filtre les nombres pairs, applique une transformation (double la valeur), puis affiche chaque élément résultant. L’opération terminale forEach déclenche l’exécution du pipeline.

Ordre et exécution des opérations

Les opérations intermédiaires sont paresseuses : elles ne s’exécutent que lorsqu’une opération terminale est présente. Par exemple, si on écrit :

nombres.stream()
       .filter(n -> {
           System.out.println("Filtrage: " + n);
           return n % 2 == 0;
       });

Rien ne sera affiché car aucune opération terminale ne déclenche l’exécution.

Exemple avec opération terminale

nombres.stream()
       .filter(n -> {
           System.out.println("Filtrage: " + n);
           return n % 2 == 0;
       })
       .forEach(System.out::println);

La console affichera les messages de filtrage et les nombres pairs, car forEach déclenche l’exécution.

Importance de l’ordre des opérations

Considérons une chaîne avec map, filter et forEach. Si on applique d’abord map puis filter, la fonction map sera appelée sur tous les éléments avant le filtrage :

nombres.stream()
       .map(n -> {
           System.out.println("Map: " + n);
           return n * 2;
       })
       .filter(n -> {
           System.out.println("Filtre: " + n);
           return n % 3 == 0;
       })
       .forEach(System.out::println);

Les méthodes map et filter sont appelées pour chaque élément, ce qui peut être inefficace.

Optimisation par changement d’ordre

En inversant filter et map, on réduit le nombre d’appels à map :

nombres.stream()
       .filter(n -> {
           System.out.println("Filtre: " + n);
           return n % 3 == 0;
       })
       .map(n -> {
           System.out.println("Map: " + n);
           return n * 2;
       })
       .forEach(System.out::println);

Le filtrage réduit le nombre d’éléments avant la transformation, rendant le pipeline plus rapide et efficace.

Glossaire des termes clés

  • Stream<T> : séquence d’éléments de type T supportant des opérations fonctionnelles.
  • Opération intermédiaire : opération sur un stream qui retourne un nouveau stream, évaluée paresseusement.
  • Opération terminale : opération qui déclenche l’exécution du pipeline et produit un résultat.
  • Pipeline : chaîne d’opérations intermédiaires suivie d’une opération terminale sur un stream.
  • Lazy evaluation (évaluation paresseuse) : les opérations intermédiaires ne s’exécutent que si une opération terminale est présente.
  • Predicate<T> : interface fonctionnelle représentant une fonction booléenne sur T.
  • Function<T, R> : interface fonctionnelle représentant une fonction de T vers R.
  • Optional<T> : conteneur pouvant contenir ou non une valeur de type T.
  • Collect : opération terminale transformant un stream en une autre structure de données.
  • Reduce : opération terminale combinant tous les éléments d’un stream en un seul résultat.

Points clés à retenir

  • Un stream est un flux de données fonctionnel, non modifiable et consommable une seule fois.
  • Les opérations intermédiaires sont paresseuses et ne s’exécutent qu’avec une opération terminale.
  • Les opérations intermédiaires retournent toujours un nouveau stream, permettant l’enchaînement.
  • Les opérations terminales déclenchent l’exécution du pipeline et ferment le stream.
  • L’ordre des opérations dans le pipeline impacte fortement la performance.
  • Les streams facilitent la programmation fonctionnelle en Java, rendant le code plus lisible et expressif.

Partager

Commentaires

Aucun commentaire pour le moment. Posez la première question.

Les commentaires sont relus avant publication. Votre e-mail n'est jamais affiché.

← Toutes les révisions