J'ai été chargé de maintenir une application écrite il y a quelque temps par des développeurs plus qualifiés. Je suis tombé sur ce morceau de code:
public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
try {
return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
rethrow(e);
}
throw new RuntimeException("cannot reach here");
}
Je suis curieux de savoir si lancer RuntimeException("cannot reach here")
est justifié. Je manque probablement quelque chose d'évident sachant que ce morceau de code vient d'un collègue plus expérimenté.
EDIT: Voici le corps de renvoi auquel certaines réponses ont fait référence. J'ai jugé que ce n'était pas important dans cette question.
private void rethrow(Exception e) throws MailException {
if (e instanceof InvalidDataException) {
InvalidDataException ex = (InvalidDataException) e;
rethrow(ex);
}
if (e instanceof EntityAlreadyExistsException) {
EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
rethrow(ex);
}
if (e instanceof EntityNotFoundException) {
EntityNotFoundException ex = (EntityNotFoundException) e;
rethrow(ex);
}
if (e instanceof NoPermissionException) {
NoPermissionException ex = (NoPermissionException) e;
rethrow(ex);
}
if (e instanceof ServiceUnavailableException) {
ServiceUnavailableException ex = (ServiceUnavailableException) e;
rethrow(ex);
}
LOG.error("internal error, original exception", e);
throw new MailUnexpectedException();
}
private void rethrow(ServiceUnavailableException e) throws
MailServiceUnavailableException {
throw new MailServiceUnavailableException();
}
private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
throw new PersonNotAuthorizedException();
}
private void rethrow(InvalidDataException e) throws
MailInvalidIdException, MailLoginNotAvailableException,
MailInvalidLoginException, MailInvalidPasswordException,
MailInvalidEmailException {
switch (e.getDetail()) {
case ID_INVALID:
throw new MailInvalidIdException();
case LOGIN_INVALID:
throw new MailInvalidLoginException();
case LOGIN_NOT_ALLOWED:
throw new MailLoginNotAvailableException();
case PASSWORD_INVALID:
throw new MailInvalidPasswordException();
case EMAIL_INVALID:
throw new MailInvalidEmailException();
}
}
private void rethrow(EntityAlreadyExistsException e)
throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
switch (e.getDetail()) {
case LOGIN_ALREADY_TAKEN:
throw new MailLoginNotAvailableException();
case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
throw new MailEmailAddressAlreadyForwardedToException();
}
}
private void rethrow(EntityNotFoundException e) throws
MailAccountNotCreatedException,
MailAliasNotCreatedException {
switch (e.getDetail()) {
case ACCOUNT_NOT_FOUND:
throw new MailAccountNotCreatedException();
case ALIAS_NOT_FOUND:
throw new MailAliasNotCreatedException();
}
}
Tout d'abord, merci d'avoir mis à jour votre question et de nous avoir montré ce que fait rethrow
. Donc, en fait, il convertit les exceptions avec des propriétés en classes d'exceptions plus fines. Plus d'informations à ce sujet plus tard.
Comme je n'ai pas vraiment répondu à la question principale à l'origine, ça y est: oui, c'est généralement un mauvais style de lancer des exceptions d'exécution dans du code inaccessible; vous feriez mieux d'utiliser des assertions, ou mieux encore, d'éviter le problème. Comme déjà souligné, le compilateur ici ne peut pas être sûr que le code ne sort jamais du bloc try/catch
. Vous pouvez refactoriser votre code en profitant du fait que ...
(Sans surprise, c'est bien connu dans go )
Prenons un exemple plus simple, celui que j'ai utilisé avant votre édition: alors imaginez que vous enregistrez quelque chose et construisez une exception de wrapper comme dans réponse de Konrad . Appelons cela logAndWrap
.
Au lieu de lever l'exception comme effet secondaire de logAndWrap
, vous pouvez le laisser faire son travail comme effet secondaire et lui faire renvoyer une exception (au moins, celle donnée en entrée). Vous n'avez pas besoin d'utiliser des génériques, juste des fonctions de base:
private Exception logAndWrap(Exception exception) {
// or whatever it actually does
Log.e("Ouch! " + exception.getMessage());
return new CustomWrapperException(exception);
}
Ensuite, vous throw
explicitement, et votre compilateur est content:
try {
return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
throw logAndWrap(e);
}
throw
?Comme expliqué dans commentaire de Joe2 , une façon programmation défensive de s'assurer que l'exception est toujours levée consisterait à faire explicitement une throw new CustomWrapperException(exception)
à la fin de logAndWrap
, comme cela se fait par Guava.Throwables . De cette façon, vous savez que l'exception sera levée et votre analyseur de type est content. Cependant, vos exceptions personnalisées doivent être des exceptions non vérifiées, ce qui n'est pas toujours possible. Je noterais également que le risque qu'un développeur manque d'écrire throw
soit très faible: le développeur doit l'oublier et le la méthode environnante ne doit rien renvoyer, sinon le compilateur détecterait un retour manquant. C'est une façon intéressante de combattre le système de type, et cela fonctionne, cependant.
Le rethrow
peut également être écrit en tant que fonction, mais j'ai des problèmes avec son implémentation actuelle:
Il y a beaucoup de moulages inutiles comme Des moulages sont en effet nécessaires (voir commentaires):
if (e instanceof ServiceUnavailableException) {
ServiceUnavailableException ex = (ServiceUnavailableException) e;
rethrow(ex);
}
Lors du lancement/retour de nouvelles exceptions, l'ancienne est supprimée; dans le code suivant, un MailLoginNotAvailableException
ne me permet pas de savoir quelle connexion n'est pas disponible, ce qui n'est pas pratique; de plus, les traces de pile seront incomplètes:
private void rethrow(EntityAlreadyExistsException e)
throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
switch (e.getDetail()) {
case LOGIN_ALREADY_TAKEN:
throw new MailLoginNotAvailableException();
case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
throw new MailEmailAddressAlreadyForwardedToException();
}
}
Pourquoi le code d'origine ne lance-t-il pas ces exceptions spécialisées en premier lieu? Je soupçonne que rethrow
est utilisé comme couche de compatibilité entre un sous-système (de publipostage) et une logique commerciale (peut-être que l'intention est de masquer les détails d'implémentation comme les exceptions levées en les remplaçant par des exceptions personnalisées). Même si je suis d'accord que il serait préférable d'avoir du code sans capture, comme suggéré dans la réponse de Pete Becker , je ne pense pas que vous aurez l'occasion de supprimer le catch
et rethrow
code ici sans remaniements majeurs.
Cette fonction rethrow(e);
viole le principe qui dit que dans des circonstances normales, une fonction retournera, tandis que dans des circonstances exceptionnelles, une fonction lèvera une exception. Cette fonction viole ce principe en lançant une exception dans des circonstances normales. C'est la source de toute la confusion.
Le compilateur suppose que cette fonction reviendra dans des circonstances normales, de sorte que, pour autant que le compilateur puisse le dire, l'exécution peut atteindre la fin de la fonction retrieveUserMailConfiguration
, auquel cas c'est une erreur de ne pas avoir de return
instruction. Le RuntimeException
jeté là est censé atténuer cette préoccupation du compilateur, mais c'est une façon plutôt maladroite de le faire. Une autre façon de prévenir l'erreur function must return a value
Consiste à ajouter une instruction return null; //to keep the compiler happy
, Mais c'est tout aussi maladroit à mon avis.
Donc, personnellement, je remplacerais ceci:
rethrow(e);
avec ça:
report(e);
throw e;
ou, mieux encore, (comme coredump suggéré ,) avec ceci:
throw reportAndTransform(e);
Ainsi, le flux de contrôle serait rendu évident pour le compilateur, de sorte que votre throw new RuntimeException("cannot reach here");
finale deviendrait non seulement redondante, mais même pas compilable, car elle serait signalée par le compilateur comme du code inaccessible.
C'est le moyen le plus élégant et le plus simple de sortir de cette horrible situation.
throw
a probablement été ajouté pour contourner l'erreur "la méthode doit renvoyer une valeur" qui se produirait autrement - l'analyseur de flux de données Java est suffisamment intelligent pour comprendre qu'aucun return
est nécessaire après une throw
, mais pas après votre méthode rethrow()
personnalisée, et il n'y a pas de @NoReturn
annotation que vous pourriez utiliser pour résoudre ce problème.
Néanmoins, la création d'une nouvelle exception dans du code inaccessible semble superflue. J'écrirais simplement return null
, sachant que cela ne se produit jamais.
Le
throw new RuntimeException("cannot reach here");
indique clairement à une personne lire le code ce qui se passe, il est donc beaucoup mieux que de retourner null par exemple. Cela facilite également le débogage si le code est modifié de manière inattendue.
Cependant rethrow(e)
semble juste faux! Donc dans votre cas je pense que la refactorisation du code est une meilleure option. Voir les autres réponses (j'aime le meilleur de coredump) pour savoir comment trier votre code.
Je ne sais pas s'il y a une convention.
Quoi qu'il en soit, une autre astuce serait de faire comme ça:
private <T> T rethrow(Exception exception) {
// or whatever it actually does
Log.e("Ouch! " + exception.getMessage());
throw new CustomWrapperException(exception);
}
Permettant cela:
try {
return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
return rethrow(e);
}
Et aucun RuntimeException
artificiel n'est plus nécessaire. Même si rethrow
ne retourne jamais de valeur, c'est suffisant pour le compilateur maintenant.
Il renvoie une valeur en théorie (signature de méthode), puis il est exempté de le faire car il lève une exception à la place.
Oui, cela peut sembler bizarre, mais encore une fois - lancer un fantôme RuntimeException
, ou retourner des valeurs nulles qui ne seront jamais vues dans ce monde - ce n'est pas exactement une chose de beauté non plus.
Dans un souci de lisibilité, vous pouvez renommer rethrow
et avoir quelque chose comme:
} catch (Exception e) {
return nothingJustRethrow(e);
}
Si vous supprimez complètement le bloc try
, vous n'avez pas besoin du rethrow
ou du throw
. Ce code fait exactement la même chose que l'original:
public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
return translate(mailManagementService.retrieveUserMailConfiguration(id));
}
Ne vous laissez pas tromper par le fait qu'il provient de développeurs plus expérimentés. C'est de la pourriture de code, et ça arrive tout le temps. Il suffit de le réparer.
EDIT: J'ai mal interprété rethrow(e)
comme relançant simplement l'exception e
. Si cette méthode rethrow
fait réellement autre chose que de relancer l'exception, la suppression de cela modifie la sémantique de cette méthode.