ExceptionsDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

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 deExceptionRuntimeException
Le compilateurexige de les traiter ou de les déclarerne dit rien
ExemplesIOException, SQLExceptionNullPointerException, IllegalArgumentException
Signifientun échec prévisible et extérieurune 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 tracesla 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'appelquatre 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 throwstoujours
  • 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 agirselon 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éescas 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é

  1. 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.
  2. Avec exceptionsRéécrivez la même chose avec des exceptions. Recomptez, et repérez où le code utile est devenu lisible d'un seul tenant.
  3. 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.
  4. Le catch videAttrapez 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.
  5. Attraper trop largeAttrapez 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é.
  6. L'exception qui perd sa causeAttrapez une exception basse et relancez-en une métier SANS transmettre la cause. Comparez la trace obtenue avec celle où la cause est transmise.
  7. Fermer quoi qu'il arriveOuvrez un fichier, provoquez une erreur avant la fermeture, et vérifiez que la ressource fuit. Corrigez avec la fermeture automatique.
  8. ChoisirPour 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 contre exceptions
/* 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.

Vérifiée ou non
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.

Les deux fautes qui effacent l'erreur
/* 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.

Transmettre la cause
/* 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.

Fermer quoi qu'il arrive
/* 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.

La table de décision
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ée

La 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.