Comprendre exceptions en Java ?

Un programme peut échouer pour presque n’importe quelle raison. Voici quelques possibilités :

  • Le code essaie de se connecter à un site web, mais la connexion Internet est interrompue.
  • Vous avez fait une erreur de codage et avez essayé d’accéder à un index invalide dans un tableau.
  • Une méthode en appelle une autre avec une valeur que la méthode ne prend pas en charge.

Comme vous pouvez le constater, certaines de ces erreurs sont des erreurs de codage. D’autres sont complètement hors de votre contrôle. Votre programme ne peut pas empêcher la connexion Internet de tomber. Ce qu’il peut faire, c’est gérer la situation.

Le Rôle des Exceptions

Une exception est la façon dont Java dit : “J’abandonne. Je ne sais pas quoi faire maintenant. À vous de gérer ça.” Quand vous écrivez une méthode, vous pouvez soit gérer l’exception, soit en faire le problème du code appelant.

Comme exemple, pensez à Java comme à un enfant qui visite le zoo. Le chemin heureux est quand rien ne va mal. L’enfant continue à regarder les animaux jusqu’à ce que le programme se termine bien. Rien n’a mal tourné, et il n’y avait aucune exception à gérer.

La petite sœur de cet enfant n’a pas la même expérience. Dans toute l’excitation, elle trébuche et tombe. Heureusement, ce n’est pas une mauvaise chute. La petite fille se relève et continue à regarder d’autres animaux. Elle a géré le problème toute seule. Malheureusement, elle tombe à nouveau plus tard dans la journée et commence à pleurer. Cette fois, elle a déclaré qu’elle avait besoin d’aide en pleurant. L’histoire se termine bien. Son papa lui frotte le genou et lui fait un câlin. Puis ils retournent voir d’autres animaux et profitent du reste de la journée.

Ce sont les deux approches que Java utilise lors de la gestion des exceptions. Une méthode peut gérer le cas d’exception elle-même ou en faire la responsabilité de l’appelant.

Scénario du Monde Réel

Codes de Retour vs. Exceptions

Les exceptions sont utilisées quand “quelque chose ne va pas”. Cependant, le mot mal est subjectif. Le code suivant renvoie -1 au lieu de lancer une exception si aucune correspondance n’est trouvée :

public int indexOf(String[] noms, String nom) {
    for (int i = 0; i < noms.length; i++) {
        if (noms[i].equals(nom)) { return i; }
    }
    return -1;
}

Bien que courant pour certaines tâches comme la recherche, les codes de retour devraient généralement être évités. Après tout, Java a fourni un framework d’exception, il faut donc l’utiliser !

Comprendre les Types d’Exception

Une exception est un événement qui modifie le flux du programme. Java a une classe Throwable pour tous les objets qui représentent ces événements. Tous n’ont pas le mot exception dans leur nom de classe, ce qui peut prêter à confusion.

Exceptions Vérifiées

Une exception vérifiée est une exception qui doit être déclarée ou gérée par le code de l’application où elle est lancée. En Java, les exceptions vérifiées héritent toutes de Exception mais pas de RuntimeException. Les exceptions vérifiées ont tendance à être plus prévisibles—par exemple, essayer de lire un fichier qui n’existe pas.

Les exceptions vérifiées incluent également toute classe qui hérite de Throwable mais pas de Error ou RuntimeException, comme une classe qui étend directement Throwable.

Exceptions vérifiées ? Que vérifions-nous ? Java a une règle appelée la règle de gestion ou de déclaration. La règle de gestion ou de déclaration signifie que toutes les exceptions vérifiées qui pourraient être lancées dans une méthode sont soit enveloppées dans des blocs try et catch compatibles, soit déclarées dans la signature de la méthode.

Comme les exceptions vérifiées ont tendance à être anticipées, Java impose la règle que le programmeur doit faire quelque chose pour montrer que l’exception a été prise en compte. Peut-être qu’elle a été gérée dans la méthode. Ou peut-être que la méthode déclare qu’elle ne peut pas gérer l’exception et que quelqu’un d’autre devrait le faire.

Jetons un coup d’œil à un exemple. La méthode tomber() suivante déclare qu’elle peut lancer une IOException, qui est une exception vérifiée :

void tomber(int distance) throws IOException {
    if(distance > 10) {
        throw new IOException();
    }
}

Notez que vous utilisez deux mots-clés différents ici. Le mot-clé throw indique à Java que vous voulez lancer une Exception, tandis que le mot-clé throws déclare simplement que la méthode pourrait lancer une Exception. Elle pourrait aussi ne pas le faire.

Maintenant que vous savez comment déclarer une exception, comment la gérer ? La version alternative suivante de la méthode tomber() gère l’exception :

void tomber(int distance) {
    try {
        if(distance > 10) {
            throw new IOException();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Notez que l’instruction catch utilise Exception, pas IOException. Puisque IOException est une sous-classe de Exception, le bloc catch est autorisé à l’attraper. Nous couvrirons les blocs try et catch plus en détail plus tard dans ce chapitre.

Exceptions Non Vérifiées

Une exception non vérifiée est toute exception qui n’a pas besoin d’être déclarée ou gérée par le code de l’application où elle est lancée. Les exceptions non vérifiées sont souvent appelées exceptions d’exécution, bien qu’en Java, les exceptions non vérifiées incluent toute classe qui hérite de RuntimeException ou Error.

Il est permis de gérer ou de déclarer une exception non vérifiée. Cela dit, il est préférable de documenter les exceptions non vérifiées que les appelants devraient connaître dans un commentaire Javadoc plutôt que de déclarer une exception non vérifiée.

Une exception d’exécution est définie comme la classe RuntimeException et ses sous-classes. Les exceptions d’exécution ont tendance à être inattendues mais pas nécessairement fatales. Par exemple, accéder à un index de tableau invalide est inattendu. Même si elles héritent de la classe Exception, ce ne sont pas des exceptions vérifiées.

Une exception non vérifiée peut se produire sur presque n’importe quelle ligne de code, car il n’est pas nécessaire qu’elle soit gérée ou déclarée. Par exemple, une NullPointerException peut être lancée dans le corps de la méthode suivante si la référence d’entrée est null :

void tomber(String entree) {
    System.out.println(entree.toLowerCase());
}

Nous travaillons avec des objets en Java si fréquemment qu’une NullPointerException peut se produire presque n’importe où. Si vous deviez déclarer des exceptions non vérifiées partout, chaque méthode aurait ce désordre ! Le code se compilera si vous déclarez une exception non vérifiée. Cependant, c’est redondant.

Error et Throwable

Error signifie que quelque chose a si horriblement mal tourné que votre programme ne devrait pas essayer de s’en remettre. Par exemple, le lecteur de disque a “disparu” ou le programme a manqué de mémoire. Ce sont des conditions anormales que vous n’êtes pas susceptible de rencontrer et dont vous ne pouvez pas vous remettre.

Pour nos besoins, la seule chose que vous devez savoir sur Throwable est que c’est la classe parente de toutes les exceptions, y compris la classe Error. Bien que vous puissiez gérer les exceptions Throwable et Error, il n’est pas recommandé de le faire dans votre code d’application. Quand nous faisons référence aux exceptions dans ce chapitre, nous entendons généralement toute classe qui hérite de Throwable, bien que nous travaillions presque toujours avec la classe Exception ou des sous-classes de celle-ci.

Révision des Types d’Exception

Assurez-vous d’étudier attentivement tout ce qui se trouve dans le Tableau 1. Souvenez-vous qu’un Throwable est soit une Exception, soit une Error. Vous ne devriez pas attraper Throwable directement dans votre code.

TypeComment le reconnaîtreEst-il acceptable que le programme l’attrape ?Le programme est-il obligé de le gérer ou de le déclarer ?
Exception non vérifiéeSous-classe de RuntimeExceptionOuiNon
Exception vérifiéeSous-classe de Exception mais pas sous-classe de RuntimeExceptionOuiOui
ErrorSous-classe de ErrorNonNon

Lancer une Exception

N’importe quel code Java peut lancer une exception ; cela inclut le code que vous écrivez. Certaines exceptions sont fournies avec Java. Vous pourriez rencontrer une exception qui a été inventée spécialement. C’est bien. La question rendra évident qu’il s’agit d’une exception en faisant en sorte que le nom de la classe se termine par Exception. Par exemple, MonExceptionInventee est clairement une exception.

Vous verrez deux types de code qui entraînent une exception. Le premier est un code qui est incorrect. Voici un exemple :

String[] animaux = new String[0];
System.out.println(animaux[0]); // ArrayIndexOutOfBoundsException

Ce code lance une ArrayIndexOutOfBoundsException puisque le tableau n’a aucun élément. Cela signifie que les questions sur les exceptions peuvent être cachées dans des questions qui semblent concerner autre chose.

Faites particulièrement attention au code qui appelle une méthode sur une référence null ou qui fait référence à un index de tableau ou de List invalide. Si vous repérez cela, vous savez que la réponse correcte est que le code lance une exception au moment de l’exécution.

La deuxième façon pour le code de provoquer une exception est de demander explicitement à Java d’en lancer une. Java vous permet d’écrire des instructions comme celles-ci :

throw new Exception();
throw new Exception("Aïe ! Je suis tombé.");
throw new RuntimeException();
throw new RuntimeException("Aïe ! Je suis tombé.");

Le mot-clé throw indique à Java que vous voulez qu’une autre partie du code s’occupe de l’exception. C’est la même chose que la petite fille qui pleure pour son papa. Quelqu’un d’autre doit déterminer quoi faire à propos de l’exception.

throw vs. throws

Chaque fois que vous voyez throw ou throws, assurez-vous que le bon est utilisé. Le mot-clé throw est utilisé comme une instruction à l’intérieur d’un bloc de code pour lancer une nouvelle exception ou relancer une exception existante, tandis que le mot-clé throws est utilisé uniquement à la fin d’une déclaration de méthode pour indiquer quelles exceptions elle prend en charge.

Lors de la création d’une exception, vous pouvez généralement passer un paramètre String avec un message, ou vous pouvez ne passer aucun paramètre et utiliser les valeurs par défaut. Nous disons généralement parce que c’est une convention. Quelqu’un a déclaré un constructeur qui prend un String. Quelqu’un pourrait également créer une classe d’exception qui n’a pas de constructeur qui prend un message.

De plus, vous devez savoir qu’une Exception est un Object. Cela signifie que vous pouvez la stocker dans une référence d’objet, et ceci est légal :

var e = new RuntimeException();
throw e;

Le code instancie une exception sur une ligne, puis la lance sur la suivante. L’exception peut venir de n’importe où, même passée à une méthode. Tant que c’est une exception valide, elle peut être lancée.

Voyez-vous pourquoi ce code ne se compile pas ?

throw RuntimeException(); // NE SE COMPILE PAS

Si votre réponse est qu’il manque un mot-clé, vous avez absolument raison. L’exception n’est jamais instanciée avec le mot-clé new.

Jetons un coup d’œil à un autre endroit où on pourrait vous piéger. Pouvez-vous voir pourquoi ce qui suit ne se compile pas ?

3: try {
4:     throw new RuntimeException();
5:     throw new ArrayIndexOutOfBoundsException(); // NE SE COMPILE PAS
6: } catch (Exception e) {}

Puisque la ligne 4 lance une exception, la ligne 5 ne peut jamais être atteinte pendant l’exécution. Le compilateur reconnaît cela et signale une erreur de code inaccessible.

Appeler des Méthodes qui Lancent des Exceptions

Lorsque vous appelez une méthode qui lance une exception, les règles sont les mêmes qu’avec une méthode. Voyez-vous pourquoi ce qui suit ne se compile pas ?

class PlusDeCarottesException extends Exception {}
public class Lapin {
    public static void main(String[] args) {
        mangerCarotte(); // NE SE COMPILE PAS
    }
    private static void mangerCarotte() throws PlusDeCarottesException {}
}

Le problème est que PlusDeCarottesException est une exception vérifiée. Les exceptions vérifiées doivent être gérées ou déclarées. Le code se compilerait si vous changiez la méthode main() en l’une de ces deux versions :

public static void main(String[] args) throws PlusDeCarottesException { 
    mangerCarotte();
}

Ou bien :

public static void main(String[] args) {
    try {
        mangerCarotte();
    } catch (PlusDeCarottesException e) {
        System.out.print("lapin triste");
    }
}

Vous avez peut-être remarqué que mangerCarotte() ne lançait pas d’exception ; elle déclarait simplement qu’elle le pouvait. Cela suffit pour que le compilateur exige que l’appelant gère ou déclare l’exception.

Le compilateur est toujours à l’affût de code inaccessible. Déclarer une exception inutilisée n’est pas considéré comme du code inaccessible. Cela donne à la méthode la possibilité de changer l’implémentation pour lancer cette exception à l’avenir. Voyez-vous le problème ici ?

public void mauvais() {
    try {
        mangerCarotte();
    } catch (PlusDeCarottesException e) { // NE SE COMPILE PAS
        System.out.print("lapin triste");
    }
}
private void mangerCarotte() {}

Java sait que mangerCarotte() ne peut pas lancer une exception vérifiée, ce qui signifie qu’il n’y a aucun moyen pour que le bloc catch dans mauvais() soit atteint.

Lorsque vous voyez une exception vérifiée déclarée à l’intérieur d’un bloc catch, assurez-vous que le code dans le bloc try associé est capable de lancer l’exception ou une sous-classe de l’exception. Sinon, le code est inaccessible et ne se compile pas. N’oubliez pas que cette règle ne s’étend pas aux exceptions non vérifiées ou aux exceptions déclarées dans la signature d’une méthode.

Surcharger des Méthodes avec des Exceptions

Quand nous avons introduit la surcharge de méthodes, nous avons inclus une règle liée aux exceptions. Une méthode surchargée ne peut pas déclarer de nouvelles exceptions vérifiées ou plus larges que la méthode dont elle hérite. Par exemple, ce code n’est pas autorisé :

class NePeutPasSauterException extends Exception {}
class Sauteur {
    public void sauter() {}
}
class Lapin extends Sauteur { 
    public void sauter() throws NePeutPasSauterException {} // NE SE COMPILE PAS
}

Java sait que sauter() n’est pas autorisée à lancer des exceptions vérifiées parce que la méthode sauter() dans la superclasse Sauteur n’en déclare aucune. Imaginez ce qui se passerait si les versions des sous-classes de la méthode pouvaient ajouter des exceptions vérifiées — vous pourriez écrire du code qui appelle la méthode sauter() de Sauteur et ne gérer aucune exception. Puis, si Lapin était utilisé à sa place, le code ne saurait pas qu’il faut gérer ou déclarer NePeutPasSauterException.

Une méthode surchargée dans une sous-classe est autorisée à déclarer moins d’exceptions que la superclasse ou l’interface. C’est légal parce que les appelants gèrent déjà ces exceptions.

class Sauteur {
    public void sauter() throws NePeutPasSauterException {}
}
class Lapin extends Sauteur {
    public void sauter() {} // C'est bien
}

Une méthode surchargée ne déclarant pas l’une des exceptions lancées par la méthode parente est similaire à la méthode déclarant qu’elle lance une exception qu’elle ne lance jamais réellement. C’est parfaitement légal. De même, une classe est autorisée à déclarer une sous-classe d’un type d’exception. L’idée est la même. La superclasse ou l’interface a déjà pris en charge un type plus large.

Afficher une Exception

Il existe trois façons d’afficher une exception. Vous pouvez laisser Java l’afficher, afficher uniquement le message, ou afficher d’où provient la trace de la pile. Cet exemple montre les trois approches :

5: public static void main(String[] args) {
6:     try {
7:         sauter();
8:     } catch (Exception e) {
9:         System.out.println(e + "\n");
10:        System.out.println(e.getMessage()+ "\n");
11:        e.printStackTrace();
12:    }
13: }
14: private static void sauter() {
15:    throw new RuntimeException("ne peut pas sauter");
16: }

Ce code affiche ce qui suit :

java.lang.RuntimeException: ne peut pas sauter

ne peut pas sauter

java.lang.RuntimeException: ne peut pas sauter
    at Gestion.sauter(Gestion.java:15)
    at Gestion.main(Gestion.java:7)

La première ligne montre ce que Java affiche par défaut : le type d’exception et le message. La deuxième ligne montre uniquement le message. Le reste montre une trace de pile. La trace de pile est généralement la plus utile car elle montre la hiérarchie des appels de méthode qui ont été faits pour atteindre la ligne qui a lancé l’exception.