Aller au contenu
KésacoBackJavadeveloppeurDéfinitionAPI

API Stream: Définition

Introduits avec Java 8, les Streams Java révolutionnent le traitement des données en offrant une approche fonctionnelle élégante. Envie d'en savoir plus ? Je vous explique tout dans cet article !

Qu'est-ce que l'API Stream ?

Introduits avec Java 8, les Streams Java révolutionnent le traitement des données en offrant une approche fonctionnelle élégante. Cette API permet de manipuler les collections de manière déclarative grâce à des opérations en chaîne, simplifiant ainsi le code tout en améliorant sa lisibilité.

Structure des opérations

L'API Stream dans Java fonctionne selon un modèle en trois phases distinctes :

✏️ Création du Stream

Plusieurs façons de créer un Stream :

// À partir d'une Collection
List<String> liste = Arrays.asList("a", "b", "c");
Stream<String> stream = liste.stream();

// Création directe
Stream<String> stream = Stream.of("a", "b", "c");

// À partir d'un tableau
String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);

Création d'un Stream

🔎 Opérations intermédiaires

Ces opérations transforment le Stream et retournent un nouveau Stream :

// Exemples d'opérations intermédiaires
.filter(Predicate<T>) // Filtre les éléments
.map(Function<T,R>)   // Transforme les éléments
.sorted()             // Trie les éléments
.distinct()           // Élimine les doublons
.limit(n)             // Limite le nombre d'éléments

Méthodes intermédiaires

⌛ Opération terminale

Une opération qui produit un résultat ou un effet de bord :

// Exemples d'opérations terminales
.collect()            // Collecte les résultats
.forEach()            // Parcourt les éléments
.reduce()             // Réduit à une seule valeur
.count()              // Compte les éléments
.anyMatch()           // Vérifie une condition

Méthodes terminales

Caractéristiques importantes

L'API Stream possède des caractéristiques innovantes qui ont contribuées à son succès par rapport aux versions Java précédentes.

🦥 Évaluation paresseuse

Stream<String> stream = list.stream()
    .filter(s -> {
        System.out.println("filtrage : " + s);
        return s.startsWith("a");
    })
    .map(s -> {
        System.out.println("mapping : " + s);
        return s.toUpperCase();
    });

Lazy loading

Si l'on évalue le code précédent, rien ne s'affichera ! En effet, avec l'API Stream, aucune opération n'est exécutée jusqu'à l'appel d'une opération terminale.

🕵️ Pipeline de traitement

long count = personnes.stream()
    .filter(p -> p.getAge() > 18) // Filtre l'élément reçu
    .map(Personne::getNom)        // Transforme l'élément éligible
    .distinct()                   // Supprime les doublons (statefull)
    .count();                     // Compte le nombre d'éléments

Pipeline de traitement

Le fait que chaque opération intermédiaire retourne un Stream permet de chainer les appels et ainsi de créer des pipelines de traitement avec une lisibilité de code accrue.

🚀 Traitement parallèle

// Conversion d'un stream séquentiel en parallèle
long count = list.stream()
    .parallel()
    .filter(predicate)
    .count();

Stream Parallel

⚠️ Attention aux opérations bloquantes dans les Streams parallèles

Lors de l'utilisation de parallelStream() ou stream().parallel(), il est crucial d'éviter les opérations bloquantes qui pourraient dégrader les performances :

// À NE PAS FAIRE ❌
list.parallelStream()
    .map(item -> appelBloquant(item))
    .toList();

// PRÉFÉRER ✅
List<CompletableFuture<String>> asyncResults = list.stream()
    .map(item -> CompletableFuture.supplyAsync(
                ()-> appelBloquant(item),
                Executors.newVirtualThreadPerTaskExecutor())
        )
    .toList();

// Traiter la liste des tâches asynchrone 
// (allOf() / anyOf() / join() etc.)

// ⚠️ Exemple minimaliste qui ne prend pas en compte les échecs
List<String> results = asyncResults.stream()
  .map(CompletableFuture::join)
  .toList();

Opérations bloquante dans un Stream

Les opérations bloquantes comme les appels synchrones à des APIs, les opérations I/O peuvent :

  • Monopoliser les threads du Common ForkJoinPool
  • Réduire significativement les performances
  • Créer des deadlocks potentiels

Pour les opérations asynchrones, privilégiez l'utilisation de CompletableFuture, de frameworks réactifs ou l'utilisation des Virtual Thread.

Les streams parallèles, et plus largement l'utilisation des threads du Common ForkJoinPool sont réservés à des tâches de calcul.

📚 Source de données et Spliterator

Les Streams s'appuient sur un mécanisme de "push" où la source de données pousse les éléments vers le Stream via un Spliterator. Ce dernier est responsable de la traversée et du partitionnement des éléments de la source.

Connexion avec une base de données

// Dans le repository
public interface UserRepository extends JpaRepository<User, Long> {
    // Spring Data JPA fournit directement les méthodes stream()
    @Query("SELECT u FROM User u")
    Stream<User> streamAllUsers();
}

// Dans le service
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    @Transactional(readOnly = true)
    public void processUsers() {
        try (var userStream = userRepository.streamAllUsers()) {
            userStream
                .filter(user -> user.getAge() > 18)
                .map(User::getName)
                .forEach(System.out::println);
        } // Le Stream est automatiquement fermé
    }
}

Streamer les résultats d'une requête en base de données

Le try-with-resources est important pour s'assurer que la session Hibernate est correctement fermée après l'utilisation du Stream.

L'avantage par rapport à un findAll qui renverrai une liste est qu'ici, nous n'avons qu'un seul objet en mémoire à l'instant T.

🗝️ Points clés à retenir

  1. Usage Unique : Un Stream ne peut être utilisé qu'une seule fois. Après une opération terminale, le Stream est fermé.
  2. Immutabilité : Les opérations de Stream ne modifient pas la source de données originale.
  3. Chaînage : Les opérations peuvent être chaînées pour créer des pipelines de traitement complexes.
  4. Short-Circuiting : Certaines opérations (comme findFirst(), limit()) peuvent arrêter le traitement avant de parcourir tous les éléments.

Exemple complet

List<String> resultat = personnes.stream()
    .filter(p -> p.getAge() > 18)  // Filtre les majeurs
    .map(Personne::getNom)         // Extrait les noms
    .sorted()                      // Trie par ordre alphabétique
    .distinct()                    // Élimine les doublons
    .toList();                     // Collecte dans une liste imutable

Exemple API Stream

Cette structure permet une manipulation efficace et expressive des données, particulièrement adaptée aux traitements complexes sur des collections.

Conclusion

Les Streams Java, introduits avec Java 8, ont transformé la manière dont les développeurs manipulent les collections de données. Grâce à une approche fonctionnelle, des opérations en chaîne et un traitement optimisé, cette API permet d’écrire un code plus lisible, concis et performant.

L’évaluation paresseuse et le support du traitement parallèle offrent des gains significatifs en efficacité, à condition de bien maîtriser leur utilisation pour éviter les pièges des opérations bloquantes.

Que ce soit pour le filtrage, la transformation ou la réduction des données, les Streams constituent un outil puissant et incontournable pour tout développeur Java moderne. 🚀

👉 Et vous, utilisez-vous déjà les Streams dans vos projets ? 😊

Si vous souhaitez en savoir plus sur l'histoire de Java, découvrez cet article 👇

Il était une fois... Java
Java, un langage de programmation qui approche la trentaine. Vieux langage poussiéreux ou rock star qui en a encore sous le capot ?

Dernier