Les exceptions

Les exceptions #

L’utilité des exceptions #

Pour comprendre l’utilité d’un mécanisme de gestion d’exceptions, commençons par montrer un exemple sans gestion. En C par exemple, les exceptions n’existent pas.

Voici un pseudo-code qui lit un fichier contenant des données altimétriques et calcule la moyenne des altitudes.

1
2
3
4
File file = ???
String content = readFile(file)
double[] alts = convertToDouble(content)
double mean = sum(alts) / count(alts)

Dans cet exemple, plusieurs problèmes pourraient survenir:

  • le fichier n’existe pas ou sa lecture est impossible,
  • le contenu contient des valeurs non numériques impossibles à convertir en double,
  • une division par zéro est effectuée (le fichier ne contient peut-être aucune altitude),

Les langages qui n’ont pas d’exceptions retournent généralement une valeur arbitraire, appelée également valeur passerelle, à la place du résultat.

Un serveur http retourne un résultat avec un code indiquant l’erreur éventuelle.

En supposant qu’il soit possible de retourner un couple contenant le résultat et un code d’erreur, voici comment les anomalies devraient être gérées.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
File file = ???
(String content, Status statusRead) = readFile(file)
if ( statusRead.isOk() ){
  (double[] alts, Status statusConvert) = convertToDouble(content)
  if ( statusConvert.isOk() ){
    (double mean, Status statusDiv) = sum(alts) / count(alts)
    if ( statusDiv.isOk() ){
      /* do something with mean */
    }else{
      // oups
    }
  }else{
    // oups
  }
}else{
  // oups
}

C’est dans ce cas où un mécanisme de gestion d’exceptions est très utile.

Utilité
Un mécanisme de gestion d’exception apporte un avantage considérable : celui de séparer la logique de la gestion des erreurs.

Voici une telle gestion à l’aide d’un pseudo-code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
try {
  /* La logique est décrite dans ce bloc */
  File f = ???
  String content = readFile(file)
  double[] alts = convertToDouble(content)
  double mean = sum(alts) / count(alts)
  // do something with mean 

/* le traitement des exceptions est complétement séparé: */
} catch (ReadException e) { // oups }
} catch (ConvertException e) { // oups }
} catch (DivisionException e) { // oups }

Ceci simplifie la logique, mais apporte également beaucoup plus de clarté.

Lever sa propre exception #

Par le passé, j’ai préconisé dans un premier temps l’emploi de RuntimeException pour lever tous types d’exceptions. En réalité, c’est une mauvaise pratique : il faudrait toujours utiliser une exception la plus spécifique possible en fonction du contexte. La signature de cette méthode n’indique rien d’utile :

Something getMeSomething(int id) throws RuntimeException;

Celle-ci est beaucoup plus intuitive:

Something getMeSomething(int id) throws NoSuchElementException;

Throwable #

En Java, une exception est une classe qui hérite de Throwable. Le langage propose une multitude d’exceptions organisée autour de trois classes principales dont le schéma est illustré en dessous:

  • Throwable : classe de base de Error et Exception
  • Error : cette classe et ses sous-classes sont généralement intraitables
    • Le problème est tellement grave que l’arrêt du programme est nécessaire (par ex: VirtualMachineError, StackOverflowError, OutOfMemoryError…)
  • Exception : toute la hiérarchie des exceptions qui doivent être traitées, à l’exception de la branche RuntimeException.
    • Exemple d’exceptions qui doivent être traitées obligatoirement (avec un bloc try/catch par exemple): IOException, SQLException, IllegalClassFormatException
  • RuntimeException : hérite de Exception. Cette classe et ses sous-classes font partie des unchecked exceptions. C’est-à-dire qu’elles n’ont pas besoin d’être traitées (par ex: NullPointerExcpetion, ArithmeticException, IndexOutOfBoundException, EmptyStackException…). Il n’est pas nécessaire non plus de les déclarer dans les méthodes et constructeurs qui pourraient les lever.

Hiérarchie des exceptions

Exceptions #

Toutes les classes qui héritent d'Exception, à l’exception de RuntimeException, doivent être traitées et déclarées.

La méthode test doit déclarer qu’elle retourne une exception.

1
2
3
void test() throws MandatoryException { 
    throw new MandatoryException("message");
 }

L’appelant doit traiter l’exception obligatoirement

  • à l’aide d’un bloc try/catch
1
2
3
4
5
void useTest() {
    try {
      test();
    } catch ( MandatoryException e) {...}
}
  • en propageant l’exception plus haut
1
2
3
void useTest() throws MandatoryException {
    test();
}
  • ou en la transformant en une nouvelle exception
1
2
3
4
5
6
7
void useTest() {
    try {
      test();
    } catch ( MandatoryException e) {
        throw new OtherException( e.getMessage() );
    }
}

Unchecked exceptions (RuntimeException & enfants) #

Comme il a été précisé, les unchecked exceptions n’ont pas besoin d’être traitées. C’est un bon choix lorsque tous les moyens sont offerts à l’utilisateur pour éviter ce genre de problème. Imaginez s’il fallait utiliser un bloc try/catch à chaque division.

Les exemples ci-dessous ne nécessitent pas obligatoirement de gestion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int i = 10;
int j = 0;
System.out.println( i / j );
|  Exception java.lang.ArithmeticException: / by zero
|        at (#23:1)

Deque<Integer> q = new LinkedList<>(); /* structure permettant un comportement
                                        * de File/Queue */
System.out.println( q.getFirst() );
|  Exception java.util.NoSuchElementException
|        at LinkedList.getFirst (LinkedList.java:248)
|        at (#25:1)

Il est possible d’éviter ces erreurs de manière traditionnelle.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int i = 10;
int j = 0;
if (j != 0) {
    System.out.println( i / j );
}

Deque<Integer> q = new LinkedList<>();
if ( !q.isEmpty() ) {
    System.out.println( q.getFirst() );
}

Conseil

Pensez également à offrir des fonctionnalités qui permettent aux utilisateurs de vérifier l’état d’un objet avant de faire une opération dangereuse. Vous pouvez également adopter un style déclaratif qui est discuté dans un chapitre choisi du cours.

Traitement des exceptions #

try-catch #

Le traitement se fait à l’aide d’un bloc try/catch.

1
2
3
4
5
6
7
try {
    /* try something */
} catch (NullPointerException ex) {
  System.err.println("Error : " + ex.getMessage());
} catch (NoSuchElementException ex) {
  System.err.println("Error : " + ex.getMessage());
}

Si le traitement des exceptions est le même, il est possible de les regrouper:

1
2
3
4
5
try {
    /* try something */
} catch (NullPointerException | NoSuchElementException ex) {
  System.err.println("Error : " + ex.getMessage());
}

Un bloc finally peut être utilisé. C’est le cas pour les ressources qui doivent être fermées dans tous les cas. Voici un (vieil) exemple pour la lecture de fichier:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public String readFirstLine() {
  BufferedReader br = null;
  try { 
      br = new BufferedReader(new FileReader("Filename.txt"));
      while ((str = br.readLine()) != null) {
           System.out.println(str); 
      }
  } catch (FileNotFoundException f) {
    System.err.println("File not found"); // affiche l'erreur sur stderr
  } catch (IOException e) {
    e.printStackTrace(); // crash en affichant les détails de la stack sur stderr
  } finally { // dans tous les cas, les ressources doivent être fermées
    if (br != null) {
        try { // et oui, une exception peut être levée lors d'un close
            br.close(); 
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  } 
}

Clause try-with-resources et Autoclosable #

Heureusement, depuis Java 8, la gestion se simplifie à l’aide l’instruction try-with-resources. Notez les parenthèses après le try. Ceci nous évite de devoir créer un objet null au préalable et nous évite également de fermer les ressources explicitement. Ceci est souvent mal fait, oublié ou compliqué. Cette instruction fonctionne pour tous les objets qui héritent de l’interface AutoClosable.

1
2
3
4
5
6
7
8
9
try ( BufferedReader br = new BufferedReader(new FileReader("Filename.txt")) ) {
  while ((str = br.readLine()) != null) {
       System.out.println(str); 
  }
} catch (FileNotFoundException f) {
    System.err.println("File not found");
} catch (IOException e) {
    e.printStackTrace();
} 

Réalisez vos propres exceptions #

Vous pouvez directement hériter votre classe de l’exception la plus adéquate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Unchecked Exception avec message commun
public class UncheckedException extends RuntimeException {
  public UncheckedException() { super("Message specific"); }
}

/* Exception qui doit être traitée, 
 * dans cet exemple, le message doit être spécifié par l'appelant */
public class MandatoryException extends Exception {
  public MandatoryException(String msg) { super(msg); }
}

Emploi d’exceptions existantes #

Avant de créer vos exceptions, vérifiez toujours si une exception appropriée existe. Sinon, réalisez les vôtres. Rappelez-vous de toujours être spécifique.

Evitez également de traiter un cas trop général.

1
2
3
4
5
  try {
      return Integer.parseInt( br.readLine() );
  } catch (Exception e) {
    /* ... */
  } 

Préférez indiquer quel type d’exception vous tentez de traiter.

1
2
3
  try {
      return Integer.parseInt( br.readLine() );
  } catch (NumberFormatException) { /* ... */ }

Pareil lorsque vous levez des exceptions. Evitez un cas trop général.

Something getMeSomething(int id) throws RuntimeException;

Préférez une exception adaptée. Dans l’extrait ci-dessous, si un élément peut être inexistant par exemple.

// si l'élément peut être inexistant par exemple
Something getMeSomething(int id) throws NoSuchElementException;











comments powered by Disqus