Comprendre et implémenter l'interface Collector en Java

L'interface Colletcor<T, A, R>

L'interface Collector est un composant fondamental de l'API Stream de Java. Elle définit la manière dont les éléments d'un flux sont agrégés dans un résultat final. Elle repose sur trois types génériques :

  • T : Le type des objets présents dans le flux à collecter.
  • A : Le type de l'accumulateur, c'est-à-dire l'objet intermédiaire qui stocke les résultats partiels.
  • R : Le type du résultat final produit par l'opération de collecte.

Voici la strcuture de l'interface :

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
    Set<Characteristics> characteristics();
}

Chaque méthode joue un rôle précis dans le cycle de vie de la réduction :

  • supplier() : Initialise un nouveau conteneur de résultats.
  • accumulator() : Définit comment intégrer un élément du flux dans le conteneur.
  • combiner() : Fusionne deux conteneurs (utilisé lors de l'exécution en parallèle).
  • finisher() : Effectue la transformation finale de l'accumulateur vers le type de résultat attendu.
  • characteristics() : Fournit des indices d'optimisation (ex: IDENTITY_FINISH, CONCURRENT).

Implémentation d'un collecteur personnalisé

Pour mieux comprendre ce mécanisme, nous allons créer une implémentation personnalisée qui simule le comportement de Collectors.toList(), tout en ajoutant des traces pour observer l'ordre d'exécution des méthodes.

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;

public class ListePersonnaliseeCollector<T> implements Collector<T, List<T>, List<T>> {

    private void tracer(String etape) {
        System.out.println("[" + Thread.currentThread().getName() + "] Étape : " + etape);
    }

    @Override
    public Supplier<List<T>> supplier() {
        tracer("Initialisation du conteneur (supplier)");
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        tracer("Accumulation (accumulator)");
        return List::add;
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        tracer("Fusion (combiner)");
        return (listeInitiale, listeAdditionnelle) -> {
            listeInitiale.addAll(listeAdditionnelle);
            return listeInitiale;
        };
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        tracer("Finalisation (finisher)");
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        tracer("Définition des caractéristiques");
        return Collections.unmodifiableSet(EnumSet.of(
            Collector.Characteristics.IDENTITY_FINISH,
            Collector.Characteristics.CONCURRENT
        ));
    }
}

Utilisation et test du collecteur

L'exemple suivant montre comment appliquer ce collecteur sur un flux de données, en mode séquentiel et en mode parallèle.

import java.util.Arrays;
import java.util.List;

public class DemoCollector {
    public static void main(String[] args) {
        ListePersonnaliseeCollector<String> monCollector = new ListePersonnaliseeCollector<>();
        String[] frameworkArray = {"Spring", "Hibernate", "Quarkus", "Micronaut", "Vert.x"};

        System.out.println("--- Test Flux Séquentiel ---");
        List<String> resultatSeq = Arrays.stream(frameworkArray)
                                         .filter(s -> s.length() > 6)
                                         .collect(monCollector);
        System.out.println("Résultat : " + resultatSeq);

        System.out.println("\n--- Test Flux Parallèle ---");
        List<String> resultatPar = Arrays.stream(frameworkArray)
                                         .parallel()
                                         .filter(s -> s.length() > 6)
                                         .collect(monCollector);
        System.out.println("Résultat : " + resultatPar);
    }
}

Lors de l'exécution, la console affichera les différentes étapes déclenchées par le moteur de flux de Java. En mode parallèle, vous remarquerez que l'étape combiner devient cruciale pour assembler les segments de listes traités par différents threads.

Étiquettes: Java Stream-API Collector functional-programming

Publié le 17 juin à 01h38