Exceptions dans les expressions Java 8 Lambda

Exceptions dans les expressions Java 8 Lambda

1. Vue d'ensemble

En Java 8, Lambda Expressions a commencé à faciliter la programmation fonctionnelle en fournissant un moyen concis d’exprimer un comportement. Cependant, lesFunctional Interfaces fournis par le JDK ne gèrent pas très bien les exceptions - et le code devient verbeux et encombrant lorsqu'il s'agit de les gérer.

Dans cet article, nous allons explorer quelques moyens de gérer les exceptions lors de l'écriture d'expressions lambda.

2. Gestion des exceptions non contrôlées

Commençons par comprendre le problème avec un exemple.

Nous avons unList<Integer> et nous voulons diviser une constante, disons 50 avec chaque élément de cette liste et imprimer les résultats:

List integers = Arrays.asList(3, 9, 7, 6, 10, 20);
integers.forEach(i -> System.out.println(50 / i));

Cette expression fonctionne mais il y a un problème. Si l'un des éléments de la liste est0, alors nous obtenons unArithmeticException: / by zero. Corrigeons ce problème en utilisant un bloctry-catch traditionnel tel que nous enregistrons une telle exception et continuons l'exécution pour les éléments suivants:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        System.out.println(50 / i);
    } catch (ArithmeticException e) {
        System.err.println(
          "Arithmetic Exception occured : " + e.getMessage());
    }
});

L’utilisation detry-catch résout le problème, mais la concision d’unLambda Expression est perdue et ce n’est plus une petite fonction comme elle est censée être.

Pour résoudre ce problème, nous pouvons écrirea lambda wrapper for the lambda function. Examinons le code pour voir comment cela fonctionne:

static Consumer lambdaWrapper(Consumer consumer) {
    return i -> {
        try {
            consumer.accept(i);
        } catch (ArithmeticException e) {
            System.err.println(
              "Arithmetic Exception occured : " + e.getMessage());
        }
    };
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(lambdaWrapper(i -> System.out.println(50 / i)));

Au début, nous avons écrit une méthode wrapper qui sera responsable de la gestion de l'exception, puis nous avons passé l'expression lambda en tant que paramètre à cette méthode.

La méthode wrapper fonctionne comme prévu, mais vous pouvez affirmer qu’elle supprime essentiellement le bloctry-catch de l’expression lambda et le déplace vers une autre méthode et qu’elle ne réduit pas le nombre réel de lignes de code en cours d’écriture.

Cela est vrai dans le cas où l'encapsuleur est spécifique à un cas d'utilisation particulier, mais nous pouvons utiliser des génériques pour améliorer cette méthode et l'utiliser pour une variété d'autres scénarios:

static  Consumer
  consumerWrapper(Consumer consumer, Class clazz) {

    return i -> {
        try {
            consumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = clazz.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw ex;
            }
        }
    };
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(
  consumerWrapper(
    i -> System.out.println(50 / i),
    ArithmeticException.class));

Comme nous pouvons le voir, cette itération de notre méthode wrapper prend deux arguments, l'expression lambda et le type deException à intercepter. Ce wrapper lambda est capable de gérer tous les types de données, pas seulementIntegers, et intercepter n'importe quel type spécifique d'exception et pas la superclasseException.

Notez également que nous avons changé le nom de la méthode delambdaWrapper àconsumerWrapper. C'est parce que cette méthode ne gère que les expressions lambda pourFunctional Interface de typeConsumer. Nous pouvons écrire des méthodes d'encapsulation similaires pour d'autres interfaces fonctionnelles telles queFunction,BiFunction,BiConsumer et ainsi de suite.

3. Gestion des exceptions vérifiées

Prenons l'exemple de la section précédente, mais au lieu de diviser et d'imprimer les nombres entiers sur la console, nous voulons les écrire dans un fichier. Cette opération d'écriture dans un fichier lèveIOException.

static void writeToFile(Integer integer) throws IOException {
    // logic to write to file which throws IOException
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));

Lors de la compilation, nous obtenons l'erreur suivante.

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

PuisqueIOException est une exception vérifiée, elle doit être gérée. Maintenant, il y a deux options, nous pouvons vouloir lever l'exception et la gérer ailleurs ou la manipuler à l'intérieur de la méthode qui contient l'expression lambda. Examinons chacun d’eux un par un.

3.1. Lancer une exception vérifiée à partir d'expressions Lambda

Lançons l'exception de la méthode dans laquelle l'expression lambda est écrite, dans ce cas, lesmain:

public static void main(String[] args) throws IOException {
    List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
    integers.forEach(i -> writeToFile(i));
}

Pourtant, lors de la compilation, nous obtenons la même erreur deIOException non gérés. En effet, les expressions lambda sont similaires aux classes internes anonymes. Dans ce cas, l'expression lambda est une implémentation de la méthodeaccept(T t) à partir de l'interfaceConsumer<T>.

Lancer l'exception à partir demain ne fait rien et comme la méthode de l'interface parent ne lève aucune exception, elle ne peut pas dans son implémentation:

Consumer consumer = new Consumer() {

    @Override
    public void accept(Integer integer) throws Exception {
        writeToFile(integer);
    }
};

Le code ci-dessus ne se compile pas car l'implémentation de la méthodeaccept ne peut pas lever d'exception.

Le moyen le plus simple serait d'utiliser untry-catch et d'encapsuler l'exception vérifiée dans une exception non vérifiée et de la renvoyer:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        writeToFile(i);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

Cette approche permet au code de compiler et d’exécuter, mais présente le même problème que l’exemple dans le cas des exceptions non vérifiées de la section précédente.

Puisque nous voulons simplement lever l'exception, nous devons écrire notre propre interface fonctionnelle client qui peut générer une exception puis une méthode wrapper qui l'utilise. Appelons-leThrowingConsumer:

@FunctionalInterface
public interface ThrowingConsumer {
    void accept(T t) throws E;
}
static  Consumer throwingConsumerWrapper(
  ThrowingConsumer throwingConsumer) {

    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    };
}

Nous pouvons maintenant écrire notre expression lambda qui peut générer des exceptions sans perdre de sa concision.

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));

3.2. Gestion d'une exception vérifiée dans l'expression Lambda

Dans cette dernière section. nous allons modifier le wrapper pour gérer les exceptions vérifiées. Puisque notre interfaceThrowingConsumer utilise des génériques, nous pouvons gérer n'importe quelle exception spécifique.

static  Consumer handlingConsumerWrapper(
  ThrowingConsumer throwingConsumer, Class exceptionClass) {

    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = exceptionClass.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw new RuntimeException(ex);
            }
        }
    };
}

Nous pouvons utiliser ce wrapper dans notre exemple pour gérer uniquement lesIOException et lever toute autre exception vérifiée en les enveloppant dans une exception non vérifiée:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(handlingConsumerWrapper(
  i -> writeToFile(i), IOException.class));

Similaire au cas des exceptions non vérifiées, lancer des frères pour d'autres interfaces fonctionnelles commeThowingFunction,ThrowingBiFunction,ThrowingBiConsumer etc. peuvent être écrits avec leurs méthodes de wrapper correspondantes.

4. Conclusion

Dans cet article, nous avons expliqué comment gérer une exception spécifique dans les expressions lambda sans perdre la concision en utilisant des méthodes d'encapsulation. Nous avons également appris à rédiger des alternatives de projection pour les interfaces fonctionnelles présentes dans JDK afin de générer une exception vérifiée en les enveloppant dans une exception non contrôlée ou de les gérer.

Une autre façon serait deexplore the sneaky-throws hack.

Le code source complet de l'interface fonctionnelle et des méthodes d'encapsulation peut être téléchargé à partir dehere et les classes de test à partir dehere, over on Github.

Si vous recherchez des solutions de travail prêtes à l'emploi, le projetThrowingFunction vaut le détour.