Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Apprendre Gestion des Exceptions dans l'API Stream | Applications Pratiques de l'API Stream
API Stream

bookGestion des Exceptions dans l'API Stream

La gestion des exceptions dans l'API Stream nécessite une approche particulière. Contrairement aux boucles traditionnelles, où un bloc try-catch peut être placé à l'intérieur du corps de la boucle, les streams fonctionnent de manière déclarative, ce qui rend la gestion des exceptions plus complexe en leur sein.

Si une exception n'est pas gérée, elle interrompt l'ensemble du traitement du flux. Dans cette section, vous découvrirez la méthode appropriée pour intercepter et gérer les exceptions dans l'API Stream.

Problématique de la gestion des exceptions

Supposons que notre boutique en ligne possède une méthode getTotal() susceptible de lever une exception si les données de commande sont corrompues ou manquantes. Par exemple, une commande peut être chargée depuis une base de données où le montant total est stocké en tant que null.

class Order {
    private final double total;

    public Order(double total) {
        this.total = total;
    }

    public double getTotal() throws Exception {
        if (total < 0) {
            throw new IllegalArgumentException("Invalid order total");
        }
        return total;
    }
}

Désormais, si une commande possède un total inférieur à 0, l'ensemble du processus Stream API s'arrêtera avec une exception.

 List<User> premiumUsers = users.stream()
                .filter(User::isActive)
                .filter(user -> user.getOrders().stream()
                        .filter(order -> order.getTotal() >= 10000)
                        .count() >= 3)
                .toList();

Cependant, il existe un problème : ce code ne s'exécutera même pas car vous ne gérez pas les exceptions susceptibles de se produire dans la méthode getTotal(). Examinons donc comment gérer les exceptions dans la Stream API.

Gestion des exceptions dans Stream API

Étant donné que try-catch ne peut pas être utilisé directement à l'intérieur des lambdas, plusieurs stratégies existent pour la gestion des exceptions dans la Stream API.

Une méthode consiste à intercepter l'exception directement dans map() et à la remplacer par un résultat traité :

List<User> premiumUsers = users.stream()
        .filter(User::isActive)
        .filter(user -> user.getOrders().stream()
                .mapToDouble(order -> {
                    try {
                        return order.getTotal();
                    } catch (IllegalArgumentException e) {
                        throw new RuntimeException("Error in user's order: " + user, e);
                    }
                })
                .filter(total -> total >= 10000)
                .count() >= 3)
        .toList();

À l'intérieur de mapToDouble(), l'exception est interceptée et une RuntimeException est levée, en précisant quel utilisateur a causé le problème. Cette méthode est utile lorsqu'il est nécessaire d'arrêter immédiatement l'exécution et d'identifier rapidement le problème.

Ignorer les éléments avec des erreurs

Parfois, il n'est pas souhaitable d'interrompre l'ensemble du processus lorsqu'une erreur survient—il suffit simplement d'ignorer les éléments problématiques. Pour cela, il est possible d'utiliser filter() avec une gestion d'exception :

List<User> premiumUsers = users.stream()
        .filter(User::isActive)
        .filter(user -> {
            try {
                return user.getOrders().stream()
                        .mapToDouble(Order::getTotal)
                        .filter(total -> total >= 10000)
                        .count() >= 3;
            } catch (Exception e) {
                return false; // Skip users with problematic orders
            }
        })
        .toList();

Si une erreur survient dans mapToDouble(Order::getTotal), le traitement du flux de commandes s'arrêterait normalement. Cependant, le bloc try-catch à l'intérieur de filter() empêche cela, garantissant que seul l'utilisateur problématique est exclu de la liste finale.

Gestion des exceptions avec un wrapper

Pour rendre notre code plus robuste, il est possible de créer une méthode wrapper permettant de gérer les exceptions à l'intérieur des lambdas tout en les capturant automatiquement.

Java n'autorise pas un Function<T, R> à lancer des exceptions vérifiées. Si une exception se produit dans apply(), il faut soit la gérer dans la méthode, soit l'encapsuler dans une RuntimeException, ce qui complexifie le code. Pour simplifier cela, définissons une interface fonctionnelle personnalisée :

@FunctionalInterface
interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;
}

Cette interface fonctionne de manière similaire à Function<T, R>, mais autorise apply() à lancer une exception.

Créons maintenant une classe ExceptionWrapper avec une méthode wrap() qui convertit un ThrowingFunction<T, R> en un Function<T, R> standard et accepte un second paramètre spécifiant la valeur de repli en cas d'exception :

class ExceptionWrapper {
    // Wrapper for `Function` to handle exceptions
    public static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> function, R defaultValue) {
        return t -> {
            try {
                return function.apply(t);
            } catch (Exception e) {
                System.out.println(e.getMessage());
                return defaultValue;
            }
        };
    }
}

La méthode wrap() prend une ThrowingFunction<T, R> et la convertit en une Function<T, R> standard tout en gérant les exceptions. Si une erreur se produit, elle enregistre le message et retourne la valeur par défaut spécifiée.

Utilisation dans Stream API

Supposons que vous disposiez d'une liste d'utilisateurs dans une boutique en ligne, et que vous deviez trouver les utilisateurs actifs ayant au moins trois commandes d'une valeur supérieure à 10 000. Cependant, si une commande présente un montant négatif, il ne faut pas interrompre le stream—il suffit de retourner 0 pour indiquer que le prix était non valide.

Main.java

Main.java

copy
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
package com.example; import java.util.List; import java.util.function.Function; public class Main { public static void main(String[] args) { List<User> users = List.of( new User("Alice", true, List.of(new Order(12000.00), new Order(15000.00), new Order(11000.00))), new User("Bob", true, List.of(new Order(8000.00), new Order(9000.00), new Order(12000.00))), new User("Charlie", false, List.of(new Order(15000.00), new Order(16000.00), new Order(17000.00))), new User("David", true, List.of(new Order(5000.00), new Order(20000.00), new Order(30000.00))), new User("Eve", true, List.of(new Order(null), new Order(10000.00), new Order(10000.00), new Order(12000.00))), new User("Frank", true, List.of(new Order(-5000.00), new Order(10000.00))) // Error: Negative order amount ); List<User> premiumUsers = users.stream() .filter(User::isActive) .filter(user -> user.getOrders().stream() .map(ExceptionWrapper.wrap(Order::getTotal, 0.0)) // Using the wrapper function .filter(total -> total >= 10000) .count() >= 3) .toList(); System.out.println("Premium users: " + premiumUsers); } } // The `Order` class represents a customer's order class Order { private final Double total; public Order(Double total) { this.total = total; } public double getTotal() throws Exception { if (total == null || total < 0) { throw new Exception("Error: Order amount cannot be negative or equal to null!"); } return total; } } // The `User` class represents an online store user class User { private final String name; private final boolean active; private final List<Order> orders; public User(String name, boolean active, List<Order> orders) { this.name = name; this.active = active; this.orders = orders; } public boolean isActive() { return active; } public List<Order> getOrders() { return orders; } public String getName() { return name; } @Override public String toString() { return "User{name='" + name + "'}"; } } // Functional interface for handling exceptions in `Function` @FunctionalInterface interface ThrowingFunction<T, R> { R apply(T t) throws Exception; } // A helper class with wrapper methods for exception handling class ExceptionWrapper { // A wrapper for `Function` that catches exceptions public static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> function, R defaultValue) { return t -> { try { return function.apply(t); } catch (Exception e) { System.out.println(e.getMessage()); return defaultValue; } }; } }

Désormais, si une commande contient un montant négatif, le programme ne s'arrête pas mais enregistre simplement une erreur et le remplace par 0,0. Cela rend le traitement des données plus robuste et pratique pour une utilisation réelle.

Tout était clair ?

Comment pouvons-nous l'améliorer ?

Merci pour vos commentaires !

Section 4. Chapitre 3

Demandez à l'IA

expand

Demandez à l'IA

ChatGPT

Posez n'importe quelle question ou essayez l'une des questions suggérées pour commencer notre discussion

Awesome!

Completion rate improved to 2.33

bookGestion des Exceptions dans l'API Stream

Glissez pour afficher le menu

La gestion des exceptions dans l'API Stream nécessite une approche particulière. Contrairement aux boucles traditionnelles, où un bloc try-catch peut être placé à l'intérieur du corps de la boucle, les streams fonctionnent de manière déclarative, ce qui rend la gestion des exceptions plus complexe en leur sein.

Si une exception n'est pas gérée, elle interrompt l'ensemble du traitement du flux. Dans cette section, vous découvrirez la méthode appropriée pour intercepter et gérer les exceptions dans l'API Stream.

Problématique de la gestion des exceptions

Supposons que notre boutique en ligne possède une méthode getTotal() susceptible de lever une exception si les données de commande sont corrompues ou manquantes. Par exemple, une commande peut être chargée depuis une base de données où le montant total est stocké en tant que null.

class Order {
    private final double total;

    public Order(double total) {
        this.total = total;
    }

    public double getTotal() throws Exception {
        if (total < 0) {
            throw new IllegalArgumentException("Invalid order total");
        }
        return total;
    }
}

Désormais, si une commande possède un total inférieur à 0, l'ensemble du processus Stream API s'arrêtera avec une exception.

 List<User> premiumUsers = users.stream()
                .filter(User::isActive)
                .filter(user -> user.getOrders().stream()
                        .filter(order -> order.getTotal() >= 10000)
                        .count() >= 3)
                .toList();

Cependant, il existe un problème : ce code ne s'exécutera même pas car vous ne gérez pas les exceptions susceptibles de se produire dans la méthode getTotal(). Examinons donc comment gérer les exceptions dans la Stream API.

Gestion des exceptions dans Stream API

Étant donné que try-catch ne peut pas être utilisé directement à l'intérieur des lambdas, plusieurs stratégies existent pour la gestion des exceptions dans la Stream API.

Une méthode consiste à intercepter l'exception directement dans map() et à la remplacer par un résultat traité :

List<User> premiumUsers = users.stream()
        .filter(User::isActive)
        .filter(user -> user.getOrders().stream()
                .mapToDouble(order -> {
                    try {
                        return order.getTotal();
                    } catch (IllegalArgumentException e) {
                        throw new RuntimeException("Error in user's order: " + user, e);
                    }
                })
                .filter(total -> total >= 10000)
                .count() >= 3)
        .toList();

À l'intérieur de mapToDouble(), l'exception est interceptée et une RuntimeException est levée, en précisant quel utilisateur a causé le problème. Cette méthode est utile lorsqu'il est nécessaire d'arrêter immédiatement l'exécution et d'identifier rapidement le problème.

Ignorer les éléments avec des erreurs

Parfois, il n'est pas souhaitable d'interrompre l'ensemble du processus lorsqu'une erreur survient—il suffit simplement d'ignorer les éléments problématiques. Pour cela, il est possible d'utiliser filter() avec une gestion d'exception :

List<User> premiumUsers = users.stream()
        .filter(User::isActive)
        .filter(user -> {
            try {
                return user.getOrders().stream()
                        .mapToDouble(Order::getTotal)
                        .filter(total -> total >= 10000)
                        .count() >= 3;
            } catch (Exception e) {
                return false; // Skip users with problematic orders
            }
        })
        .toList();

Si une erreur survient dans mapToDouble(Order::getTotal), le traitement du flux de commandes s'arrêterait normalement. Cependant, le bloc try-catch à l'intérieur de filter() empêche cela, garantissant que seul l'utilisateur problématique est exclu de la liste finale.

Gestion des exceptions avec un wrapper

Pour rendre notre code plus robuste, il est possible de créer une méthode wrapper permettant de gérer les exceptions à l'intérieur des lambdas tout en les capturant automatiquement.

Java n'autorise pas un Function<T, R> à lancer des exceptions vérifiées. Si une exception se produit dans apply(), il faut soit la gérer dans la méthode, soit l'encapsuler dans une RuntimeException, ce qui complexifie le code. Pour simplifier cela, définissons une interface fonctionnelle personnalisée :

@FunctionalInterface
interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;
}

Cette interface fonctionne de manière similaire à Function<T, R>, mais autorise apply() à lancer une exception.

Créons maintenant une classe ExceptionWrapper avec une méthode wrap() qui convertit un ThrowingFunction<T, R> en un Function<T, R> standard et accepte un second paramètre spécifiant la valeur de repli en cas d'exception :

class ExceptionWrapper {
    // Wrapper for `Function` to handle exceptions
    public static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> function, R defaultValue) {
        return t -> {
            try {
                return function.apply(t);
            } catch (Exception e) {
                System.out.println(e.getMessage());
                return defaultValue;
            }
        };
    }
}

La méthode wrap() prend une ThrowingFunction<T, R> et la convertit en une Function<T, R> standard tout en gérant les exceptions. Si une erreur se produit, elle enregistre le message et retourne la valeur par défaut spécifiée.

Utilisation dans Stream API

Supposons que vous disposiez d'une liste d'utilisateurs dans une boutique en ligne, et que vous deviez trouver les utilisateurs actifs ayant au moins trois commandes d'une valeur supérieure à 10 000. Cependant, si une commande présente un montant négatif, il ne faut pas interrompre le stream—il suffit de retourner 0 pour indiquer que le prix était non valide.

Main.java

Main.java

copy
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
package com.example; import java.util.List; import java.util.function.Function; public class Main { public static void main(String[] args) { List<User> users = List.of( new User("Alice", true, List.of(new Order(12000.00), new Order(15000.00), new Order(11000.00))), new User("Bob", true, List.of(new Order(8000.00), new Order(9000.00), new Order(12000.00))), new User("Charlie", false, List.of(new Order(15000.00), new Order(16000.00), new Order(17000.00))), new User("David", true, List.of(new Order(5000.00), new Order(20000.00), new Order(30000.00))), new User("Eve", true, List.of(new Order(null), new Order(10000.00), new Order(10000.00), new Order(12000.00))), new User("Frank", true, List.of(new Order(-5000.00), new Order(10000.00))) // Error: Negative order amount ); List<User> premiumUsers = users.stream() .filter(User::isActive) .filter(user -> user.getOrders().stream() .map(ExceptionWrapper.wrap(Order::getTotal, 0.0)) // Using the wrapper function .filter(total -> total >= 10000) .count() >= 3) .toList(); System.out.println("Premium users: " + premiumUsers); } } // The `Order` class represents a customer's order class Order { private final Double total; public Order(Double total) { this.total = total; } public double getTotal() throws Exception { if (total == null || total < 0) { throw new Exception("Error: Order amount cannot be negative or equal to null!"); } return total; } } // The `User` class represents an online store user class User { private final String name; private final boolean active; private final List<Order> orders; public User(String name, boolean active, List<Order> orders) { this.name = name; this.active = active; this.orders = orders; } public boolean isActive() { return active; } public List<Order> getOrders() { return orders; } public String getName() { return name; } @Override public String toString() { return "User{name='" + name + "'}"; } } // Functional interface for handling exceptions in `Function` @FunctionalInterface interface ThrowingFunction<T, R> { R apply(T t) throws Exception; } // A helper class with wrapper methods for exception handling class ExceptionWrapper { // A wrapper for `Function` that catches exceptions public static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> function, R defaultValue) { return t -> { try { return function.apply(t); } catch (Exception e) { System.out.println(e.getMessage()); return defaultValue; } }; } }

Désormais, si une commande contient un montant négatif, le programme ne s'arrête pas mais enregistre simplement une erreur et le remplace par 0,0. Cela rend le traitement des données plus robuste et pratique pour une utilisation réelle.

Tout était clair ?

Comment pouvons-nous l'améliorer ?

Merci pour vos commentaires !

Section 4. Chapitre 3
some-alt