Les exceptions vérifiées sont-elles bonnes ou mauvaises?

Java prend en charge les exceptions vérifiées. Cette fonctionnalité de langage controversée est aimée par certains et détestée par d'autres, au point que la plupart des langages de programmation évitent les exceptions vérifiées et ne prennent en charge que leurs homologues non vérifiés.

Dans cet article, j'examine la controverse entourant les exceptions vérifiées. Je présente d'abord le concept d'exceptions et décris brièvement le support du langage Java pour les exceptions afin d'aider les débutants à mieux comprendre la controverse.

Quelles sont les exceptions?

Dans un monde idéal, les programmes informatiques ne rencontreraient jamais de problèmes: les fichiers existeraient alors qu'ils sont supposés exister, les connexions réseau ne se fermeraient jamais de manière inattendue, il n'y aurait jamais de tentative d'invoquer une méthode via la référence nulle, division entière-par -zéro tentatives ne se produiraient pas, etc. Cependant, notre monde est loin d'être idéal; ces exceptions et d'autres à l'exécution idéale du programme sont répandues.

Les premières tentatives de reconnaissance des exceptions incluaient le renvoi de valeurs spéciales indiquant un échec. Par exemple, la fopen()fonction du langage C retourne NULLquand elle ne peut pas ouvrir un fichier. De plus, la mysql_query()fonction PHP retourne FALSElorsqu'un échec SQL se produit. Vous devez chercher ailleurs le code d'échec réel. Bien que facile à mettre en œuvre, il existe deux problèmes avec cette approche de «retour de valeur spéciale» pour reconnaître les exceptions:

  • Les valeurs spéciales ne décrivent pas l'exception. Que signifie NULLou que FALSEsignifie vraiment? Tout dépend de l'auteur de la fonctionnalité qui renvoie la valeur spéciale. De plus, comment reliez-vous une valeur spéciale au contexte du programme lorsque l'exception s'est produite afin que vous puissiez présenter un message significatif à l'utilisateur?
  • Il est trop facile d'ignorer une valeur spéciale. Par exemple, int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);est problématique car ce fragment de code C s'exécute fgetc()pour lire un caractère du fichier même lors des fopen()retours NULL. Dans ce cas, fgetc()ne réussira pas: nous avons un bug qui peut être difficile à trouver.

Le premier problème est résolu en utilisant des classes pour décrire les exceptions. Le nom d'une classe identifie le type d'exception et ses champs agrègent le contexte de programme approprié pour déterminer (via des appels de méthode) ce qui n'a pas fonctionné. Le deuxième problème est résolu en demandant au compilateur de forcer le programmeur à répondre directement à une exception ou à indiquer que l'exception doit être traitée ailleurs.

Certaines exceptions sont très graves. Par exemple, un programme peut tenter d'allouer de la mémoire lorsqu'aucune mémoire libre n'est disponible. La récursivité illimitée qui épuise la pile est un autre exemple. Ces exceptions sont appelées erreurs .

Exceptions et Java

Java utilise des classes pour décrire les exceptions et les erreurs. Ces classes sont organisées en une hiérarchie enracinée dans la java.lang.Throwableclasse. (La raison pour laquelle on Throwablea choisi de nommer cette classe spéciale apparaîtra sous peu.) Directement en dessous se Throwabletrouvent les classes java.lang.Exceptionet java.lang.Error, qui décrivent respectivement les exceptions et les erreurs.

Par exemple, la bibliothèque Java inclut java.net.URISyntaxException, qui s'étend Exceptionet indique qu'une chaîne n'a pas pu être analysée en tant que référence Uniform Resource Identifier. Notez que URISyntaxExceptionsuit une convention de dénomination dans laquelle un nom de classe d'exception se termine par le mot Exception. Une convention similaire s'applique aux noms de classe d'erreur, tels que java.lang.OutOfMemoryError.

Exceptionest sous-classée par java.lang.RuntimeException, qui est la superclasse des exceptions qui peuvent être levées pendant le fonctionnement normal de la machine virtuelle Java (JVM). Par exemple, java.lang.ArithmeticExceptiondécrit les échecs arithmétiques tels que les tentatives de division des entiers par l'entier 0. java.lang.NullPointerExceptionDécrit également les tentatives d'accès aux membres d'objet via la référence null.

Une autre façon de voir RuntimeException

La section 11.1.1 de la spécification du langage Java 8 indique: RuntimeExceptionest la superclasse de toutes les exceptions qui peuvent être levées pour de nombreuses raisons lors de l'évaluation de l'expression, mais dont la récupération peut encore être possible.

Lorsqu'une exception ou une erreur se produit, un objet de la classe Exceptionou de la Errorsous-classe appropriée est créé et transmis à la JVM. Le fait de passer l'objet est appelé lancer l'exception . Java fournit la throwdéclaration à cet effet. Par exemple, throw new IOException("unable to read file");crée un nouvel java.io.IOExceptionobjet initialisé avec le texte spécifié. Cet objet est ensuite renvoyé à la JVM.

Java fournit l' tryinstruction pour délimiter le code à partir duquel une exception peut être levée. Cette instruction se compose d'un mot-clé trysuivi d'un bloc délimité par des accolades. Le fragment de code suivant illustre tryet throw:

try { method(); } // ... void method() { throw new NullPointerException("some text"); }

Dans ce fragment de code, l'exécution entre dans le trybloc et appelle method(), ce qui lève une instance de NullPointerException.

La machine virtuelle Java reçoit le jetable et recherche dans la pile d'appels de méthode un gestionnaire pour gérer l'exception. Les exceptions non dérivées RuntimeExceptionsont souvent traitées; les exceptions et les erreurs d'exécution sont rarement gérées.

Pourquoi les erreurs sont rarement gérées

Les erreurs sont rarement gérées car il n'y a souvent rien qu'un programme Java puisse faire pour récupérer de l'erreur. Par exemple, lorsque la mémoire libre est épuisée, un programme ne peut pas allouer de mémoire supplémentaire. Cependant, si l'échec d'allocation est dû au fait de conserver beaucoup de mémoire qui doit être libérée, un pirate peut tenter de libérer la mémoire avec l'aide de la JVM. Bien qu'un gestionnaire puisse sembler utile dans ce contexte d'erreur, la tentative peut échouer.

Un gestionnaire est décrit par un catchbloc qui suit le trybloc. Le catchbloc fournit un en-tête qui répertorie les types d'exceptions qu'il est prêt à gérer. Si le type du jetable est inclus dans la liste, le jetable est passé au catchbloc dont le code s'exécute. Le code répond à la cause de l'échec de telle manière que le programme se poursuive, voire s'arrête:

try { method(); } catch (NullPointerException npe) { System.out.println("attempt to access object member via null reference"); } // ... void method() { throw new NullPointerException("some text"); }

Dans ce fragment de code, j'ai ajouté un catchbloc au trybloc. Lorsque l' NullPointerExceptionobjet est renvoyé depuis method(), la machine virtuelle Java localise et transmet l'exécution au catchbloc, qui génère un message.

Enfin bloque

Un trybloc ou son catchbloc final peut être suivi d'un finallybloc utilisé pour effectuer des tâches de nettoyage, telles que la libération de ressources acquises. Je n'ai plus rien à dire finallycar ce n'est pas pertinent pour la discussion.

Les exceptions décrites par Exceptionet ses sous-classes, à l'exception de RuntimeExceptionet de ses sous-classes, sont appelées exceptions vérifiées . Pour chaque throwinstruction, le compilateur examine le type de l'objet d'exception. Si le type indique vérifié, le compilateur vérifie le code source pour s'assurer que l'exception est gérée dans la méthode où elle est levée ou est déclarée pour être gérée plus haut dans la pile des appels de méthode. Toutes les autres exceptions sont appelées exceptions non vérifiées .

Java vous permet de déclarer qu'une exception vérifiée est gérée plus haut dans la pile des appels de méthode en ajoutant une throwsclause (mot-clé throwssuivi d'une liste délimitée par des virgules de noms de classe d'exceptions vérifiées) à un en-tête de méthode:

try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }

Comme il IOExceptions'agit d'un type d'exception vérifié, les instances levées de cette exception doivent être gérées dans la méthode où elles sont levées ou déclarées pour être gérées plus haut dans la pile des appels de méthode en ajoutant une throwsclause à l'en-tête de chaque méthode affectée. Dans ce cas, une throws IOExceptionclause est ajoutée à l method()'en - tête de. L' IOExceptionobjet levé est passé à la JVM, qui localise et transfère l'exécution au catchgestionnaire.

Argumenter pour et contre les exceptions vérifiées

Checked exceptions have proven to be very controversial. Are they a good language feature or are they bad? In this section, I present the cases for and against checked exceptions.

Checked exceptions are good

James Gosling created the Java language. He included checked exceptions to encourage the creation of more robust software. In a 2003 conversation with Bill Venners, Gosling pointed out how easy it is to generate buggy code in the C language by ignoring the special values that are returned from C's file-oriented functions. For example, a program attempts to read from a file that wasn't successfully opened for reading.

The seriousness of not checking return values

Not checking return values might seem like no big deal, but this sloppiness can have life-or-death consequences. For example, think about such buggy software controlling missile guidance systems and driverless cars.

Gosling also pointed out that college programming courses don't adequately discuss error handling (although that may have changed since 2003). When you go through college and you're doing assignments, they just ask you to code up the one true path [of execution where failure isn't a consideration]. I certainly never experienced a college course where error handling was at all discussed. You come out of college and the only stuff you've had to deal with is the one true path.

Focusing only on the one true path, laziness, or another factor has resulted in a lot of buggy code being written. Checked exceptions require the programmer to consider the source code's design and hopefully achieve more robust software.

Checked exceptions are bad

Many programmers hate checked exceptions because they're forced to deal with APIs that overuse them or incorrectly specify checked exceptions instead of unchecked exceptions as part of their contracts. For example, a method that sets a sensor's value is passed an invalid number and throws a checked exception instead of an instance of the unchecked java.lang.IllegalArgumentException class.

Here are a few other reasons for disliking checked exceptions; I've excerpted them from Slashdot's Interviews: Ask James Gosling About Java and Ocean Exploring Robots discussion:

  • Checked exceptions are easy to ignore by rethrowing them as RuntimeException instances, so what's the point of having them? I've lost count of the number of times I've written this block of code:
    try { // do stuff } catch (AnnoyingcheckedException e) { throw new RuntimeException(e); }

    99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).

  • Checked exceptions can be ignored by swallowing them, so what's the point of having them? I've also lost count of the number of times I've seen this:
    try { // do stuff } catch (AnnoyingCheckedException e) { // do nothing }

    Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).

  • Checked exceptions result in multiple throws clause declarations. The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException, it's easy to overlook this).
  • Checked exceptions are not really exceptions. The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.

    The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for.

De plus, j'ai rencontré l'argument selon lequel les applications doivent gérer un grand nombre d'exceptions vérifiées qui sont générées à partir des multiples bibliothèques auxquelles elles accèdent. Cependant, ce problème peut être surmonté grâce à une façade intelligemment conçue qui tire parti de la fonction d'exception chaînée de Java et de la relance d'exceptions pour réduire considérablement le nombre d'exceptions à gérer tout en préservant l'exception d'origine qui a été levée.

Conclusion

Les exceptions vérifiées sont-elles bonnes ou mauvaises? En d'autres termes, les programmeurs devraient-ils être obligés de gérer les exceptions vérifiées ou avoir la possibilité de les ignorer? J'aime l'idée d'appliquer des logiciels plus robustes. Cependant, je pense aussi que le mécanisme de gestion des exceptions de Java doit évoluer pour le rendre plus convivial pour les programmeurs. Voici quelques façons d'améliorer ce mécanisme: