Exceptions

Les exceptions sont un mécanisme de propagation des erreurs.
Très pratique car permet d'écrire du code plus lisible, non pollué par la gestion des cas d'erreur.

Le mécanisme des exceptions utilise 5 mots-clés du langage : try, catch, finally, throw et throws.

Sans les exceptions :
fonction ContenuDuFichier(String filename){

    ouvrirFichier(filename)
    
    si erreur d'ouverture
        return null
    
    String contentu = lireFichier
    
    Si erreur de lecture
        return null
    
    return contenu
}
Avec les exceptions :
fonction ContenuDuFichier(String filename){
    try{
        ouvrirFichier(filename)
        String contentu = lireFichier
        return contenu
    }
    catch(ErreurOuverture e){
        return null
    }
    catch(ErreurLecture e){
        return null
    }
}
Le programme s'exécute ; si une instruction déclenche une exception, l'exécution s'arrête et continue dans le bloc catch correspondant au type d'exception.

En plus de try et catch, on peut définir un bloc finally, qui va s'exécuter dans tous les cas, qu'une exception ait été lancée ou pas.
Le bloc finally est exécuté même si une exception est propagée ; utile par exemple pour fermer une connexion réseau ou à une base de données.
catch et finally sont optionnels, mais au moins un des deux est nécessaire.
Les accolades sont obligatoires dans ces blocs, même s'ils ne contiennent qu'une ligne.
try {

    // instructions
    
} catch(ExceptionType1 e){
    // traitement de ce cas anormal de type ExceptionType1
    // e est de type ExceptionType1
}

catch(ExceptionType2 e){
    // traitement de ce cas anormal de type ExceptionType2
    // e est de type ExceptionType2
}

finally {
    traitement de fin de bloc try
}
Une même clause catch peut gérer plusieurs types d'exception, en les séparant par |.
try{
    ...
}
catch(ExceptionType1 | ExceptionType2 e){
    // la variable e est un objet de type ExceptionType1 ou ExceptionType2
}

Les classes d'exception

Les exceptions que l'on peut lancer ou attrapper sont des objets de type java.lang.Exception ou de ses sous-classes.
L'API java fournit un grand nombre de classes : Hiérarchie Throwable
On distingue 2 types principaux d'exceptions :
- les exceptions vérifiées (checked exceptions), qui peuvent être anticipées et doivent être gérées par l'application.
- les exceptions non vérifiées (unchecked exceptions), qui ne peuvent pas être anticipées (par ex OutOfMemoryError).
Les unchecked exceptions descendent de Error ou de RuntimeException .

Une exception contient un message, accessible via getMessage().
printStackTrace() est aussi courrament utilisée.

Créer ses propres exceptions

On peut créer des exceptions personnalisées en sous-classant Exception :
public MyException extends Exception {}
Dans le code qu'on écrit on a le choix entre utiliser Exception et ses sous-classes lorsque c'est pertinent, ou les classes d'exceptions spécifiques à l'application.

Lancer des exceptions

Une exception est "lancée" avec l'instruction throw lorsqu'une condition anormale d'exécution est rencontrée :
public static double factorial(int x) throws IllegalArgumentException{
    if(x < 0){
        throw new IllegalArgumentException("x must be >= 0");
    }
    double fact;
    for(fact=1.0; x > 1; fact *= x, x--)
        ; // instruction vide
    return fact;
}
(code dans Factorial.java)

Propagation des exceptions

Si l'interpréteur java rencontre une exception, il arrête l'exécution du code et cherche un gestionnaire d'exception (un bloc catch).
L'interpréteur java va remonter la pile d'appel des méthodes jusqu'à ce qu'il trouve un bloc catch correspondant au type de l'exception en cours ou à une superclasse.
Par exemple, si on se place dans une méthode qui utilise factorial() :
void maMéthode(){
    ...
    double f = factorial(-5);
    ...
}
- Soit maMéthode() contient un bloc try catch gérant IllegalArgumentException
void maMéthode(){
    try{
        ...
        double f = factorial(-5);
        ...
    }
    catch(IllegalArgumentException e){
        // gestion de e
    }
}
Dans ce cas le code présent dans ce bloc catch sera exécuté.

- Soit maMéthode() ne gère pas IllegalArgumentException et doit alors obligatoirement déclarer qu'elle est susceptible de lancer une exception avec le mot-clé throws :
void maMéthode throws IllegalArgumentException(){
    ...
    double f = factorial(-5);
    ...
}
Dans ce cas, l'exception sera propagée à la méthode qui a appelé maMéthode(), ainsi de suite jusqu'à la fonction main().
Donc en général, le code d'une fonction main() contient un try catch :
public static void main(String[] args) {
    try{
    }
    catch(Exception e){
    }
Dans ce cas, on a intérêt à utiliser la classe Exception, qui est la superclasse de toutes les autres exceptions, çe qui permet de rattrapper toute erreur imprévue dans le programme, quelque soit son type.

A noter : les blocs catch sont évalués les uns après les autres. Le compilateur impose donc que les cas particuliers soient traités d'abord.
void maMéthodeQuiNePassePasALaCompil(){
    try{
        ...
        double f = factorial(-5);
        ...
    }
    catch(Exception e){
    }
    catch(IllegalArgumentException e){
    }
}
Si ce code passait à la compilation, le catch de IllegalArgumentException ne serait jamais appelé puisque c'est une sous-classe de Exception.
Exercice Multiples2

try with resources (TWR)

Voir le cours sur java.io