Programmation orientée objet · C4 Robustesse et modélisation · Chapitre 1 · 5 h
Exceptions
Erreurs et exceptions, try, catch, finally, propagation et throws, exceptions personnalisées, et quand lever plutôt que rendre un code d'erreur.
Comment une méthode signale-t-elle qu'elle n'a pas pu faire son travail ?
La réponse du C était le code de retour : -1 en cas d'échec, et un appelant discipliné qui
le teste. Le cours de programmation a montré ce que cela coûte — scanf rend le nombre de
conversions réussies, et personne ne le regarde jamais.
int retirer(double montant) { if (montant > solde) return -1; /* échec */ solde -= montant; return 0;} compte.retirer(1000); /* et voilà. Personne n'a rien vérifié. */Quatre défauts, indépendants les uns des autres. Le code d'erreur peut être ignoré en toute
impunité — c'est le cas ci-dessus. Il occupe la valeur de retour, donc une méthode qui doit
rendre un résultat utile n'a plus de place pour signaler l'échec. Il ne porte aucune
information : -1 ne dit pas combien il manquait. Et il doit être propagé à la main à
travers chaque niveau d'appel, chacun devant le tester et le retransmettre.
Les exceptions règlent les quatre.
Le mécanisme
public void retirer(double montant) { if (montant <= 0) { throw new IllegalArgumentException("montant non positif : " + montant); } if (montant > solde) { throw new SoldeInsuffisantException(montant - solde); } solde -= montant;}throw interrompt immédiatement la méthode. La valeur de retour n'est jamais produite : il
n'y a plus de code d'erreur à ignorer, et le chemin d'échec est séparé du chemin normal.
Du côté de l'appelant :
try { compte.retirer(1000); System.out.println("retrait effectué");} catch (SoldeInsuffisantException e) { System.out.println("il manque " + e.getManquant() + " euros");} catch (IllegalArgumentException e) { System.out.println("montant invalide : " + e.getMessage());}Ce qui se passe alors mérite d'être décrit précisément, car c'est le mécanisme du bloc I
d'Algorithmique 2 employé à l'envers. Quand une exception est levée, la machine remonte la
pile d'appels — elle dépile les cadres un par un — jusqu'à trouver un catch capable de la
traiter. Tous les cadres traversés sont abandonnés. Si aucun catch ne se présente, la remontée
atteint main, le programme s'arrête, et la trace affichée est exactement le contenu de la
pile au moment du throw.
D'où la propriété centrale : l'exception franchit les niveaux intermédiaires sans qu'ils aient un mot à dire. Une méthode qui ne sait pas traiter un échec n'a rien à écrire du tout — c'est ce qui supprime la propagation manuelle du code d'erreur.
L'ordre des catch compte : ils sont examinés dans l'ordre, et le premier compatible gagne. Un
catch (Exception e) placé avant les autres les rend inatteignables, et le compilateur le
refuse — c'est la substituabilité du chapitre 6 appliquée aux erreurs, puisque toutes les
exceptions héritent d'un ancêtre commun.
finally
FileReader f = null;try { f = new FileReader("donnees.txt"); /* lecture */} catch (IOException e) { System.err.println("lecture impossible");} finally { if (f != null) f.close(); /* exécuté DANS TOUS LES CAS */}Le bloc finally s'exécute quoi qu'il arrive : sortie normale, exception attrapée, exception
non attrapée qui traverse — et même après un return. C'est le seul endroit où l'on peut
garantir qu'une ressource sera rendue.
Java offre depuis la version 7 une forme plus sûre, le try avec ressources, qui appelle
close() automatiquement :
try (FileReader f = new FileReader("donnees.txt")) { /* lecture */} catch (IOException e) { ... }C'est la forme à employer. Elle évite l'oubli du close, le null à tester, et l'exception que
close() pourrait lever dans le finally — cas tordu qui masquerait l'exception d'origine.
Un piège à connaître : un return dans le finally écrase celui du try, exception
comprise. C'est légal, c'est déroutant, et cela ne s'écrit pas.
Contrôlées ou non contrôlées
Java est le seul grand langage à faire cette distinction, et elle structure tout le chapitre.
| Contrôlées (checked) | Non contrôlées (unchecked) | |
|---|---|---|
| Héritent de | Exception | RuntimeException |
| Le compilateur | exige de les traiter ou de les déclarer | ne dit rien |
| Exemples | IOException, SQLException | NullPointerException, IllegalArgumentException |
| Signifient | un échec prévisible et extérieur | une erreur de programmation |
Une méthode qui peut lever une exception contrôlée sans la traiter doit l'annoncer :
public void charger(String chemin) throws IOException { ... }Le critère de choix, tel qu'on l'enseigne : une exception contrôlée signale une situation anormale mais attendue, sur laquelle l'appelant peut agir — fichier absent, réseau coupé. Une exception non contrôlée signale un bogue : un argument invalide, un pointeur nul, un indice hors bornes. On ne demande pas à l'appelant de traiter un bogue, on le corrige.
La distinction est débattue : elle produit des signatures encombrées et pousse à écrire des
catch vides pour faire taire le compilateur. C'est pourquoi les langages venus après Java —
C#, Kotlin — ne l'ont pas reprise. Il faut la connaître parce que Java l'impose, et savoir
qu'elle n'est pas une évidence.
Exceptions personnalisées
On en écrit une dès que le type standard ne dit pas assez, ou qu'on veut transporter une donnée.
public class SoldeInsuffisantException extends RuntimeException { private final double manquant; public SoldeInsuffisantException(double manquant) { super("il manque " + manquant + " euros"); this.manquant = manquant; } public double getManquant() { return this.manquant; }}Trois choix se lisent dans ces lignes. Le nom finit par Exception, convention universelle.
Le message passe au parent par super(...), et sera affiché dans la trace. Et la classe
porte une donnée — le montant manquant — que l'appelant peut exploiter : c'est le troisième
défaut du code d'erreur, réglé.
Le choix du parent — Exception ou RuntimeException — est le vrai choix de conception, et il
découle du tableau précédent.
Les trois anti-usages
Ils sont fréquents, et chacun annule le bénéfice du chapitre.
Avaler l'exception.
try { ... } catch (Exception e) { } /* le pire code de ce cours */L'échec disparaît sans trace. Le programme continue dans un état inconnu, et le diagnostic devient impossible. Si l'on ne sait vraiment pas quoi faire, on laisse remonter — c'est gratuit, il suffit de ne rien écrire.
Employer les exceptions comme structure de contrôle. Sortir d'une boucle par une exception, ou tester l'existence d'une clé en attrapant l'échec, fonctionne et coûte cher : construire une exception implique de capturer la pile d'appels. Une exception doit rester exceptionnelle.
Attraper trop large. catch (Exception e) ramasse tout, y compris ce qu'on n'avait pas
prévu — un bogue de programmation qu'il aurait mieux valu voir. On attrape le type le plus
précis possible.
Quiz · 1 question
Quels défauts du code de retour les exceptions corrigent-elles ?
- Un seul : elles rendent l'erreur plus lisible dans les traces — la lisibilité
- Quatre : le code d'erreur peut être IGNORÉ, il OCCUPE la valeur de retour, il ne porte AUCUNE INFORMATION, et il doit être PROPAGÉ à la main à travers chaque niveau d'appel — quatre défauts
- Aucun : c'est une question de style, et les deux approches sont équivalentes — équivalentes
Réponse : Les quatre défauts sont indépendants. IGNORABLE : rien n'oblige à tester une valeur de retour, et scanf en est la preuve — presque personne ne regarde ce qu'elle rend. OCCUPE LE RETOUR : une méthode qui doit produire un résultat utile n'a plus de place pour signaler l'échec, d'où les conventions bancales (rendre −1, ou null, ou un objet vide). AUCUNE INFORMATION : −1 ne dit pas combien il manquait, alors qu'une exception personnalisée transporte la donnée. PROPAGATION MANUELLE : chaque niveau intermédiaire doit tester et retransmettre, alors qu'une exception traverse la pile sans que les niveaux aient un mot à dire. Le throw interrompt de plus la méthode immédiatement, ce qui sépare nettement le chemin d'échec du chemin normal.
Quiz · 1 question
Quand faut-il faire hériter son exception de RuntimeException plutôt que d'Exception ?
- Toujours : RuntimeException évite d'encombrer les signatures avec throws — toujours
- Quand l'exception signale une ERREUR DE PROGRAMMATION — argument invalide, état incohérent — qu'on corrige au lieu de la traiter. Exception (contrôlée) est pour un échec prévisible et extérieur, sur lequel l'appelant peut agir — selon la nature de l'échec
- Quand l'exception est levée dans un constructeur, seul cas où le compilateur n'accepte pas les exceptions contrôlées — cas du constructeur
Réponse : Le critère est la NATURE de l'échec, pas la commodité. Une exception CONTRÔLÉE (Exception) signale une situation anormale mais ATTENDUE, venue de l'extérieur du programme : fichier absent, réseau coupé, base indisponible. L'appelant peut raisonnablement agir — réessayer, demander un autre fichier — donc le compilateur exige qu'il la traite ou la déclare. Une exception NON CONTRÔLÉE (RuntimeException) signale un BOGUE : argument invalide, pointeur nul, indice hors bornes. On ne demande pas à l'appelant de traiter un bogue, on le corrige — d'où l'absence de contrainte. Choisir RuntimeException pour alléger les signatures est le contre-emploi : cela masque un échec que l'appelant aurait dû prévoir. La distinction est d'ailleurs débattue, et les langages venus après Java ne l'ont pas reprise — mais Java l'impose.
À vous
L'exercice suit le fil de bibliothèque, et compare les deux approches sur le même service.
D'abord la version à code de retour : vous écrivez emprunter qui rend -1, -2, 0, puis un
appelant qui oublie de tester — et vous constatez qu'un livre est emprunté deux fois sans
que rien ne le dise.
Puis la version à exceptions, avec une exception personnalisée transportant la donnée utile. Vous observerez la remontée de pile sur trois niveaux d'appel : le niveau intermédiaire n'écrit rien, et l'exception le traverse.
Enfin les pièges : l'ordre des catch, le finally qui s'exécute même après un return, et le
catch vide — que vous instrumenterez pour voir combien d'échecs il a fait disparaître.
Exercice de code
Opposez code de retour et exceptions sur le même service, puis mesurez ce qu'un catch vide fait disparaître.
Point de départ
// ── 1. Version à code de retour ───────────────────────────────────────────
const OK = 0, DEJA_EMPRUNTE = -1, INCONNU = -2;
class BibliothequeCodes {
constructor(livres) { this.livres = livres; }
emprunter(titre, qui) {
const l = this.livres.find((x) => x.titre === titre);
if (!l) return INCONNU;
if (!l.disponible) return DEJA_EMPRUNTE;
l.disponible = false; l.par = qui;
return OK;
}
}
// ── 2. Version à exceptions ───────────────────────────────────────────────
class LivreIndisponibleException extends Error {
constructor(titre, detenteur) {
super("« " + titre + " » est déjà emprunté par " + detenteur);
this.name = "LivreIndisponibleException";
// ← à écrire : transporter la donnée utile (le détenteur actuel)
}
}
class BibliothequeExceptions {
constructor(livres) { this.livres = livres; }
emprunter(titre, qui) {
const l = this.livres.find((x) => x.titre === titre);
if (!l) throw new Error("titre inconnu : " + titre);
// ← à écrire : lever LivreIndisponibleException si déjà emprunté
l.disponible = false; l.par = qui;
}
}
// Trois niveaux d'appel : le niveau intermédiaire n'écrit RIEN.
function servirUnLecteur(biblio, titre, qui) { traiterDemande(biblio, titre, qui); }
function traiterDemande(biblio, titre, qui) { biblio.emprunter(titre, qui); }
// ── 3. Le catch vide, instrumenté ─────────────────────────────────────────
let avales = 0;
function catchVide(action) {
try { action(); } catch (e) { avales++; } // le pire code du cours
}
// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Appelez emprunter() deux fois SANS tester le code de retour, et
// constatez ce qui n'est pas dit.
// 2. Complétez l'exception personnalisée et son levée. Vérifiez que le
// niveau intermédiaire n'a rien eu à écrire.
// 3. Faites vingt emprunts dont plusieurs impossibles, à travers catchVide :
// combien d'échecs ont disparu, et qu'en sait le programme ?
const catalogue = () => [
{ titre: "Dune", disponible: true, par: null },
{ titre: "Ubik", disponible: true, par: null },
];
const b = new BibliothequeCodes(catalogue());
b.emprunter("Dune", "Ana");
b.emprunter("Dune", "Bo");
console.log(" état : " + JSON.stringify(b.livres[0]));
Solution
const OK = 0, DEJA_EMPRUNTE = -1, INCONNU = -2;
class BibliothequeCodes {
constructor(livres) { this.livres = livres; }
emprunter(titre, qui) {
const l = this.livres.find((x) => x.titre === titre);
if (!l) return INCONNU;
if (!l.disponible) return DEJA_EMPRUNTE;
l.disponible = false; l.par = qui;
return OK;
}
}
class LivreIndisponibleException extends Error {
constructor(titre, detenteur) {
super("« " + titre + " » est déjà emprunté par " + detenteur);
this.name = "LivreIndisponibleException";
// La donnée utile voyage avec l'exception : c'est le troisième défaut
// du code de retour, réglé.
this.detenteur = detenteur;
this.titre = titre;
}
}
class BibliothequeExceptions {
constructor(livres) { this.livres = livres; }
emprunter(titre, qui) {
const l = this.livres.find((x) => x.titre === titre);
if (!l) throw new Error("titre inconnu : " + titre);
if (!l.disponible) throw new LivreIndisponibleException(titre, l.par);
l.disponible = false; l.par = qui;
}
}
// Le niveau intermédiaire : PAS UNE LIGNE consacrée aux erreurs.
function traiterDemande(biblio, titre, qui) { biblio.emprunter(titre, qui); }
function servirUnLecteur(biblio, titre, qui) { traiterDemande(biblio, titre, qui); }
const catalogue = () => [
{ titre: "Dune", disponible: true, par: null },
{ titre: "Ubik", disponible: true, par: null },
];
console.log("— 1. code de retour, non testé —");
const b = new BibliothequeCodes(catalogue());
b.emprunter("Dune", "Ana");
b.emprunter("Dune", "Bo"); // échoue, et personne ne le sait
b.emprunter("Solaris", "Cy"); // titre inconnu, idem
console.log(" " + JSON.stringify(b.livres[0]));
console.log(" Bo croit avoir emprunté Dune, qui est chez Ana. Le programme n'a");
console.log(" rien dit, et rien dans son état ne permet de s'en apercevoir.");
console.log("");
console.log("— 2. exceptions, à travers trois niveaux —");
const c = new BibliothequeExceptions(catalogue());
servirUnLecteur(c, "Dune", "Ana");
console.log(" premier emprunt : ok");
try {
servirUnLecteur(c, "Dune", "Bo");
} catch (e) {
if (e instanceof LivreIndisponibleException) {
console.log(" refusé : " + e.message);
console.log(" donnée exploitable : détenteur = " + e.detenteur +
" — on peut proposer une réservation");
} else {
console.log(" autre échec : " + e.message);
}
}
console.log(" traiterDemande n'a écrit AUCUNE ligne sur les erreurs : l'exception");
console.log(" l'a traversé sans lui demander son avis.");
console.log("");
console.log("— l'ordre des catch —");
try {
try { throw new LivreIndisponibleException("Dune", "Ana"); }
catch (e) {
// En Java, un catch (Exception e) placé AVANT le type précis rendrait
// celui-ci inatteignable, et le compilateur le refuserait. On attrape
// donc toujours du plus précis au plus général.
if (e instanceof LivreIndisponibleException) throw new Error("relancée après traitement précis");
}
} catch (e) { console.log(" " + e.message); }
console.log("");
console.log("— finally s'exécute même après un return —");
function avecFinally() {
try { return "valeur du try"; }
finally { console.log(" finally s'exécute — avant que la valeur ne soit rendue"); }
}
console.log(" " + avecFinally());
console.log("");
console.log("— 3. le catch vide, instrumenté —");
let avales = 0;
function catchVide(action) { try { action(); } catch (e) { avales++; } }
const d = new BibliothequeExceptions(catalogue());
const demandes = [["Dune", "Ana"], ["Dune", "Bo"], ["Ubik", "Cy"], ["Ubik", "Dee"],
["Solaris", "Eve"], ["Dune", "Fay"]];
for (const [t, q] of demandes) catchVide(() => d.emprunter(t, q));
console.log(" " + demandes.length + " demandes, " + avales + " échecs AVALÉS sans trace.");
console.log(" Le programme s'est terminé normalement, avec quatre lecteurs qui");
console.log(" croient avoir un livre. C'est exactement l'état du point 1 — le");
console.log(" catch vide a rétabli tous les défauts du code de retour.");
console.log(" Si l'on ne sait pas quoi faire : ne rien écrire, et laisser remonter.");
En travaux pratiques
Travaux pratiques 7 · 3 h
Signaler l'erreur là où on sait la traiter
Choisir entre code de retour et exception, distinguer vérifiées et non vérifiées, et supprimer de son code les deux fautes qui rendent une erreur invisible.
Avant de commencer
- Les TP 1 à 6
Énoncé
- Sans exceptions — Écrivez le chargement d'un catalogue en signalant les erreurs par des codes de retour. Comptez les lignes de traitement d'erreur par rapport aux lignes utiles.
- Avec exceptions — Réécrivez la même chose avec des exceptions. Recomptez, et repérez où le code utile est devenu lisible d'un seul tenant.
- Vérifiée ou non — Écrivez une exception vérifiée et une non vérifiée. Appelez les deux sans les traiter et comparez ce que dit le compilateur.
- Le catch vide — Attrapez une exception et ne faites rien. Provoquez l'erreur et observez ce que voit l'utilisateur. Puis affichez la trace et comparez. Indice : Le pire n'est pas le plantage : c'est le programme qui continue avec des données fausses.
- Attraper trop large — Attrapez le type le plus général possible autour d'un bloc, et faites-y survenir une erreur de programmation. Constatez ce qui est masqué.
- L'exception qui perd sa cause — Attrapez une exception basse et relancez-en une métier SANS transmettre la cause. Comparez la trace obtenue avec celle où la cause est transmise.
- Fermer quoi qu'il arrive — Ouvrez un fichier, provoquez une erreur avant la fermeture, et vérifiez que la ressource fuit. Corrigez avec la fermeture automatique.
- Choisir — Pour six situations que vous listerez, décidez : code de retour, exception vérifiée, ou non vérifiée. Justifiez chaque choix en une phrase.
C'est réussi quand
- Votre version à exceptions sépare visiblement le cas nominal du traitement d'erreur
- Vous montrez un catch qui masque une erreur de programmation
- Votre trace conserve la cause d'origine sur trois niveaux
- Votre fichier est fermé même quand une exception survient
Correction
/* codes de retour : le cas nominal est NOYÉ */
int r = ouvrir(f);
if (r != 0) return r;
r = lireEntete(f);
if (r != 0) { fermer(f); return r; }
r = lireCorps(f);
if (r != 0) { fermer(f); return r; }
/* exceptions : le cas nominal se lit d'un trait */
try (Fichier f = ouvrir(chemin)) {
lireEntete(f);
lireCorps(f);
} catch (FormatInvalideException e) {
journal.erreur("catalogue illisible : " + chemin, e);
}L'exception sépare le CHEMIN NOMINAL du traitement d'erreur, au lieu de les entrelacer. C'est son vrai apport, avant même la propagation automatique. Elle a aussi un défaut, à connaître : le flot de contrôle devient invisible dans le code, et une exception peut sortir de n'importe quelle ligne.
class CatalogueIntrouvableException extends Exception { } /* VÉRIFIÉE */
class CoteInvalideException extends RuntimeException { } /* non */
void charger() throws CatalogueIntrouvableException { … }
/* l'appelant DOIT attraper ou déclarer : le compilateur l'exige */
void valider() { throw new CoteInvalideException(); }
/* l'appelant peut ignorer : rien ne l'oblige */Le critère classique : vérifiée quand l'appelant peut RAISONNABLEMENT réagir — fichier absent, réseau indisponible ; non vérifiée quand c'est un bogue de programmation — argument nul, indice hors bornes, invariant rompu. Le débat reste vif, et les langages récents ont tous abandonné les exceptions vérifiées ; l'usage dominant en Java est de les réserver aux erreurs réellement récupérables.
/* 1. le catch vide */
try { catalogue = charger(chemin); }
catch (IOException e) { } /* catalogue reste null,
le programme continue et
plante 200 lignes plus loin */
/* 2. attraper trop large */
try { traiterTout(); }
catch (Exception e) { journal.avertir("souci mineur"); }
/* attrape aussi NullPointerException, ArrayIndexOutOfBounds…
c'est-à-dire vos BOGUES, requalifiés en « souci mineur » */Le catch vide est la faute la plus coûteuse du langage : l'information existait, elle a été jetée. Attraper Exception est la même faute en plus discret — un bogue de programmation y devient un avertissement. La règle : attraper le type le plus PRÉCIS possible, et si l'on ne sait pas quoi faire, ne pas attraper.
/* SANS la cause : la trace commence au niveau métier */
catch (SQLException e) {
throw new CatalogueException("chargement impossible");
}
→ CatalogueException: chargement impossible
at Catalogue.charger(Catalogue.java:42)
/* et POURQUOI ? Personne ne le saura jamais. */
/* AVEC la cause */
catch (SQLException e) {
throw new CatalogueException("chargement impossible", e);
}
→ CatalogueException: chargement impossible
at Catalogue.charger(Catalogue.java:42)
Caused by: SQLException: connection refused: port 5432
at …Le « Caused by » est ce qui rend une trace exploitable : il conserve toute la chaîne, du symptôme métier jusqu'à la cause technique. Traduire une exception basse en exception métier est une bonne pratique — l'appelant n'a pas à connaître SQL — mais jeter la cause en chemin transforme un diagnostic d'une minute en enquête d'une journée.
/* la ressource FUIT si lireTout() lève */
BufferedReader r = new BufferedReader(new FileReader(f));
String contenu = lireTout(r);
r.close(); /* jamais atteint */
/* try-with-resources : close() est appelé DANS TOUS LES CAS */
try (BufferedReader r = new BufferedReader(new FileReader(f))) {
return lireTout(r);
}
/* et si close() lève à son tour, l'exception d'origine est conservée,
la seconde étant attachée comme « supprimée » */Un descripteur de fichier non fermé est exactement la fuite du TP 7 de Systèmes : la ressource est finie, et un serveur qui fuit un descripteur par requête tombe en quelques heures. Le bloc try-with-resources garantit la fermeture sans finally verbeux, et conserve la bonne exception — ce qu'un finally écrit à la main manque presque toujours.
fichier de configuration absent → exception vérifiée (récupérable)
argument null passé à une méthode → non vérifiée (bogue de l'appelant)
recherche sans résultat → NI l'une NI l'autre : Optional
(l'absence est un cas NORMAL)
saisie utilisateur invalide → valeur de retour ou Optional
(attendu, fréquent, pas exceptionnel)
invariant interne rompu → non vérifiée, et ne pas rattraper
disque plein à l'écriture → vérifiéeLa ligne la plus utile est la troisième : une recherche infructueuse n'est PAS une erreur, et lever une exception pour un cas normal coûte cher — construire la trace est une opération lourde — et brouille la lecture. Optional exprime l'absence dans le type de retour, ce qui la rend impossible à oublier.
Ce que la suite en fait
Le chapitre 8 donne la notation pour dessiner ce que les six premiers chapitres ont construit, exceptions comprises : une hiérarchie d'exceptions est une hiérarchie de classes ordinaire, et elle se dessine comme telle.
Le chapitre 9 les fera rencontrer les collections et les fichiers, où elles sont partout :
IOException à la lecture, NumberFormatException à la conversion,
NoSuchElementException sur une collection vide. C'est là qu'on mesure que les exceptions ne
sont pas un ornement mais le mode de signalement de toute la bibliothèque standard.
À retenir
Flashcards · 5 cartes
- Quels sont les quatre défauts du code de retour ?
- 1) IL PEUT ÊTRE IGNORÉ en toute impunité. 2) IL OCCUPE LA VALEUR DE RETOUR, donc une méthode produisant un résultat utile n'a plus de place pour signaler l'échec. 3) IL NE PORTE AUCUNE INFORMATION : −1 ne dit pas combien il manquait. 4) IL DOIT ÊTRE PROPAGÉ À LA MAIN à travers chaque niveau d'appel. Les exceptions règlent les quatre, et throw interrompt de plus la méthode immédiatement.
- Que se passe-t-il exactement quand une exception est levée ?
- La machine REMONTE LA PILE D'APPELS, dépilant les cadres un par un, jusqu'à trouver un catch capable de la traiter ; tous les cadres traversés sont abandonnés. Sans catch, la remontée atteint main, le programme s'arrête, et la trace affichée EST le contenu de la pile au moment du throw. Propriété centrale : l'exception franchit les niveaux intermédiaires sans qu'ils aient un mot à dire — une méthode qui ne sait pas traiter un échec n'écrit rien du tout.
- Que garantit finally, et quelle forme lui préférer ?
- Il s'exécute QUOI QU'IL ARRIVE : sortie normale, exception attrapée, exception qui traverse, et même après un return. C'est le seul endroit où garantir qu'une ressource sera rendue. Depuis Java 7 on préfère le TRY AVEC RESSOURCES, qui appelle close() automatiquement : il évite l'oubli, le null à tester, et l'exception que close() pourrait lever en masquant l'originale. Piège : un return dans le finally écrase celui du try, exception comprise.
- Contrôlée ou non contrôlée : quel critère, et quelle nuance ?
- CONTRÔLÉE (hérite d'Exception) : échec anormal mais ATTENDU, venu de l'extérieur — fichier absent, réseau coupé — sur lequel l'appelant peut agir ; le compilateur exige de la traiter ou de la déclarer par throws. NON CONTRÔLÉE (hérite de RuntimeException) : erreur de PROGRAMMATION — argument invalide, pointeur nul, indice hors bornes — qu'on corrige au lieu de la traiter. Nuance : la distinction est débattue, elle encombre les signatures et pousse aux catch vides ; C# et Kotlin ne l'ont pas reprise.
- Quels sont les trois anti-usages des exceptions ?
- 1) LES AVALER : catch (Exception e) { } fait disparaître l'échec sans trace, et le programme continue dans un état inconnu. Si l'on ne sait pas quoi faire, on LAISSE REMONTER — il suffit de ne rien écrire. 2) LES EMPLOYER COMME STRUCTURE DE CONTRÔLE : construire une exception capture la pile d'appels, c'est coûteux ; une exception doit rester exceptionnelle. 3) ATTRAPER TROP LARGE : catch (Exception e) ramasse aussi les bogues qu'il aurait mieux valu voir.