Je pense que vous avez probablement vécu une situation où vous exécutez du code et vous retrouvez avec quelque chose comme NullPointerException , ClassCastException , ou pire... Ceci est suivi d'un long processus de débogage, d'analyse, de recherche sur Google, etc. Les exceptions sont merveilleuses telles quelles : elles indiquent la nature du problème et où il s'est produit. Si vous voulez vous rafraîchir la mémoire et en savoir un peu plus, jetez un œil à cet article : Exceptions : cochées, décochées et personnalisées .

Cela dit, il peut y avoir des situations où vous devez créer votre propre exception. Par exemple, supposons que votre code doive demander des informations à un service distant qui n'est pas disponible pour une raison quelconque. Ou supposons que quelqu'un remplisse une demande de carte bancaire et fournisse un numéro de téléphone qui, accidentellement ou non, est déjà associé à un autre utilisateur dans le système.

Bien sûr, le comportement correct ici dépend toujours des exigences du client et de l'architecture du système, mais supposons que vous ayez été chargé de vérifier si le numéro de téléphone est déjà utilisé et de lever une exception si c'est le cas.

Créons une exception :


public class PhoneNumberAlreadyExistsException extends Exception {

   public PhoneNumberAlreadyExistsException(String message) {
       super(message);
   }
}
    

Ensuite, nous l'utiliserons lorsque nous effectuerons notre vérification :


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

Pour simplifier notre exemple, nous utiliserons plusieurs numéros de téléphone codés en dur pour représenter une base de données. Et enfin, essayons d'utiliser notre exception :


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

Et maintenant, il est temps d'appuyer sur Maj+F10 (si vous utilisez IDEA), c'est-à-dire d'exécuter le projet. Voici ce que vous verrez dans la console :

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException : le numéro de téléphone spécifié est déjà utilisé par un autre client !
à exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Regarde toi! Vous avez créé votre propre exception et l'avez même un peu testée. Félicitations pour cette réalisation ! Je recommande d'expérimenter un peu le code pour mieux comprendre comment cela fonctionne.

Ajoutez une autre vérification - par exemple, vérifiez si le numéro de téléphone comprend des lettres. Comme vous le savez probablement, les lettres sont souvent utilisées aux États-Unis pour faciliter la mémorisation des numéros de téléphone, par exemple 1-800-MY-APPLE. Votre vérification pourrait vous assurer que le numéro de téléphone ne contient que des chiffres.

D'accord, nous avons donc créé une exception vérifiée. Tout irait bien et bien, mais...

La communauté de programmation est divisée en deux camps - ceux qui sont en faveur des exceptions vérifiées et ceux qui s'y opposent. Les deux parties présentent des arguments solides. Les deux incluent des développeurs de premier ordre : Bruce Eckel critique les exceptions vérifiées, tandis que James Gosling les défend. Il semble que cette affaire ne sera jamais réglée définitivement. Cela dit, examinons les principaux inconvénients de l'utilisation des exceptions vérifiées.

Le principal inconvénient des exceptions cochées est qu'elles doivent être gérées. Et ici, nous avons deux options : soit le gérer sur place à l'aide d'un try-catch , soit, si nous utilisons la même exception à plusieurs endroits, utiliser throws pour lever les exceptions et les traiter dans des classes de niveau supérieur.

De plus, nous pouvons nous retrouver avec du code « passe-partout », c'est-à-dire un code qui prend beaucoup de place, mais qui ne fait pas beaucoup de travail lourd.

Des problèmes apparaissent dans des applications assez volumineuses avec un grand nombre d'exceptions gérées : la liste de lancements d'une méthode de niveau supérieur peut facilement s'étendre pour inclure une douzaine d'exceptions.

public OurCoolClass() lève FirstException, SecondException, ThirdException, ApplicationNameException...

Les développeurs n'aiment généralement pas cela et optent plutôt pour une astuce : ils font en sorte que toutes leurs exceptions vérifiées héritent d'un ancêtre commun - ApplicationNameException . Maintenant, ils doivent également intercepter cette exception ( cochée !) Dans un gestionnaire :


catch (FirstException e) {
    // TODO
}
catch (SecondException e) {
    // TODO
}
catch (ThirdException e) {
    // TODO
}
catch (ApplicationNameException e) {
    // TODO
}
    

Ici, nous sommes confrontés à un autre problème : que devons-nous faire dans le dernier bloc catch ? Ci-dessus, nous avons déjà traité toutes les situations attendues, donc à ce stade ApplicationNameException ne signifie rien de plus pour nous que " Exception : une erreur incompréhensible s'est produite". Voici comment nous le gérons :


catch (ApplicationNameException e) {
    LOGGER.error("Unknown error", e.getMessage());
}
    

Et au final, on ne sait pas ce qui s'est passé.

Mais ne pourrions-nous pas lever toutes les exceptions d'un coup, comme ça ?


public void ourCoolMethod() throws Exception {
// Do some work
}
    

Oui, nous pourrions. Mais que nous dit "lance une exception" ? Que quelque chose est cassé. Vous devrez enquêter sur tout de haut en bas et vous familiariser avec le débogueur pendant longtemps pour comprendre la raison.

Vous pouvez également rencontrer une construction parfois appelée « déglutition d'exception » :


try {
// Some code
} catch(Exception e) {
   throw new ApplicationNameException("Error");
}
    

Il n'y a pas grand-chose à ajouter ici à titre d'explication — le code rend tout clair, ou plutôt, il rend tout peu clair.

Bien sûr, vous pouvez dire que vous ne verrez pas cela dans le code réel. Eh bien, examinons les entrailles (le code) de la classe URL du package java.net . Suivez-moi si vous voulez savoir !

Voici l'une des constructions de la classe URL :


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

Comme vous pouvez le voir, nous avons une exception vérifiée intéressante — MalformedURLException . Voici quand il peut être lancé (et je cite):
"si aucun protocole n'est spécifié, ou un protocole inconnu est trouvé, ou spec est nul, ou l'URL analysée ne respecte pas la syntaxe spécifique du protocole associé."

C'est-à-dire:

  1. Si aucun protocole n'est spécifié.
  2. Un protocole inconnu est trouvé.
  3. La spécification est null .
  4. L'URL ne respecte pas la syntaxe spécifique du protocole associé.

Créons une méthode qui crée un objet URL :


public URL createURL() {
   URL url = new URL("https://codegym.cc");
   return url;
}
    

Dès que vous écrivez ces lignes dans l'IDE (je code dans IDEA, mais cela fonctionne même dans Eclipse et NetBeans), vous verrez ceci :

Cela signifie que nous devons soit lancer une exception, soit envelopper le code dans un bloc try-catch . Pour l'instant, je suggère de choisir la deuxième option pour visualiser ce qui se passe :


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://codegym.cc");
   } catch(MalformedURLException e) {
  e.printStackTrace();
   }
   return url;
}
    

Comme vous pouvez le voir, le code est déjà assez verbeux. Et nous y avons fait allusion plus haut. C'est l'une des raisons les plus évidentes d'utiliser des exceptions non contrôlées.

Nous pouvons créer une exception non contrôlée en étendant RuntimeException en Java.

Les exceptions non vérifiées sont héritées de la classe Error ou de la classe RuntimeException . De nombreux programmeurs pensent que ces exceptions ne peuvent pas être gérées dans nos programmes car elles représentent des erreurs dont nous ne pouvons pas nous attendre à récupérer pendant que le programme est en cours d'exécution.

Lorsqu'une exception non vérifiée se produit, elle est généralement causée par une utilisation incorrecte du code, en passant un argument nul ou non valide.

Eh bien, écrivons le code :


public class OurCoolUncheckedException extends RuntimeException {
   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }
  
   public OurCoolUncheckedException(String message, Throwable throwable) {
       super(message, throwable);
   }
}
    

Notez que nous avons créé plusieurs constructeurs à des fins différentes. Cela nous permet de donner plus de capacités à notre exception. Par exemple, nous pouvons faire en sorte qu'une exception nous donne un code d'erreur. Pour commencer, faisons une énumération pour représenter nos codes d'erreur :


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

Ajoutons maintenant un autre constructeur à notre classe d'exception :


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

Et n'oublions pas d'ajouter un champ (on a failli oublier) :


private Integer errorCode;
    

Et bien sûr, une méthode pour obtenir ce code :


public Integer getErrorCode() {
   return errorCode;
}
    

Regardons l'ensemble de la classe afin de pouvoir la vérifier et comparer :

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

Ta-da ! Notre exception est terminée ! Comme vous pouvez le voir, il n'y a rien de particulièrement compliqué ici. Voyons-le en action :


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

Lorsque nous exécuterons notre petite application, nous verrons quelque chose comme ceci dans la console :

Profitons maintenant des fonctionnalités supplémentaires que nous avons ajoutées. Nous allons ajouter un peu au code précédent :


public static void main(String[] args) throws Exception {

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode) {
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

Vous pouvez travailler avec des exceptions de la même manière que vous travaillez avec des objets. Bien sûr, je suis sûr que vous savez déjà que tout en Java est un objet.

Et regardez ce que nous avons fait. Tout d'abord, nous avons changé la méthode, qui maintenant ne lance pas, mais crée simplement une exception, en fonction du paramètre d'entrée. Ensuite, à l'aide d'une instruction switch-case , nous générons une exception avec le code d'erreur et le message souhaités. Et dans la méthode principale, nous obtenons l'exception créée, obtenons le code d'erreur et le lançons.

Exécutons ceci et voyons ce que nous obtenons sur la console :

Regardez - nous avons imprimé le code d'erreur que nous avons obtenu de l'exception, puis avons lancé l'exception elle-même. De plus, nous pouvons même suivre exactement où l'exception a été levée. Si nécessaire, vous pouvez ajouter toutes les informations pertinentes au message, créer des codes d'erreur supplémentaires et ajouter de nouvelles fonctionnalités à vos exceptions.

Eh bien, qu'en pensez-vous? J'espère que tout s'est bien passé pour toi !

En général, les exceptions sont un sujet assez vaste et pas clair. Il y aura bien d'autres disputes à son sujet. Par exemple, seul Java a vérifié les exceptions. Parmi les langues les plus populaires, je n'en ai pas vu une qui les utilise.

Bruce Eckel a très bien écrit sur les exceptions dans le chapitre 12 de son livre "Thinking in Java" — je vous recommande de le lire ! Jetez également un coup d'œil au premier volume de "Core Java" de Horstmann - Il contient également beaucoup de choses intéressantes au chapitre 7.

Un petit résumé

  1. Écrivez tout dans un journal! Journaliser les messages dans les exceptions levées. Cela aidera généralement beaucoup au débogage et vous permettra de comprendre ce qui s'est passé. Ne laissez pas un bloc catch vide, sinon il "avalera" simplement l'exception et vous n'aurez aucune information pour vous aider à traquer les problèmes.

  2. Quand il s'agit d'exceptions, c'est une mauvaise pratique de les attraper toutes en même temps (comme l'a dit un de mes collègues, "ce n'est pas Pokemon, c'est Java"), alors évitez catch (Exception e) ou pire, catch ( Throwable t ) .

  3. Levez les exceptions le plus tôt possible. C'est une bonne pratique de programmation Java. Lorsque vous étudiez des frameworks comme Spring, vous verrez qu'ils suivent le principe "fail fast". C'est-à-dire qu'ils "échouent" le plus tôt possible afin de permettre de trouver rapidement l'erreur. Bien sûr, cela apporte certains inconvénients. Mais cette approche permet de créer un code plus robuste.

  4. Lors de l'appel d'autres parties du code, il est préférable d'intercepter certaines exceptions. Si le code appelé lève plusieurs exceptions, c'est une mauvaise pratique de programmation de n'attraper que la classe parent de ces exceptions. Par exemple, supposons que vous appelez du code qui lève FileNotFoundException et IOException . Dans votre code qui appelle ce module, il est préférable d'écrire deux blocs catch pour intercepter chacune des exceptions, au lieu d'un seul catch pour intercepter Exception .

  5. Interceptez les exceptions uniquement lorsque vous pouvez les gérer efficacement pour les utilisateurs et pour le débogage.

  6. N'hésitez pas à écrire vos propres exceptions. Bien sûr, Java en a beaucoup de prêts à l'emploi, quelque chose pour chaque occasion, mais parfois vous devez encore inventer votre propre "roue". Mais vous devez clairement comprendre pourquoi vous faites cela et vous assurer que l'ensemble standard d'exceptions n'a pas déjà ce dont vous avez besoin.

  7. Lorsque vous créez vos propres classes d'exception, faites attention au nommage ! Vous savez probablement déjà qu'il est extrêmement important de nommer correctement les classes, les variables, les méthodes et les packages. Les exceptions ne font pas exception ! :) Terminez toujours par le mot Exception , et le nom de l'exception doit clairement indiquer le type d'erreur qu'il représente. Par exemple, FileNotFoundException .

  8. Documentez vos exceptions. Nous vous recommandons d'écrire une balise Javadoc @throws pour les exceptions. Cela sera particulièrement utile lorsque votre code fournit des interfaces de toutes sortes. Et vous trouverez également plus facile de comprendre votre propre code plus tard. Que pensez-vous, comment pouvez-vous déterminer de quoi parle MalformedURLException ? De Javadoc! Oui, l'idée d'écrire de la documentation n'est pas très attrayante, mais croyez-moi, vous vous remercierez lorsque vous reviendrez à votre propre code six mois plus tard.

  9. Libérez des ressources et ne négligez pas la construction try-with-resources .

  10. Voici le résumé général : utilisez les exceptions à bon escient. Lancer une exception est une opération assez "coûteuse" en termes de ressources. Dans de nombreux cas, il peut être plus facile d'éviter de lancer des exceptions et de renvoyer, par exemple, une variable booléenne indiquant si l'opération a réussi, en utilisant un if- else simple et "moins coûteux" .

    Il peut également être tentant de lier la logique de l'application à des exceptions, ce que vous ne devriez clairement pas faire. Comme nous l'avons dit au début de l'article, les exceptions sont pour des situations exceptionnelles, non prévues, et il existe divers outils pour les prévenir. En particulier, il y a Facultatif pour empêcher une NullPointerException , ou Scanner.hasNext et autres pour empêcher une IOException , que la méthode read() peut lancer.