Gestion des exceptions en Java

Gestion des exceptions en Java

1. Vue d'ensemble

Dans ce didacticiel, nous allons passer en revue les bases de la gestion des exceptions en Java ainsi que certains de ses pièges.

2. Premiers principes

2.1. Qu'Est-ce que c'est?

Pour mieux comprendre les exceptions et la gestion des exceptions, faisons une comparaison réelle.

Imaginez que nous commandions un produit en ligne, mais pendant le trajet, il y a un échec de livraison. Une bonne entreprise peut gérer ce problème et réacheminer gracieusement notre colis afin qu'il arrive toujours à l'heure.

De même, en Java, le code peut rencontrer des erreurs lors de l'exécution de nos instructions. Bonexception handling peut gérer les erreurs et réacheminer le programme avec élégance pour donner à l'utilisateur une expérience toujours positive

2.2. Pourquoi l'utiliser?

Nous écrivons généralement du code dans un environnement idéalisé: le système de fichiers contient toujours nos fichiers, le réseau est en bon état et la JVM dispose toujours de suffisamment de mémoire. Parfois, nous appelons cela le «chemin heureux».

En production, cependant, les systèmes de fichiers peuvent être corrompus, les réseaux s’effondrer et les machines virtuelles Java manquent de mémoire. Le bien-être de notre code dépend de la façon dont il traite les «chemins malheureux».

Nous devons gérer ces conditions car elles affectent négativement le flux de l'application et formentexceptions:

public static List getPlayers() throws IOException {
    Path path = Paths.get("players.dat");
    List players = Files.readAllLines(path);

    return players.stream()
      .map(Player::new)
      .collect(Collectors.toList());
}

Ce code choisit de ne pas gérer lesIOException, le passant à la place dans la pile d'appels. Dans un environnement idéalisé, le code fonctionne bien.

Mais que pourrait-il se passer en production siplayers.dat est manquant?

Exception in thread "main" java.nio.file.NoSuchFileException: players.dat <-- players.dat file doesn't exist
    at sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
    at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
    // ... more stack trace
    at java.nio.file.Files.readAllLines(Unknown Source)
    at java.nio.file.Files.readAllLines(Unknown Source)
    at Exceptions.getPlayers(Exceptions.java:12) <-- Exception arises in getPlayers() method, on line 12
    at Exceptions.main(Exceptions.java:19) <-- getPlayers() is called by main(), on line 19

Without handling this exception, an otherwise healthy program may stop running altogether! Nous devons nous assurer que notre code a un plan en cas de problème.

Notez également un avantage supplémentaire aux exceptions, à savoir la trace de la pile elle-même. En raison de cette trace de pile, nous pouvons souvent identifier le code incriminé sans avoir besoin d'attacher un débogueur.

3. Hiérarchie des exceptions

En fin de compte,exceptions  ne sont que des objets Java, tous s'étendant à partir deThrowable:

              ---> Throwable <---
              |    (checked)     |
              |                  |
              |                  |
      ---> Exception           Error
      |    (checked)        (unchecked)
      |
RuntimeException
  (unchecked)

Il existe trois grandes catégories de conditions exceptionnelles:

  • Exceptions vérifiées

  • Exceptions non contrôlées / Exceptions d'exécution

  • les erreurs

Les exceptions d'exécution et non vérifiées font référence à la même chose. Nous pouvons souvent les utiliser de façon interchangeable.

3.1. Checked Exceptions

Les exceptions cochées sont des exceptions que le compilateur Java nous demande de gérer. Nous devons soit lancer de manière déclarative l'exception dans la pile d'appels, soit le gérer nous-mêmes. Plus sur ces deux dans un moment.

Oracle’s documentation nous dit d'utiliser des exceptions vérifiées lorsque nous pouvons raisonnablement nous attendre à ce que l'appelant de notre méthode puisse récupérer.

Quelques exemples d'exceptions vérifiées sontIOException etServletException.

3.2. Exceptions non contrôlées

Les exceptions non cochées sont des exceptions que le compilateur Java nous oblige à gérer.

En termes simples, si nous créons une exception qui étendRuntimeException, elle sera décochée; sinon, il sera vérifié.

Et bien que cela semble pratique,Oracle’s documentation nous dit qu'il y a de bonnes raisons pour les deux concepts, comme la distinction entre une erreur de situation (cochée) et une erreur d'utilisation (non cochée).

Quelques exemples d'exceptions non vérifiées sontNullPointerException, IllegalArgumentException, andSecurityException.

3.3. les erreurs

Les erreurs représentent des conditions sérieuses et généralement irrécupérables telles qu'une incompatibilité de bibliothèque, une récursion infinie ou des fuites de mémoire.

Et même s’ils n’étendent pasRuntimeException, ils sont également décochés.

Dans la plupart des cas, il serait étrange pour nous de gérer, instancier ou étendreErrors. Habituellement, nous voulons que ceux-ci se propagent à la hausse.

Quelques exemples d'erreurs sont unStackOverflowError etOutOfMemoryError.

4. Gestion des exceptions

Dans l'API Java, il y a beaucoup d'endroits où les choses peuvent mal tourner, et certains de ces endroits sont marqués d'exceptions, que ce soit dans la signature ou dans le Javadoc:

/**
 * @exception FileNotFoundException ...
 */
public Scanner(String fileName) throws FileNotFoundException {
   // ...
}

Comme indiqué un peu plus tôt, lorsque nous appelons ces méthodes «risquées», nousmust traitons les exceptions vérifiées, et nousmay traitons celles qui ne sont pas vérifiées. Java nous donne plusieurs façons de le faire:

4.1. throws

Le moyen le plus simple de “gérer” une exception est de la réexaminer:

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {

    Scanner contents = new Scanner(new File(playerFile));
    return Integer.parseInt(contents.nextLine());
}

Étant donné queFileNotFoundException  est une exception vérifiée, c'est le moyen le plus simple de satisfaire le compilateur, maisit does mean that anyone that calls our method now needs to handle it too!

parseInt peut lancer unNumberFormatException, mais comme il n'est pas coché, nous ne sommes pas obligés de le gérer.

4.2. try -catch

Si nous voulons essayer de gérer l'exception nous-mêmes, nous pouvons utiliser un bloctry-catch. Nous pouvons y faire face en réintégrant notre exception:

public int getPlayerScore(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile) {
        throw new IllegalArgumentException("File not found");
    }
}

Ou en effectuant les étapes de récupération:

public int getPlayerScore(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch ( FileNotFoundException noFile ) {
        logger.warn("File not found, resetting score.");
        return 0;
    }
}

4.3. finally

Maintenant, il y a des moments où nous avons du code qui doit s'exécuter indépendamment du fait qu'une exception se produise, et c'est là que le mot cléfinally entre en jeu.

Dans nos exemples jusqu'à présent, il y a eu un bogue méchant qui se cache dans l'ombre, à savoir que Java par défaut ne renvoie pas les descripteurs de fichiers au système d'exploitation.

Certes, que nous puissions lire le fichier ou non, nous voulons nous assurer que nous effectuons le nettoyage approprié!

Essayons d'abord la méthode "paresseuse":

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {
    Scanner contents = null;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } finally {
        if (contents != null) {
            contents.close();
        }
    }
}

Ici, le blocfinally indique le code que nous voulons que Java exécute indépendamment de ce qui se passe en essayant de lire le fichier.

Même si unFileNotFoundException est lancé dans la pile d'appels, Java appellera le contenu definally avant de faire cela.

Nous pouvons également tous les deux gérer l'exceptionand assurez-vous que nos ressources sont fermées:

public int getPlayerScore(String playerFile) {
    Scanner contents;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile ) {
        logger.warn("File not found, resetting score.");
        return 0;
    } finally {
        try {
            if (contents != null) {
                contents.close();
            }
        } catch (IOException io) {
            logger.error("Couldn't close the reader!", io);
        }
    }
}

Parce queclose est aussi une méthode «risquée», nous devons également attraper son exception!

Cela peut paraître assez compliqué, mais nous avons besoin de chaque pièce pour traiter chaque problème potentiel qui peut survenir correctement.

4.4. try-avec-ressources

Heureusement, à partir de Java 7, nous pouvons simplifier la syntaxe ci-dessus lorsque vous travaillez avec des éléments qui étendentAutoCloseable:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
      return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e ) {
      logger.warn("File not found, resetting score.");
      return 0;
    }
}

Lorsque nous plaçons des références qui sontAutoClosable dans la déclarationtry , nous n’avons pas besoin de fermer la ressource nous-mêmes.

Cependant, nous pouvons toujours utiliser un blocfinally pour faire tout autre type de nettoyage souhaité.

Consultez notre article dédié auxtry-with-resources pour en savoir plus.

4.5. Plusieurs blocscatch

Parfois, le code peut lever plus d'une exception, et nous pouvons avoir plus d'un bloccatch gérer chacun individuellement:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (IOException e) {
        logger.warn("Player file wouldn't load!", e);
        return 0;
    } catch (NumberFormatException e) {
        logger.warn("Player file was corrupted!", e);
        return 0;
    }
}

Les captures multiples nous donnent la possibilité de gérer chaque exception différemment, le cas échéant.

Notez également ici que nous n’avons pas attrapéFileNotFoundException, et c’est parce qu’ilextends IOException. Parce que nous attraponsIOException, Java considérera n'importe laquelle de ses sous-classes également gérée.

Disons, cependant, que nous devons traiterFileNotFoundException  différemment desIOException plus généraux:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile)) ) {
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e) {
        logger.warn("Player file not found!", e);
        return 0;
    } catch (IOException e) {
        logger.warn("Player file wouldn't load!", e);
        return 0;
    } catch (NumberFormatException e) {
        logger.warn("Player file was corrupted!", e);
        return 0;
    }
}

Java nous permet de gérer les exceptions de sous-classes séparément,remember to place them higher in the list of catches.

4.6. Blocs Unioncatch

Lorsque nous savons que nous allons gérer les erreurs de la même manière, Java 7 a introduit la possibilité d’attraper plusieurs exceptions dans le même bloc:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (IOException | NumberFormatException e) {
        logger.warn("Failed to load score!", e);
        return 0;
    }
}

5. Lancer des exceptions

Si nous ne voulons pas gérer l'exception nous-mêmes ou si nous voulons générer nos exceptions pour que d'autres les gèrent, nous devons nous familiariser avec le mot cléthrow.

Supposons que nous ayons l'exception cochée suivante que nous avons créée nous-mêmes:

public class TimeoutException extends Exception {
    public TimeoutException(String message) {
        super(message);
    }
}

et nous avons une méthode qui pourrait prendre un certain temps:

public List loadAllPlayers(String playersFile) {
    // ... potentially long operation
}

5.1. Lancer une exception vérifiée

Comme pour revenir d'une méthode, nous pouvonsthrow at à n'importe quel point.

Bien sûr, nous devrions jeter lorsque nous essayons d'indiquer que quelque chose s'est mal passé:

public List loadAllPlayers(String playersFile) throws TimeoutException {
    while ( !tooLong ) {
        // ... potentially long operation
    }
    throw new TimeoutException("This operation took too long");
}

PuisqueTimeoutException est coché, nous devons également utiliser le mot-cléthrows dans la signature pour que les appelants de notre méthode sachent comment le gérer.

5.2. Throwchante une exception non vérifiée

Si nous voulons faire quelque chose comme, par exemple, valider une entrée, nous pouvons utiliser une exception non vérifiée:

public List loadAllPlayers(String playersFile) throws TimeoutException {
    if(!isFilenameValid(playersFile)) {
        throw new IllegalArgumentException("Filename isn't valid!");
    }

    // ...
}

PuisqueIllegalArgumentException n'est pas coché, nous n'avons pas à marquer la méthode, bien que nous soyons les bienvenus.

Certains marquent quand même la méthode comme une forme de documentation.

5.3. Emballage et relance

Nous pouvons également choisir de renvoyer une exception que nous avons détectée:

public List loadAllPlayers(String playersFile)
  throws IOException {
    try {
        // ...
    } catch (IOException io) {
        throw io;
    }
}

Ou faites un tour et rehrow:

public List loadAllPlayers(String playersFile)
  throws PlayerLoadException {
    try {
        // ...
    } catch (IOException io) {
        throw new PlayerLoadException(io);
    }
}

Cela peut être utile pour consolider de nombreuses exceptions différentes en une seule.

5.4. RelancerThrowable ouException

Maintenant pour un cas particulier.

Si les seules exceptions possibles qu'un bloc de code donné pourrait soulever sont des exceptionsunchecked, alors nous pouvons attraper et renvoyerThrowable ouException ans les ajouter à notre signature de méthode:

public List loadAllPlayers(String playersFile) {
    try {
        throw new NullPointerException();
    } catch (Throwable t) {
        throw t;
    }
}

Bien que simple, le code ci-dessus ne peut pas lancer d'exception vérifiée et à cause de cela, même si nous renvoyons une exception vérifiée, nous n'avons pas à marquer la signature avec une clausethrows .

This is handy with proxy classes and methods. Plus d'informations à ce sujet peuvent être trouvéeshere.

5.5. Inheritance

Lorsque nous marquons des méthodes avec un mot cléthrows, cela affecte la façon dont les sous-classes peuvent remplacer notre méthode.

Dans les cas où notre méthode lève une exception vérifiée:

public class Exceptions {
    public List loadAllPlayers(String playersFile)
      throws TimeoutException {
        // ...
    }
}

Une sous-classe peut avoir une signature «moins risquée»:

public class FewerExceptions extends Exceptions {
    @Override
    public List loadAllPlayers(String playersFile) {
        // overridden
    }
}

Mais pas une signature «more riskier»:

public class MoreExceptions extends Exceptions {
    @Override
    public List loadAllPlayers(String playersFile) throws MyCheckedException {
        // overridden
    }
}

En effet, les contrats sont déterminés lors de la compilation par le type de référence. Si je crée une instance deMoreExceptions and, enregistrez-la dansExceptions:

Exceptions exceptions = new MoreExceptions();
exceptions.loadAllPlayers("file");

Ensuite, la JVM me dira seulement àcatch lesTimeoutException, ce qui est faux car j'ai dit queMoreExceptions#loadAllPlayers lève une exception différente.

En termes simples, les sous-classes peuvent lancer des exceptions vérifiéesfewer par rapport à leur superclasse, mais pasmore.

6. Anti-motifs

6.1. Avaler des exceptions

Maintenant, il y a une autre manière de satisfaire le compilateur:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (Exception e) {} // <== catch and swallow
    return 0;
}

The above is calledswallowing an exception. La plupart du temps, ce serait un peu méchant pour nous de le faire car cela ne résout pas le problèmeand, cela empêche également d'autres codes de résoudre le problème.

Il y a des moments où il y a une exception vérifiée dont nous sommes convaincus qu'elle ne se produira tout simplement jamais. In those cases, we should still at least add a comment stating that we intentionally ate the exception:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        // this will never happen
    }
}

Une autre façon d’éviter une exception est d’imprimer l’exception au flux d’erreurs simplement:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0;
}

Nous avons un peu amélioré notre situation en écrivant au moins l'erreur quelque part pour un diagnostic ultérieur.

Il vaudrait cependant mieux que nous utilisions un enregistreur:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        logger.error("Couldn't load the score", e);
        return 0;
    }
}

Bien qu'il soit très pratique pour nous de gérer les exceptions de cette manière, nous devons nous assurer de ne pas avaler des informations importantes que les appelants de notre code pourraient utiliser pour résoudre le problème.

Enfin, nous pouvons par inadvertance avaler une exception en ne l'incluant pas comme cause lorsque nous lançons une nouvelle exception:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        throw new PlayerScoreException();
    }
}

Ici, nous nous félicitons d'avoir alerté notre appelant sur une erreur, maiswe fail to include the IOException as the cause. Pour cette raison, nous avons perdu des informations importantes que les appelants ou les opérateurs pourraient utiliser pour diagnostiquer le problème.

Nous ferions mieux de faire:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        throw new PlayerScoreException(e);
    }
}

Remarquez la différence subtile d'inclureIOException en tant quecause dePlayerScoreException.

6.2. Utilisation dereturn dans un blocfinally

Une autre façon d'avaler les exceptions consiste à utiliserreturn du blocfinally. Cela est grave car, en revenant brusquement, la machine virtuelle supprimera l'exception, même si elle a été rejetée par notre code:

public int getPlayerScore(String playerFile) {
    int score = 0;
    try {
        throw new IOException();
    } finally {
        return score; // <== the IOException is dropped
    }
}

Si l'exécution du bloc try se termine brusquement pour une autre raisonR, alors le bloc finally est exécuté, puis il y a un choix.

Si le blocfinally se termine normalement, alors l'instruction try se termine brusquement pour la raison R.

Si le blocfinally se termine brusquement pour la raison S, alors l'instruction try se termine brusquement pour la raison S (et la raison R est ignorée).

6.3. Utilisation dethrow dans un blocfinally

Similaire à l'utilisation dereturn dans un blocfinally, l'exception lancée dans un blocfinally aura priorité sur l'exception qui survient dans le bloc catch.

Cela «effacera» l'exception d'origine du bloctry, et nous perdrons toutes ces informations précieuses:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch ( IOException io ) {
        throw new IllegalStateException(io); // <== eaten by the finally
    } finally {
        throw new OtherException();
    }
}

6.4. Utilisation dethrow commegoto

Certaines personnes ont également cédé à la tentation d'utiliserthrow comme une instructiongoto:

public void doSomething() {
    try {
        // bunch of code
        throw new MyException();
        // second bunch of code
    } catch (MyException e) {
        // third bunch of code
    }
}

Cela est étrange car le code tente d'utiliser des exceptions pour le contrôle de flux plutôt que pour la gestion des erreurs.

7. Exceptions et erreurs courantes

Voici quelques exceptions et erreurs courantes que nous rencontrons tous de temps en temps:

7.1. Exceptions vérifiées

  • IOException - Cette exception est généralement un moyen de dire que quelque chose sur le réseau, le système de fichiers ou la base de données a échoué.

7.2. Exceptions d'exécution

  • ArrayIndexOutOfBoundsException - cette exception signifie que nous avons essayé d'accéder à un index de tableau inexistant, comme lorsque nous essayons d'obtenir l'index 5 à partir d'un tableau de longueur 3.

  • ClassCastException – cette exception signifie que nous avons essayé d'effectuer une conversion illégale, comme essayer de convertir unString en unList. Nous pouvons généralement l'éviter en effectuant des schecks défensifsinstanceof avant de lancer.

  • IllegalArgumentException - cette exception est un moyen générique pour nous de dire que l'un des paramètres de méthode ou de constructeur fournis n'est pas valide.

  • IllegalStateException - Cette exception est une manière générique pour nous de dire que notre état interne, comme l'état de notre objet, est invalide.

  • NullPointerException - Cette exception signifie que nous avons essayé de référencer un objetnull. Nous pouvons généralement l'éviter en effectuant des contrôles défensifs denull ou en utilisantOptional.

  • NumberFormatException - Cette exception signifie que nous avons essayé de convertir unString en nombre, mais la chaîne contenait des caractères illégaux, comme essayer de convertir «5f3» en nombre.

7.3. les erreurs

  • StackOverflowError – cette exception signifie que la trace de pile est trop grande. Cela peut parfois arriver dans des applications massives; Cependant, cela signifie généralement que notre code contient une récursion infinie.

  • NoClassDefFoundError - cette exception signifie qu'une classe n'a pas pu se charger soit parce qu'elle ne se trouvait pas sur le chemin de classe, soit en raison d'un échec de l'initialisation statique.

  • OutOfMemoryError - cette exception signifie que la machine virtuelle Java n’a plus de mémoire disponible à allouer pour plus d’objets. Parfois, cela est dû à une fuite de mémoire.

8. Conclusion

Dans cet article, nous avons passé en revue les bases de la gestion des exceptions ainsi que quelques exemples de bonnes et de mauvaises pratiques.

Comme toujours, tout le code trouvé dans cet article peut être trouvéover on GitHub!