Java 24 – Un aperçu de l’API Gatherer

Depuis son introduction dans JDK 8, l’API Stream a changé profondément la manière dont nous traitons les collections et les séquences de données en Java. Grâce à son approche fonctionnelle et à son support des opérations en pipeline, elle permet d’écrire un code plus expressif et souvent plus efficace.

L’API Stream est très riche et offre de nombreuses possibilités de traitement des données en mémoire, selon le modèle map-filter-reduce. Elle s’appuie sur l’API Spliterator et la polyvalence de ces deux modèles offre de nombreuses possibilités. L’inconvénient est que l’API Spliterator n’est pas facile à utiliser et ne produit pas de code simple et lisible. De plus, si vous devez exploiter des flux parallèles, son utilisation peut s’avérer très complexe.

Avec l’API Gatherer, introduite officiellement dans JDK 24 après plusieurs phases de preview, Java propose un mécanisme pour définir des opérations intermédiaires personnalisées. Cette nouveauté ouvre la porte à une flexibilité et une expressivité inédites dans les pipelines de flux. Sa conception est similaire à celle de l’API Collector pour les opérations Stream terminales.

Comme décrit dans JEP 485, cette API permet d’enrichir les pipelines de flux en introduisant des opérations intermédiaires avancées auparavant non supportées.

Une solution aux limites des transformations de flux

L’API Stream propose des opérations essentielles telles que mapfilterreduce et sort. Toutefois, certaines transformations avancées restent difficiles à réaliser avec ces outils standards, par exemple :

  • Appliquer une opération distinct selon un attribut d’un objet (ex. : filtrer des chaînes de caractères par leur longueur unique).
  • Regrouper des éléments en fenêtres de taille fixe au fil du flux.
  • Détecter des motifs ou séquences dans un flux d’événements, nécessitant un état persistant.

Ces opérations ne peuvent pas être définies avec l’API Stream et nécessitent de recourir aux Spliterators. L’API Gatherer offre une solution à ces problèmes en permettant de modéliser des opérations intermédiaires sur mesure, de la même manière que l’API Collector permet de définir des opérations terminales personnalisées.

Elle propose un modèle plus simple que l’API Spliterator, avec une excellente prise en charge du parallélisme : même avec une implémentation séquentielle de votre Gatherer, vous pouvez bénéficier des performances offertes par les flux parallèles sur l’amont et l’aval. L’API Spliterator ne vous offre pas cet avantage.

Qu’est-ce qu’un Gatherer ?

Un Gatherer est un objet définissant une opération intermédiaire personnalisée sur un flux. Il reçoit des éléments d’un flux en amont (upstream), effectue une transformation, et transmet (ou non) les éléments à un flux en aval (downstream).

Un Gatherer peut prendre plusieurs formes :

  • 1-to-1 : chaque élément en entrée produit un élément en sortie (comme map).
  • 1-to-N : chaque élément produit zéro ou plusieurs éléments en sortie (comme flatMap).
  • N-to-1 : plusieurs éléments en entrée sont combinés en un seul (comme fold).
  • N-to-M : transformation complexe impliquant plusieurs entrées et sorties (ex. : opérations de fenêtrage).

Les Gatherers offrent plusieurs fonctionnalités avancées :

  • Le maintien d’un état mutable pour conserver une mémoire des opérations précédentes.
  • L’interruption du traitement pour gérer les flux infinis ou optimiser les traitements (short-circuit).
  • Le support de l’exécution parallèle via une fonction de combinaison d’état (combiner).

Comprendre l’interface Gatherer

Un Gatherer est défini via l’interface java.util.stream.Gatherer, qui repose sur quatre méthodes clés :

  • initializer() (optionnel) : fournit un état initial mutable, utilisé pour maintenir des informations à travers les éléments.
  • integrator() (obligatoire) : définit la transformation à appliquer à chaque élément sous la forme d’un Integrator. Il reçoit l’état courant, l’élément d’entrée et un Downstream pour pousser les éléments transformés.
  • combiner() (optionnel) : combine les états en cas de traitement parallèle.
  • finisher() (optionnel) : exécute une opération finale après traitement de tous les éléments.

Un Gatherer peut être instancié en implémentant directement cette interface ou via des méthodes de fabrique comme Gatherer.of() et Gatherer.ofSequential().

Exemple d’utilisation de Gatherer

L’API Gatherer existe pour simplifier des opérations complexes. Il est donc pratiquement impossible d’écrire des exemples simples qui rendent compte de l’intérêt de l’API. Voici toutefois deux exemples triviaux.

  • map

Cette opération effectue le même travail qu’une opération map et transforme toutes les chaînes de caractères d’un flux en majuscules.

Java

Ici, Gatherer.of() est utilisé avec une lambda pour transformer chaque élément en majuscule avant de le transmettre à l’étape suivante. Cette version est la plus simple disponible : elle ne nécessite pas de maintenir un état.

💡Il est important de noter que l’intégrateur doit retourner un boolean qui indique si le flux accepte d’avantage de donnée ou non. Dans cet exemple, nous nous contentons de reporter l’information donnée par l’opération push sur le flux aval, faisant ainsi remonter une éventuelle fin de traitement vers l’amont (short-circuit).
  • join

Le Gatherer suivant effectue un travail similaire à un Collectors.joining() via une opération séquentielle sur les éléments du flux.

Java

Ici, on maintient un état mutable State pour accumuler de manière efficace l’opération de join sur les éléments successifs. Notez que l’intégrateur ne publie pas dans son flux aval dans ce cas particulier et nous pouvons donc utiliser une optimisation via Integrator.ofGreedy() pour indiquer que l’on va consommer l’intégralité du flux. Seul l’opération finale va publier (si besoin).

💡On notera l’usage de ? dans la déclaration du type du Gatherer ici car ce dernier doit inclure le type de l’état mutable, or celui-ci n’est pas accessible en dehors de la fonction join.

Des Gatherers prêts à l’emploi

Le JDK 24 fournit plusieurs Gatherers prédéfinis via java.util.stream.Gatherers, notamment :

  • fold( initial, folder ) : agrégation incrémentale d’un état mutable.
  • scan( initial, scanner ) : accumulation incrémentale d’un état sur le flux.
  • windowFixed( windowSize ) : regroupement des éléments en fenêtres de taille fixe.
  • windowSliding( windowSize ) : fenêtres glissantes sur le flux.
  • mapConcurrent( maxConcurrency, mapper ) : application concurrente d’une transformation avec préservation de l’ordre.

A titre d’illustration, voici un exemple d’usage du fenêtrage de taille fixe :

Java

Pourquoi adopter l’API Gatherer ?

L’API Gatherer vient combler un manque important dans l’API Stream, offrant une flexibilité inédite aux développeurs travaillant avec les flux de données. Bien que son apprentissage puisse demander un certain effort initial, elle permet de :

  • Exprimer des transformations complexes directement dans un pipeline Stream.
  • Simplifier et rendre plus lisible le code de traitement de données.
  • Optimiser les performances grâce à la gestion avancée de l’état et du parallélisme.

Si vous avez déjà été limité par les opérations intermédiaires standard des flux Java, il est temps d’explorer gather() et de repousser les frontières des transformations de données dans vos applications Java 24 !

Références

  1. JEP 485: Stream Gatherers – https://openjdk.org/jeps/485
  2. Java API – Gatherer<T,A,R> – https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/stream/Gatherer.html
  3. Java API – Gatherer.Integrator<A,T,R> – https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/stream/Gatherer.Integrator.html
  4. Java API – Gatherer.Downstream<T> – https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/stream/Gatherer.Downstream.html
  5. Java API – Gatherers – https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/stream/Gatherers.html
  6. Oracle: Stream Gatherers – https://docs.oracle.com/en/java/javase/24/core/stream-gatherers.html
  7. The Gatherer API – https://dev.java/learn/api/streams/gatherers/
  8. To Gather or not to Gather? That is the question. – https://dev.to/onepoint/to-gather-or-not-to-gather-that-is-the-question-36oo
  9. Stream Gatherers: Intro to Intermediate Operations Modeler – https://dzone.com/articles/stream-gatherers-intermediate-operations-modeler