Comment gérer les exceptions en Java ?

Que faites-vous lorsque vous rencontrez une exception ? Comment gérez-vous ou récupérez-vous après l’exception ? Dans cette section, nous montrons les différentes instructions en Java qui permettent de gérer les exceptions.

Utilisation des instructions try et catch

Maintenant que vous savez ce que sont les exceptions, explorons comment les gérer. Java utilise une instruction try pour séparer la logique qui pourrait lancer une exception de la logique qui gère cette exception. La figure ci-dessous montre la syntaxe d’une instruction try.

Le code dans le bloc try s’exécute normalement. Si l’une des instructions lance une exception qui peut être attrapée par le type d’exception listé dans le bloc catch, le bloc try arrête son exécution, et l’exécution passe à l’instruction catch. Si aucune des instructions du bloc try ne lance une exception qui peut être attrapée, la clause catch n’est pas exécutée.

Vous avez probablement remarqué les mots bloc et clause utilisés de manière interchangeable. Les deux sont corrects. Bloc est correct car il y a des accolades présentes. Clause est correct car c’est une partie d’une instruction try.

Il n’y a pas beaucoup de règles de syntaxe ici. Les accolades sont obligatoires pour les blocs try et catch. Dans notre exemple, la petite fille se relève toute seule la première fois qu’elle tombe. Voici à quoi cela ressemble :

void explorer() {
  try {
    tomber();
    System.out.println("jamais atteint");
  } catch (RuntimeException e) {
    seRelever();
  }
  voirAnimaux();
}

void tomber() { throw new RuntimeException(); }

D’abord, la ligne 5 appelle la méthode tomber(). La ligne 12 lance une exception. Cela signifie que Java passe directement au bloc catch, en sautant la ligne 6. La fille se relève à la ligne 8. Maintenant l’instruction est terminée, et l’exécution se poursuit normalement avec la ligne 10.

Examinons maintenant quelques instructions try invalides. Voyez-vous ce qui ne va pas avec celle-ci ?

try // NE COMPILE PAS
  tomber();
catch (Exception e)
  System.out.println("se lever");

Le problème est que les accolades {} sont manquantes. Les instructions try sont comme des méthodes en ce sens que les accolades sont obligatoires même s’il n’y a qu’une seule instruction à l’intérieur des blocs de code, tandis que les instructions if et les boucles sont spéciales et vous permettent d’omettre les accolades.

Et celle-ci ?

try { // NE COMPILE PAS
  tomber();
}

Ce code ne compile pas car le bloc try n’a rien après lui. Rappelez-vous, le but d’une instruction try est que quelque chose se produise si une exception est lancée. Sans une autre clause, l’instruction try est isolée. Comme vous le verrez bientôt, il existe un type spécial d’instruction try qui inclut un bloc finally implicite, bien que la syntaxe soit assez différente de cet exemple.

Chaîner des blocs catch

Dans cette section, vous pourriez rencontrer des classes d’exceptions et devoir comprendre comment elles fonctionnent. Voici comment les aborder. D’abord, vous devez être capable de reconnaître si l’exception est une exception vérifiée ou non vérifiée. Ensuite, vous devez déterminer si certaines exceptions sont des sous-classes des autres.

class AnimauxEnPromenade extends RuntimeException {}
class ExpositionFermee extends RuntimeException {}
class ExpositionFermeePourDejeuner extends ExpositionFermee {}

Dans cet exemple, il y a trois exceptions personnalisées. Toutes sont des exceptions non vérifiées car elles étendent directement ou indirectement RuntimeException. Maintenant, nous chaînons les deux types d’exceptions avec deux blocs catch et les gérons en imprimant le message approprié :

public void visiterPorc_epic() {
  try {
    voirAnimal();
  } catch (AnimauxEnPromenade e) { // premier bloc catch
    System.out.print("revenez plus tard");
  } catch (ExpositionFermee e) { // deuxième bloc catch
    System.out.print("pas aujourd'hui");
  }
}

Il y a trois possibilités lorsque ce code est exécuté. Si voirAnimal() ne lance pas d’exception, rien n’est imprimé. Si l’animal est en promenade, seul le premier bloc catch s’exécute. Si l’exposition est fermée, seul le deuxième bloc catch s’exécute. Il n’est pas possible que les deux blocs catch soient exécutés lorsqu’ils sont chaînés de cette façon.

Une règle existe pour l’ordre des blocs catch. Java les regarde dans l’ordre où ils apparaissent. S’il est impossible que l’un des blocs catch soit exécuté, une erreur de compilation concernant du code inaccessible se produit. Par exemple, cela se produit lorsqu’un bloc catch de superclasse apparaît avant un bloc catch de sous-classe.

Dans l’exemple du porc-épic, l’ordre des blocs catch pourrait être inversé car les exceptions n’héritent pas l’une de l’autre. Et oui, nous avons déjà vu un porc-épic se promener en laisse.

L’exemple suivant montre des types d’exception qui héritent l’un de l’autre :

public void visiterSinges() {
  try {
    voirAnimal();
  } catch (ExpositionFermeePourDejeuner e) { // Exception de sous-classe
    System.out.print("revenez plus tard");
  } catch (ExpositionFermee e) { // Exception de superclasse
    System.out.print("pas aujourd'hui");
  }
}

Si l’exception plus spécifique ExpositionFermeePourDejeuner est lancée, le premier bloc catch s’exécute. Sinon, Java vérifie si l’exception de superclasse ExpositionFermee est lancée et l’attrape. Cette fois, l’ordre des blocs catch est important. L’inverse ne fonctionne pas.

public void visiterSinges() {
  try {
    voirAnimal();
  } catch (ExpositionFermee e) {
    System.out.print("pas aujourd'hui");
  } catch (ExpositionFermeePourDejeuner e) { // NE COMPILE PAS
    System.out.print("revenez plus tard");
  }
}

Si l’exception plus spécifique ExpositionFermeePourDejeuner est lancée, le bloc catch pour ExpositionFermee s’exécute — ce qui signifie qu’il n’y a aucun moyen pour que le deuxième bloc catch s’exécute. Java vous indique correctement qu’il y a un bloc catch inaccessible.

Essayons une dernière fois. Voyez-vous pourquoi ce code ne compile pas ?

public void visiterSerpents() {
  try {
  } catch (IllegalArgumentException e) {
  } catch (NumberFormatException e) { // NE COMPILE PAS
  }
}

Souvenez-vous que NumberFormatException est une sous-classe de IllegalArgumentException ? C’est la raison de cet exemple. Puisque NumberFormatException est une sous-classe, elle sera toujours attrapée par le premier bloc catch, rendant le second bloc catch du code inaccessible qui ne compile pas. De même, sachez que FileNotFoundException est une sous-classe de IOException et ne peut pas être utilisée de manière similaire.

Pour résumer les blocs catch multiples, rappelez-vous qu’au maximum un bloc catch s’exécutera, et ce sera le premier bloc catch qui peut gérer l’exception. De plus, rappelez-vous qu’une exception définie par l’instruction catch n’est dans la portée que pour ce bloc catch. Par exemple, ce qui suit cause une erreur de compilation puisqu’il essaie d’utiliser l’objet exception en dehors du bloc pour lequel il a été défini :

public void visiterLamantins() {
  try {
  } catch (NumberFormatException e1) {
    System.out.println(e1);
  } catch (IllegalArgumentException e2) {
    System.out.println(e1); // NE COMPILE PAS
  }
}

Application d’un bloc multi-catch

Souvent, nous voulons que le résultat d’une exception lancée soit le même, indépendamment de l’exception particulière lancée. Par exemple, examinez cette méthode :

public static void main(String args[]) {
  try {
    System.out.println(Integer.parseInt(args[1]));
  } catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Entrée manquante ou invalide");
  } catch (NumberFormatException e) {
    System.out.println("Entrée manquante ou invalide");
  }
}

Remarquez que nous avons la même instruction println() pour deux blocs catch différents. Nous pouvons gérer cela plus élégamment en utilisant un bloc multi-catch. Un bloc multi-catch permet à plusieurs types d’exceptions d’être attrapés par le même bloc catch. Réécrivons l’exemple précédent en utilisant un bloc multi-catch :

public static void main(String[] args) {
  try {
    System.out.println(Integer.parseInt(args[1]));
  } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) {
    System.out.println("Entrée manquante ou invalide");
  }
}

C’est beaucoup mieux. Il n’y a pas de code en double, la logique commune est regroupée à un seul endroit, et la logique se trouve exactement là où vous vous attendriez à la trouver. Si vous le souhaitez, vous pourriez toujours avoir un second bloc catch pour Exception au cas où vous voudriez gérer d’autres types d’exceptions différemment.

La figure ci-dessous montre la syntaxe du multi-catch. C’est comme une clause catch régulière, sauf que deux ou plusieurs types d’exceptions sont spécifiés, séparés par une barre verticale. La barre verticale (|) est également utilisée comme opérateur “ou”, ce qui facilite la mémorisation que vous pouvez utiliser l’un ou l’autre des types d’exceptions. Notez qu’il n’y a qu’un seul nom de variable dans la clause catch. Java dit que la variable nommée e peut être de type Exception1 ou Exception2.

L’examen pourrait essayer de vous piéger avec une syntaxe invalide. Rappelez-vous que les exceptions peuvent être listées dans n’importe quel ordre dans la clause catch. Cependant, le nom de la variable ne doit apparaître qu’une seule fois et à la fin. Voyez-vous pourquoi ces exemples sont valides ou invalides ?

catch(Exception1 e | Exception2 e | Exception3 e) // NE COMPILE PAS
catch(Exception1 e1 | Exception2 e2 | Exception3 e3) // NE COMPILE PAS
catch(Exception1 | Exception2 | Exception3 e)

La première ligne est incorrecte car le nom de variable apparaît trois fois. Le fait qu’il s’agisse du même nom de variable ne le rend pas correct. La deuxième ligne est incorrecte car le nom de variable apparaît encore trois fois. Utiliser des noms de variables différents ne l’améliore pas. La troisième ligne compile. Elle montre la syntaxe correcte pour spécifier trois exceptions.

Java a l’intention que le multi-catch soit utilisé pour des exceptions qui ne sont pas liées, et il vous empêche de spécifier des types redondants dans un multi-catch. Voyez-vous ce qui ne va pas ici ?

try {
  throw new IOException();
} catch (FileNotFoundException | IOException p) {} // NE COMPILE PAS

Spécifier des exceptions liées dans le multi-catch est redondant, et le compilateur donne un message comme celui-ci :

L’exception FileNotFoundException est déjà attrapée par l’alternative IOException

Puisque FileNotFoundException est une sous-classe de IOException, ce code ne compilera pas. Un bloc multi-catch suit des règles similaires à l’enchaînement de blocs catch, que vous avez vu dans la section précédente. Par exemple, les deux déclenchent des erreurs de compilation lorsqu’ils rencontrent du code inaccessible ou des exceptions en double. La seule différence entre les blocs multi-catch et l’enchaînement de blocs catch est que l’ordre n’a pas d’importance pour un bloc multi-catch au sein d’une seule expression catch.

Pour revenir à l’exemple, le code correct consiste simplement à supprimer la référence de sous-classe superflue, comme indiqué ici :

try {
  throw new IOException();
} catch (IOException e) {}

Ajout d’un bloc finally

L’instruction try vous permet également d’exécuter du code à la fin avec une clause finally, indépendamment du fait qu’une exception soit lancée ou non. La figure ci-dessous montre la syntaxe d’une instruction try avec cette fonctionnalité supplémentaire.

Il y a deux chemins à travers le code avec à la fois un catch et un finally. Si une exception est lancée, le bloc finally est exécuté après le bloc catch. Si aucune exception n’est lancée, le bloc finally est exécuté après que le bloc try soit terminé.

Revenons à notre exemple de petite fille, cette fois avec finally :

void explorer() {
  try {
    voirAnimaux();
    tomber();
  } catch (Exception e) {
    avoirCalinDePapa();
  } finally {
    voirPlusAnimaux();
  }
  rentrerMaison();
}

La fille tombe à la ligne 15. Si elle se relève toute seule, le code passe au bloc finally et exécute la ligne 19. Ensuite, l’instruction try est terminée, et le code continue à la ligne 21. Si la fille ne se relève pas toute seule, elle lance une exception. Le bloc catch s’exécute, et elle reçoit un câlin à la ligne 17. Avec ce câlin, elle est prête à voir plus d’animaux à la ligne 19. Ensuite, l’instruction try est terminée, et le code continue à la ligne 21. Dans les deux cas, la fin est la même. Le bloc finally est exécuté, et l’exécution continue après l’instruction try.

Voyez-vous pourquoi les exemples suivants compilent ou ne compilent pas ?

try { // NE COMPILE PAS
  tomber();
} finally {
  System.out.println("tout va bien");
} catch (Exception e) {
  System.out.println("se lever");
}

try { // NE COMPILE PAS
  tomber();
}

try {
  tomber();
} finally {
  System.out.println("tout va bien");
}

Le premier exemple (lignes 25-31) ne compile pas car les blocs catch et finally sont dans le mauvais ordre. Le deuxième exemple (lignes 33-35) ne compile pas car il doit y avoir un bloc catch ou finally. Le troisième exemple (lignes 37-41) est parfait. Le bloc catch n’est pas requis si finally est présent.

Voici un exemple simple avec finally :

public static void main(String[] unused) {
  StringBuilder sb = new StringBuilder();
  try {
    sb.append("t");
  } catch (Exception e) {
    sb.append("c");
  } finally {
    sb.append("f");
  }
  sb.append("a");
  System.out.print(sb.toString());
}

La réponse est tfa. Le bloc try est exécuté. Puisqu’aucune exception n’est lancée, Java passe directement au bloc finally. Ensuite, le code après l’instruction try est exécuté.

Il y a une règle supplémentaire que vous devriez connaître pour les blocs finally. Si une instruction try avec un bloc finally est entrée, alors le bloc finally sera toujours exécuté, indépendamment du fait que le code se termine avec succès. Regardez la méthode rentrerMaison() suivante. En supposant qu’une exception puisse ou non être lancée à la ligne 14, quelles sont les valeurs possibles que cette méthode pourrait imprimer ? De plus, quelle serait la valeur de retour dans chaque cas ?

int rentrerMaison() {
  try {
    // Lancez éventuellement une exception ici
    System.out.print("1");
    return -1;
  } catch (Exception e) {
    System.out.print("2");
    return -2;
  } finally {
    System.out.print("3");
    return -3;
  }
}

Si une exception n’est pas lancée à la ligne 14, alors la ligne 15 sera exécutée, imprimant 1. Avant que la méthode ne retourne, cependant, le bloc finally est exécuté, imprimant 3. Si une exception est lancée, alors les lignes 15 et 16 seront sautées et les lignes 17-19 seront exécutées, imprimant 2 suivi de 3 du bloc finally. Bien que la première valeur imprimée puisse différer, la méthode imprime toujours 3 en dernier puisqu’il est dans le bloc finally.

Quelle est la valeur de retour de la méthode rentrerMaison() ? Dans ce cas, c’est toujours -3. Parce que le bloc finally est exécuté peu avant que la méthode ne se termine, il interrompt l’instruction return de l’intérieur des blocs try et catch.

Pour l’examen, vous devez vous rappeler qu’un bloc finally sera toujours exécuté. Cela dit, il peut ne pas se terminer avec succès. Regardez l’extrait de code suivant. Que se passerait-il si info était null à la ligne 32 ?

} finally {
  info.afficherDetails();
  System.out.print("Sortie");
  return "zoo";
}

Si info était null, alors le bloc finally serait exécuté, mais il s’arrêterait à la ligne 32 et lancerait une NullPointerException. Les lignes 33 et 34 ne seraient pas exécutées. Dans cet exemple, vous voyez que bien qu’un bloc finally soit toujours exécuté, il peut ne pas se terminer.

System.exit()

Il y a une exception à la règle “le bloc finally sera toujours exécuté” : Java définit une méthode que vous appelez comme System.exit(). Elle prend un paramètre entier qui représente le code de statut qui est retourné.

try {
  System.exit(0);
} finally {
  System.out.print("Jamais atteint"); // Non imprimé
}

System.exit() dit à Java, “Arrête. Termine le programme maintenant”. Lorsque System.exit() est appelé dans le bloc try ou catch, le bloc finally ne s’exécute pas.