Les exceptions

Les Exceptions

Déroulement d’un programme

Lors de l’exécution d’un programme, on a une chaîne d’appel.

Chaque élément de la chaîne est sollicité par un appelant (caller).

L’élément sollicité est nommé appelé (callee).

L’exemple qui suit montre la lecture de données sur un support de stockage.

Déroulement normal d’un programme
Déroulement normal d’un programme

A chaque niveau d’appel, un problème peut survenir.

Erreurs possibles
Erreurs possibles

Quelle décision doit prendre un élément appelé quant au traitement du problème ?

Et de tous les appelants de la chaîne, quel est celui qui doit traiter l’événement exceptionnel ?

Java propose un mécanisme puissant permettant de gérer tous ces cas de figure

Traitement des événements exceptionnels en Java

Les exceptions sont des anomalies susceptibles de se produire dans un programme Java, écartant celui—ci du comportement normal ou souhaité.

On dit alors qu’une exception est levée.

Les origines des exceptions sont diverses :

Le comportement à adopter sera différent suivant la nature des exceptions.

Le compilateur Java impose la gestion des exceptions lorsque vous tentez d’utiliser des méthodes prévoyants des exceptions.

Par exemple, la méthode readShort de la classe DataInputStream qui lit depuis un fichier peut éventuellement lever des exceptions IOException; elle est donc définie avec un modificateur spécial throws qui signale ces erreurs potentielles.


public final short readShort() throws IOException

Exemples de code

Traitement facultatif

Un exemple d’exception dont le traitement n’est pas imposé :

public class ExceptionDemo {
    public void testTableau(){
        int[] tableauEntier = new int[5];
        tableauEntier[6] = 12; //On est en dehors des limites du tableau
    }

    public static void main(String[] args) {
        new ExceptionDemo().testTableau(); //ArrayIndexOutOfBoundsException
    }
}

Traitement imposé à la compilation

Un exemple d’exception dont le traitement est imposé par le compilateur:

import java.io.File;
import java.io.FileInputStream;
public class ExceptionDemo {
  public void testFichier(){
    File file = new File("NExistePas.txt");
    //Ci-dessous: ERREUR de COMPILATION
    //Unhandled exception type FileNotFoundException    ExceptionDemo.java
    FileInputStream fis = new FileInputStream(file);
    }
    public static void main(String[] args) {
    new ExceptionDemo().testFichier();
  }
}

Exemple : readShort

Cet exemple montre la méthode readShort lève une exception:

File f = new File(filename);

FileInputstream fis = new FileInputStream(f);
BufferedInputStream bis = new BufferedInputStream(fis,(int)f.length());
DataInputStream dis = new DataInputStream(bis);

short magicNumber = dis.readShort(); //Levée d'IOException

La méthode readshort est définie comme suit :

public final short readShort() throws IOException

En Java on est obligé de traiter les exceptions dans le code, sinon il ne peut être compilé et le message suivant est affiché :

Exception java.lang.InterruptedException
must be caught or it must be declared in the throws clause of this method.

La méthode readShort levant une exception, le code de l’exemple précédent ne passe pas à la compilation.

Gestion des exceptions

Lorsqu’une exception est levée, le déroulement du code est interrompu et le traitement des exceptions consiste alors à rechercher quelles dispositions prendre.

Au niveau de la programmation, il est possible de prévoir deux modes de codage :

  1. Soit la méthode dont le code a été interrompu dispose des informations suffisantes

pour corriger certaines anomalies observées

(ou bien pour émettre des messages d’erreur explicites).

Dans ce cas, la méthode décide de traiter elle-même certaines exceptions dans des blocs catch associés à un bloc try.

  1. Soit la méthode concernée ne dispose pas elle-même des informations suffisantes

pour traiter certains types d’exceptions,

mais les méthodes appelantes peuvent disposer de ces informations.

Dans ce cas les exceptions sont déclarées dans la clause throws de la signature de la méthode afin que les méthodes appelantes soient informées des types d’exccptions susceptibles d’être levées par une méthode.

Traitement dans la méthode où l’exception se produit

Reprenons l’exemple précédent :

public void lireImageRGB(String filename ) {
  File f = new File(filename);

  FileInputstream fis = new FileInputStream(f);
  BufferedInputstream bis = new BufferedInputStream(fis,(int)f.length());
  DataInputstream dis = new DataInputStream(bis);

  short magicNumber = dis.readShort();
  //...
}

La première étape consiste à inclure le code susceptible de lever des exceptions dans un bloc try { ... }

A un bloc try doivent être associées une ou plusieurs clauses catch et éventuellement un bloc finally.

public void lireImageRGB(String filename ) {
    try {
      File f = new File(filename);
      FileInputStream fis = new FileInputStream(f);
      BufferedInputStream bis = new BufferedInputStream(fis,(int)f.length());
      DataInputStream dis = new DataInputStream(bis);
      short magicNumber = dis.readShort();
    }
    catch ( Exception e ) {
      System.out.println(" exception: " + e.getMessage());
      e.printStackTrace();
    }
}

Ou plus finement (il est toujours préférable de gérer les exceptions selon leur type) :

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class GestionExceptionProgramme {
    public void lireImageRGB(String filename ) {
        DataInputStream dis = null;
        try {
            File f = new File(filename);
            FileInputStream fis = new FileInputStream(f);
            BufferedInputStream bis = new BufferedInputStream(fis,(int)f.length());
            dis = new DataInputStream(bis);
            short magicNumber = dis.readShort () ;
        }
        catch ( FileNotFoundException f ) { // sous—classe de IOException
            System.err.println("exception: " + f.getMessage());
            f.printStackTrace();
        }
        catch ( IOException e ) { // sous—classe de Exception
            System.out.println("exception: " + e.getMessage());
            e.printStackTrace();
        }
        finally { //Ferme proprement le fichier
            try {
                if (dis != null){ dis.close(); }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new GestionExceptionProgramme()
          .lireImageRGB("UnFichierInexistant.jpg");
    }
}

Le bloc try commence avant la ligne File f = new File(filename); car le constructeur de la classe File peut lui aussi lever une exception de type NullPointerException, dans le cas où le nom de fichier passé en paramètre est égal à null.

Si une exception se produit dans un bloc try, l’exécution de code est interrompue et les clauses catch qui lui sont associées sont examinées dans l’ordre pour chercher si l’unc d’elles a prévu un traitement de l’exception produite. Les clauses catch sont comme des méthodes recevant un seul argument.

Si une clause catch correspond à l’exception, elle prend le contrôle de l’exécution.

Le code d’un bloc associé à une clause finally est exécuté dans tous les cas, soit après une exécution normale du bloc try soit après traitement d’une exception

Traitement dans la méthode appelante

Pour indiquer que l’Exception n’est pas gérée dans la méthode lireImageRGB() et que par conséquent, elle le sera dans la méthode appelante, on utilise la clause throws.

Reprenons l’exemple précédent :

public void lireImageRGB(String filename ) throws Exception {

  File f = new File(filename);

  FileInputStream fis = new FileInputStream(f);
  BufferedInputStream bis = new BufferedInputStream(fis, (int)f.length());
  DataInputStream dis = new DataInputStream(bis);

  short magicNumber = dis.readShort();

}

ou plus finement

public void lireImageRGB(String filename )
    throws  FileNotFoundException,
            IOException  {

  File f = new File(filename);
  FileInputStream fis = new FileInputStream(f);
  BufferedInputStream bis = new BufferedInputStream(fis, (int)f.length());
  DataInputStream dis = new DataInputStream(bis);
  short magicNumber = dis.readShort();
}

Types d’exceptions

Le type Throwable

Une exception est un objet (instance d’une classe qui hérite de la classe Throwable) qui possède des données et des méthodes membres.

La classe Exception
La classe Exception

Throwable a deux sous-classes : Error et Exception.

Les instances d’Error sont des erreurs internes de l’environnement d’exécution Java (la machine virtuelle). Ces erreurs sont rares, et habituellement fatales; ces exceptions ne doivent pas être levées, ni capturées dans un programme.

Les sous-classes d’Exception sont réparties entre deux catégories :

Exceptions non vérifiées

Runtime Exception

Un bloc try/catch n’est pas imposé pour traiter ces exceptions

Error

Les erreurs font parties des objets Throwables non vérifiés:

Exceptions vérifiées

Exceptions à traiter obligatoirement

Bloc try/catch obligatoire

Exemples d’événements anormaux

Hiérarchie de classes

La hiérarchie des exceptions est comme celle des autres classes, les super-classes correspondant à des erreurs plus générales, et les sous-classes à des erreurs plus spécifiques.

La plupart des exceptions font partie du package java.lang (qui inclut Throwable, Exception et RuntimeException). Mais un grand nombre d’autres packages définissent d’autres exceptions, et ces exceptions sont utilisées dans toute la bibliothèque de classes.

Par exemple

Le package java.io définit une classe générale d’exceptions appelée IOException, qui est sous-classée non seulement dans le package java.io pour les exceptions (d’entrée et de sortie (EOFException, FileNotFoundException), mais aussi dans les classes du package java.net pour les exceptions de réseau telles que MalFormedURLException.

Ci dessous, est représentée une partie de la hiérarchie des exceptions.

Exemple concret

Prenons l’exemple suivant :


public class ExceptionCatch {
    int moyenne(String[] liste) {
        int somme = 0, entier, nbNotes = 0;
        for (int i = 0; i < liste.length;i++) {
            try {
                entier = Integer.parseInt(liste[i]);
                somme += entier;
                nbNotes++;
            }
            catch (NumberFormatException e) {
                if ( i == 0 ) {
                    System.out.println("La 1ere note n'est pas entiere");
                }
                else {
                    System.out.println("La "+(i+1)+"eme note n'est pas entiere");
                }
            }
        }
        return somme/nbNotes;
    }
    public static void main(String[] argv) {
        String[] liste = {"1", "5", "toto"};

        System.out.println(
                "La moyenne est " + new ExceptionCatch().moyenne(liste));
    }
}

On obtient en sortie:

La 3eme note n'est pas entiere
La moyenne est 3

Si on remplace la ligne

String[] liste = {"1", "5", "toto"};

Par

String[] liste = {"toto", "15.5"};

On obtient:

La 1ere note n'est pas entiere
La 2eme note n'est pas entiere
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at ExceptionCatch.moyenne(ExceptionCatch.java:20)
    at ExceptionCatch.main(ExceptionCatch.java:26)

Dans cet exemple deux types d’exceptions peuvent être levées :

  somme+=entier;
  nbNotes++;

ne sont pas effectuées, et on passe à l’instruction println intérieure au “bloc catch”, puis on remonte dans la “boucle for” qui reprend son déroulement.

Celle—ci n’étant pas attrapée, elle se propage alors jusqu’à la fin du programme, comme on peut le constater sur le deuxième exemple d’exécution.

Mais seule l’exception java.lang.NumberFormatException doit être obligatoirement traitée, soit localement (c’est le cas ici), soit dans la méthode appelante. En effet la méthode parseInt prévoyant cette exception, le compilateur oblige la gestion de l’exception dans le code.

Création et levée de vos propres exceptions

Création d’exception

Il est possible pour le programmeur de créer ses propres classes d’exception en héritant soit de la classe Exception (sommet de la hiérarchie des classes explicites), soit en cherchant une exception plus proche de celle que l’on veut créer. Par exemple : une exception pour un mauvais format de fichier doit normalement être une IOException.

Les classes d’exceptions ont typiquement deux constructeurs : le premier ne prend pas d’argument, et le second prend une seule chaîne comme argument. Dans le second, vous devez appeler super() dans le constructeur pour vous assurer que la chaine est utilisée au bon endroit dans l’exception.

Les classes d’exceptions ont le même aspect que d’autres classes. Vous pouvez les placer dans leur propres fichiers source et les compiler comme n’importe quelles autres classes.

Création d’exception

public class NotesNonValidesException extends Exception {

    public NotesNonValidesException(String message) {
    // message sera utilisé
        super(message); // par la méthode getMessage()
    }

    public String toString() {
        // Rappel : cette méthode permet
        // de convertir l'objet en une
        return("Aucune note n'est valide"); // chaîne de caractères
    }
}

Levée d’exception

L’exception est levée dans le code à l’aide de la clause throw.

Reprenons l’exemple précédent (ExceptionCatch). Imaginons que nous voulions éviter, l’exception java.lang.ArithmeticException.

Une solution consiste à tester si nbNotes == 0, avant d’effectuer le calcul de la moyenne et à signaler le problème en levant l’exception créée précédemment (NotesNonValidesException).

Programme


public class TraiterNotes {
    static int moyenne(String[] liste) throws NotesNonValidesException {
        int somme = 0,entier, nbNotes = 0;
        for (int i = 0; i < liste.length;i++){
            try {
                entier = Integer.parseInt(liste[i]);
                somme += entier;
                nbNotes++;
            }
            catch (NumberFormatException e) {
        String msg = "n'est pas entiere";
                if (i == 0) {
              msg = "La 1ere note " + msg ;}
                else{ msg = "La "+(i+1)+" note " + msg ;}
            }
        }
        if (nbNotes == 0){ throw new NotesNonValidesException("Dommage"); }

        return somme/nbNotes;
    }

    public static void main(String[] argv) {
        try {
            System.out.println("La moyenne est "+moyenne(argv));
        }
        catch (NotesNonValidesException e) {
            System.out.println(e);
            System.out.println(e.getMessage());
        }
    }
}

Pour :

java TraiterNotes ha 15.5

On obtient :

La 1ere note n'est: pas entière
La 2eme note n'est pas entiere
Aucune note n'est valide
Dommage

Explications

throws NotesNonvalidesException

La méthode moyenne indique ainsi qu’il est possible qu’une de ses instructions envoie une exception de type NotesNonValidesException sans que celle-ci soit attrapée par un mécanisme try-catch.

Il est obligatoire d’indiquer ainsi un éventuel lancement d’exception non attrapée, sauf pour les exceptions les plus courantes de l‘API (RuntimeException). Si vous oubliez de signaler par la clause throws l’éventualité d’un tel lancement d’exception, le compilateur vous le rappelera.

throw new NotesNonvalidesException()

On demande ainsi le lancement d’une instance de NotesNonvalidesException. Une fois lancée, l’exception se propagera comme expliqué dans l’exemple précédent.

Ici, il y aura sortie de la méthode moyenne, on se retrouve alors à l’appel de la la méthode moyenne dans le main, appel qui se trouve dans un bloc try suivi d’un bloc catch qui attrape les NotesNonValidesException :

Exercices

Calcul

Reprendre le programme gérant les opérations mathématique (les sources sont disponibles ici.

Ajoutez l’opération de Division

Celle-ci devra lever une exception de division par zéro (nouvelle classe DivisionParZeroException). Bien choisir sa classe parente afin d’avoir un minimum de modifications à faire sur l’ensemble des classes (peut-être y a-t-il déjà une classe pouvant correspondre à ce type d’erreur dans l’API Java ?).

Tester ce programme en rajoutant une clause catch permettant d’afficher un message concernant l’erreur à l’utilisateur.

Le nom de l’exception

Prenez les codes suivants et analysez-les:

  1. renommer les exceptions en leur donnant un nom plus explicite.
  2. déterminer comment lancer le programme sans qu’une erreur ne se produise depuis Eclipse (aller pour cela dans le menu Run > Run Configurations... puis dans l’onglet Arguments)
  3. que pensez-vous de la déclaration de la méthode main ? En quoi est-il utile d’indiquer la clause throws ici ?
  4. modifier le code pour que la méthode main traite l’exception

Classes E1:

package com.example.exception;

public class E1 extends Exception {
    public E1(String mess) {
        super(mess);
    }
}

Classe E2:

package com.example.exception;

public class E2 extends Exception {
    public E2(String mess) {
        super(mess);
    }
}

Classe AutoDivision :

package com.example.exception;

public class AutoDivision {
    private double x;

    public AutoDivision(double x) {
        this.x = x;
    }

    public double getX() {
        return x;
    }

    public void calculer(double y) throws E1,E2 {
        if (y == 0){
            throw new E1("Paramètre nul: le paramètre ne peut être égal à zéro");
        }
        if (x * y < 0){
            throw new E2("paramètre de signe opposé à x");
        }
        x = x / y;
    }

    public void executer(double y) throws E1 {
        try {
            calculer(y);
        }
        catch (E2 e) {
            System.err.println(e.getMessage());
        }
    }

    public static void main(String[] args) throws E1 {
        AutoDivision ad = new AutoDivision(Double.parseDouble(args[0]));
        ad.executer(Double.parseDouble(args[1]));
        System.out.println("x = " + ad.getX());
        ad.executer(Double.parseDouble(args[2]));
        System.out.println("x = " + ad.getX());
    }
}

Factorielle

Voici une classe de calcul de factorielle:

package com.example.maths;

public class Fonctions {
    private static long factorielleT(long n, long acc){
        if(n <= 1){
            return acc;
        }
        else{
            return factorielleT(n - 1, n * acc);
        }
    }
    public static long factorielle(long n){
        return factorielleT(n, 1);
    }
  public static long factorielle(long n){
        return factorielleT(n, 1);
    }
}

Créer les exceptions permettant de gérer deux cas d’erreur:

Modifier les méthodes afin de prendre en compte ces exceptions.

A vous de déterminer la valeur maximale pouvant être traitée par cette fonction (les constantes Long.MAX_VALUE et Integer.MAX_VALUE peuvent vous aider). Dans ce cas, il est possible aussi que le nombre retourné devienne négatif : comment faire alors ?

Gestion de mot de passe

Parmi les failles de sécurité réseau, on trouve notamment l’utilisation du dépassement.

Par exemple, sur certaines programmes, un login ou un mot de passe de plus d’un mega-octet peut le faire planter. Ceci donne alors un accès root au système.

Dans d’autre cas, il est possible de faire une injection SQL.

Vous allez écrire un programme capable de gérer ces problèmes avec différents types d’exception.

  1. Écrire un programme qui demande en boucle un nom d’utilisateur (login) et un mot de passe (password) jusqu’à recevoir une paire login/password correcte

Vous stockerez les données de connexion suivantes en dur dans ce programme: duke/Ellington et alexander/Abreu.

  1. Implémenter les exceptions suivantes:
  1. Implémenter de façon à utiliser ces exceptions de façon pertinente pour pouvoir tracer ces exceptions. Ainsi, la SQLInjectionException ne devra pas faire l’objet d’un affichage standard d’erreur (l’attaquant ne doit pas savoir que le système sait détecter les injections SQL)
  2. Déterminer les autres cas où l’on pourrait lever la SQLInjectionException