Solution de contournement pour les exceptions vérifiées par Java

49

J'apprécie beaucoup les nouvelles fonctionnalités de Java 8 concernant les interfaces de méthode lambdas et par défaut. Pourtant, je me lasse toujours des exceptions vérifiées. Par exemple, si je veux juste lister tous les champs visibles d'un objet, j'aimerais simplement écrire ceci:

    Arrays.asList(p.getClass().getFields()).forEach(
        f -> System.out.println(f.get(p))
    );

Cependant, comme la getméthode peut générer une exception vérifiée, ce qui ne correspond pas au Consumercontrat d'interface, je dois alors intercepter cette exception et écrire le code suivant:

    Arrays.asList(p.getClass().getFields()).forEach(
            f -> {
                try {
                    System.out.println(f.get(p));
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
    );

Cependant, dans la plupart des cas, je souhaite simplement que l'exception soit émise en tant que RuntimeExceptionet laisse le programme gérer, ou non, l'exception sans erreurs de compilation.

Je voudrais donc connaître votre opinion sur la solution de contournement controversée que je propose pour la gêne des exceptions vérifiées. À cette fin, j'ai créé une interface auxiliaire ConsumerCheckException<T>et une fonction utilitaire rethrow( mise à jour conformément à la suggestion du commentaire de Doval ) comme suit:

  @FunctionalInterface
  public interface ConsumerCheckException<T>{
      void accept(T elem) throws Exception;
  }

  public class Wrappers {
      public static <T> Consumer<T> rethrow(ConsumerCheckException<T> c) {
        return elem -> {
          try {
            c.accept(elem);
          } catch (Exception ex) {
            /**
             * within sneakyThrow() we cast to the parameterized type T. 
             * In this case that type is RuntimeException. 
             * At runtime, however, the generic types have been erased, so 
             * that there is no T type anymore to cast to, so the cast
             * disappears.
             */
            Wrappers.<RuntimeException>sneakyThrow(ex);
          }
        };
      }

      /**
       * Reinier Zwitserloot who, as far as I know, had the first mention of this
       * technique in 2009 on the java posse mailing list.
       * http://www.mail-archive.com/[email protected]/msg05984.html
       */
      public static <T extends Throwable> T sneakyThrow(Throwable t) {
          throw (T) t;
      }
  }

Et maintenant je peux juste écrire:

    Arrays.asList(p.getClass().getFields()).forEach(
            rethrow(f -> System.out.println(f.get(p)))
    );

Je ne suis pas sûr que ce soit l'idiome le plus approprié pour contourner les exceptions vérifiées, mais comme je l'ai expliqué, j'aimerais disposer d'un moyen plus pratique de réaliser mon premier exemple sans traiter des exceptions vérifiées. C'est le moyen le plus simple que j'ai trouvé. pour le faire.

Fernando Miguel Carvalho
la source
2
En plus du lien de Robert, jetez également un coup d'oeil aux exceptions vérifiées de manière sournoise . Si vous le souhaitez, vous pouvez utiliser sneakyThrowdans rethrowpour lancer l'original, exception vérifiée au lieu de l'envelopper dans un fichier RuntimeException. Vous pouvez également utiliser l' @SneakyThrowsannotation de Project Lombok qui fait la même chose.
Doval
1
Notez également que le Consumers in forEachpeut être exécuté de manière parallèle lors de l’utilisation de Streams parallèles . Un jetable généré chez le consommateur se propage ensuite vers le fil appelant, ce qui 1) n'arrête pas les autres consommateurs exécutant simultanément, ce qui peut ou non être approprié, et 2) si plus d'un consommateur jette quelque chose, uniquement l'un des objets jetables sera vu par le fil d'appel.
Joonas Pulakka
2
Les Lambda sont si laids.
Tulains Córdova

Réponses:

16

Avantages, inconvénients et limites de votre technique:

  • Si le code appelant doit gérer l'exception vérifiée, vous DEVEZ l'ajouter à la clause throws de la méthode qui contient le flux. Le compilateur ne vous obligera plus à l'ajouter, il est donc plus facile de l'oublier. Par exemple:

    public void test(Object p) throws IllegalAccessException {
        Arrays.asList(p.getClass().getFields()).forEach(rethrow(f -> System.out.println(f.get(p))));
    }
    
  • Si le code d'appel gère déjà l'exception vérifiée, le compilateur vous rappellera d'ajouter la clause throws à la déclaration de méthode qui contient le flux (sinon, il dira: une exception n'est jamais levée dans le corps de l'instruction try correspondante) .

  • Dans tous les cas, vous ne pourrez pas entourer le flux lui-même pour attraper l'exception vérifiée À L'INTÉRIEUR de la méthode qui contient le flux (si vous essayez, le compilateur dira: L'exception n'est jamais levée dans le corps de l'instruction try correspondante).

  • Si vous appelez une méthode qui ne peut littéralement jamais lever l'exception qu'elle déclare, vous ne devez pas inclure la clause throws. Par exemple: new String (byteArr, "UTF-8") lève une exception UnsupportedEncodingException, mais UTF-8 est garanti par la spécification Java pour qu'il soit toujours présent. Ici, la déclaration des lancers est une nuisance et toute solution pour la réduire au silence avec un passe-partout minimal est la bienvenue.

  • Si vous détestez les exceptions vérifiées et estimez qu'elles ne devraient jamais être ajoutées au langage Java pour commencer (un nombre croissant de personnes pensent de cette façon, et je ne suis pas l'un d'entre eux), alors n'ajoutez pas l'exception vérifiée aux lancers. clause de la méthode qui contient le flux. L'exception cochée se comportera alors comme une exception non vérifiée.

  • Si vous implémentez une interface stricte pour laquelle vous n'avez pas l'option d'ajouter une déclaration de projection et que le lancement d'une exception est tout à fait approprié, encapsuler une exception uniquement pour avoir le privilège de le lancer résulte en un stacktrace avec des exceptions fausses qui ne donne aucune information sur ce qui s'est réellement passé. Un bon exemple est Runnable.run (), qui ne lève aucune exception vérifiée. Dans ce cas, vous pouvez décider de ne pas ajouter l'exception vérifiée à la clause throws de la méthode contenant le flux.

  • Dans tous les cas, si vous décidez de NE PAS ajouter (ou ne pas oublier d’ajouter) l’exception vérifiée à la clause throws de la méthode qui contient le flux, tenez compte de ces 2 conséquences de la levée des exceptions CHECKED:

    1. Le code appelant ne pourra pas l'attraper par son nom (si vous essayez, le compilateur dira: une exception n'est jamais levée dans le corps de l'instruction try correspondante). Il fera des bulles et sera probablement attrapé dans la boucle principale du programme par un "catch Exception" ou un "catch Throwable", ce qui peut être ce que vous voulez de toute façon.

    2. Cela viole le principe de la moindre surprise: il ne suffira plus d'attraper RuntimeException pour pouvoir garantir la capture de toutes les exceptions possibles. Pour cette raison, j'estime que cela ne devrait pas être fait dans le code cadre, mais uniquement dans le code métier que vous contrôlez complètement.

Références:

REMARQUE: Si vous décidez d'utiliser cette technique, vous pouvez copier la LambdaExceptionUtilclasse d'assistance de StackOverflow: https://stackoverflow.com/questions/27644361/how-can-i-throw-checked-exceptions-from-inside-java-8. -streams . Il vous donne l'implémentation complète (Fonction, Consommateur, Fournisseur ...), avec des exemples.

MarcG
la source
6

Dans cet exemple, cela peut-il vraiment échouer? Ne crois pas, mais peut-être que ton cas est spécial. Si c'est vraiment "ne peut pas échouer", et que c'est juste un truc ennuyeux pour le compilateur, j'aime bien emballer l'exception et lancer un Erroravec un commentaire " ça ne peut pas arriver". Rend les choses très claires pour la maintenance. Sinon, ils se demanderont "comment cela peut-il arriver" et "qui diable gère cela?"

Ceci dans une pratique controversée donc YMMV. Je vais probablement avoir des votes négatifs.

utilisateur949300
la source
3
+1 parce que vous allez jeter une erreur. Combien de fois ai-je fini après des heures de débogage à un bloc catch ne contenant qu'un seul commentaire "ça ne peut pas arriver" ...
Axel
3
-1 parce que vous allez jeter une erreur. Les erreurs doivent indiquer que quelque chose ne va pas dans la machine virtuelle Java et que les niveaux supérieurs peuvent les gérer en conséquence. Si vous choisissez cette méthode, vous devriez lancer RuntimeException. Les assertions (indicateur Need -ea activé) ou la journalisation sont une autre solution de contournement possible.
Duros
3
@duros: Selon la raison pour laquelle l'exception « ne peut pas se produire », le fait qu'il est jeté peut indiquer que quelque chose est sérieusement mal. Supposons, par exemple, que l’on appelle cloneun type scellé Foodont on sait qu’il le supporte et qu’il jette CloneNotSupportedException. Comment cela pourrait-il se produire à moins que le code ne soit lié à un autre type inattendu Foo? Et si cela se produit, peut-on faire confiance à quelque chose?
Supercat
1
@supercat excellent exemple. D'autres lèvent des exceptions pour les encodages de chaînes ou les MessageDigests manquants. Si UTF-8 ou SHA manque, votre environnement d'exécution est probablement corrompu.
user949300
1
@duros C'est une idée fausse complète. An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.(cité dans le Javadoc de Error) C’est exactement ce genre de situation, donc loin d’être inadéquate, lancer un Errorici est l’option la plus appropriée.
biziclop
1

une autre version de ce code où exception vérifiée est juste retardée:

public class Cocoon {
         static <T extends Throwable> T forgetThrowsClause(Throwable t) throws T{
            throw (T) t;
        }

        public static <X, T extends Throwable> Consumer<X> consumer(PeskyConsumer<X,T> touchyConsumer) throws T {
            return new Consumer<X>() {
                @Override
                public void accept(X t) {
                    try {
                        touchyConsumer.accept(t);
                    } catch (Throwable exc) {
                        Cocoon.<RuntimeException>forgetThrowsClause(exc) ;
                    }

                }
            } ;
        }
// and so on for Function, and other codes from java.util.function
}

la magie est que si vous appelez:

 myArrayList.forEach(Cocoon.consumer(MyClass::methodThatThrowsException)) ;

alors votre code sera obligé d'attraper l'exception

ours de java
la source
Que se passe-t-il si cela est exécuté sur un flux parallèle où les éléments sont consommés dans différents threads?
Prunge
0

SneakyThrow est cool! Je ressens totalement ta douleur.

Si vous devez rester en Java ...

Paguro a des interfaces fonctionnelles qui encapsulent les exceptions vérifiées afin que vous n'ayez plus à y penser. Il inclut des collections immuables et des transformations fonctionnelles similaires aux transducteurs Clojure (ou Java 8 Streams) qui prennent les interfaces fonctionnelles et encapsulent les exceptions vérifiées.

Il y a aussi VAVr et les collections Eclipse à essayer.

Sinon, utilisez Kotlin

Kotlin est compatible bi -directionnel avec Java, ne contient pas d'exceptions vérifiées, pas de primitives (enfin, le programmeur n'a pas à y penser), un meilleur système de types, suppose l'immutabilité, etc. Même si je gère la bibliothèque Paguro ci-dessus, je m convertir tout le code Java que je peux en Kotlin. Tout ce que je faisais auparavant en Java, je préfère maintenant le faire à Kotlin.

GlenPeterson
la source
Quelqu'un a voté contre cela, ce qui est bien, mais je n'apprendrai rien à moins que vous me disiez pourquoi vous l'avez voté négativement.
GlenPeterson
1
+1 pour votre bibliothèque dans github, vous pouvez également consulter eclipse.org/collections ou project vavr qui fournissent des collections fonctionnelles et immuables. Quelles sont vos pensées sur ceux-ci?
Firephil
-1

Les exceptions cochées sont très utiles et leur traitement dépend du type de produit dont vous définissez le code:

  • une bibliothèque
  • une application de bureau
  • un serveur tournant dans une boite
  • un exercice académique

Habituellement, une bibliothèque ne doit pas gérer les exceptions vérifiées, mais déclarer qu'elle est levée par une API publique. Le cas rare où ils pourraient être intégrés à RuntimeExcepton est, par exemple, la création d'un document xml privé à l'intérieur du code de bibliothèque, son analyse, la création d'un fichier temporaire, puis sa lecture.

Pour les applications de bureau, vous devez commencer le développement du produit en créant votre propre infrastructure de traitement des exceptions. En utilisant votre propre framework, vous allez généralement encapsuler une exception vérifiée dans deux à quatre wrappers (vos sous-classes personnalisées de RuntimeExcepion) et les gérer avec un seul gestionnaire d'exception.

Pour les serveurs, généralement votre code s’exécutera dans une structure donnée. Si vous n'en utilisez aucun (serveur nu), créez votre propre cadre similaire (basé sur Runtimes) décrit ci-dessus.

Pour les exercices académiques, vous pouvez toujours les envelopper directement dans RuntimeExcepton. Quelqu'un peut aussi créer un petit cadre.

Razmik
la source
-1

Vous voudrez peut-être jeter un coup d'œil à ce projet ; Je l'ai créé spécifiquement à cause du problème des exceptions vérifiées et des interfaces fonctionnelles.

Avec cela, vous pouvez faire cela au lieu d'écrire votre code personnalisé:

// I don't know the type of f, so it's Foo...
final ThrowingConsumer<Foo> consumer = f -> System.out.println(f.get(p));

Puisque toutes les interfaces Throwing * étendent leurs homologues non Throwing, vous pouvez les utiliser directement dans le flux:

Arrays.asList(p.getClass().getFields()).forEach(consumer);

Ou vous pouvez simplement:

import static com.github.fge.lambdas.consumers.Consumers.wrap;

Arrays.asList(p.getClass().getFields())
    .forEach(wrap(f -> System.out.println(f.get(p)));

Et d'autres choses.

fge
la source
Encore mieuxfields.forEach((ThrowingConsumer<Field>) f -> out.println(f.get(o)))
user2418306
Est-ce que les électeurs descendants pourraient expliquer?
Fge