8  Excepcions i depuració

Objectius

  • Distingir entre errors de compilació, errors d’execució i errors lògics.
  • Entendre què és una excepció i com Java l’utilitza per assenyalar problemes.
  • Capturar excepcions amb try-catch-finally.
  • Diferenciar excepcions comprovades (checked) i no comprovades (unchecked).
  • Llançar les teves pròpies excepcions amb throw.
  • Fer servir assercions per validar suposicions durant el desenvolupament.
  • Depurar programes amb el debugger: punts de ruptura, execució pas a pas i inspecció de variables.
  • Documentar el teu codi amb Javadoc.

Fins ara hem suposat que tot anava bé: l’usuari introduïa nombres quan li demanàvem nombres, els fitxers existien, les divisions no eren per zero… El món real no és tan amable. Els programes robustos han de preveure allò que pot anar malament i reaccionar-hi amb elegància. Aprendrem a gestionar errors amb excepcions i a caçar-los amb les eines de depuració.

8.1 Tipus d’errors

No tots els errors són iguals. Segons quan apareixen, els classifiquem en tres famílies:

Tipus d’error Quan apareix? Qui l’avisa? Exemple
De compilació Al compilar, abans d’executar El compilador Un nom mal escrit, bblidar un ;
D’execució Mentre el programa s’executa La JVM (una excepció) Dividir per zero, fitxer inexistent
Lògic Mai avisa: el programa “funciona” però dona un resultat incorrecte Ningú (l’has de trobar tu) Sumar quan calia restar


Els errors de compilació són els més amables: el codi no compila i no es pot executar. Els errors d’execució apareixen quan menys t’ho esperes i, si no els gestiones, aturen el programa de cop. Els errors lògics són els més perillosos: el programa sembla que va bé, però els resultats no són correctes.

NotaAnalogia: la recepta de cuina
  • Error de compilació: la recepta té una frase sense sentit i no la pots ni llegir. O està en un idioma que no entenem.
  • Error d’execució: la recepta demana un ingredient que no tens a la nevera i t’atures a mig fer.
  • Error lògic: has seguit la recepta al peu de la lletra, però has confós el sucre amb la sal. El plat surt… però no s’hi val.

8.2 Què és una excepció?

Una excepció és un objecte que Java crea automàticament quan passa alguna cosa anòmala durant l’execució.

Quan es produeix, diem que Java llança (throws) l’excepció.

Si ningú la captura (catch), el programa s’atura i mostra un missatge d’error anomenat traça de pila (stack trace).

Exemple. Aquest codi divideix per zero. Fitxer Prova.java.

int a = 10;
int b = 0;
int resultat = a / b;               // ArithmeticException!
System.out.println(resultat);

En executar-lo, el programa s’atura i escriu una cosa semblant a:

    Exception in thread "main" java.lang.ArithmeticException: / by zero
        at Prova.main(Prova.java:4)

Aquest missatge et diu tres coses valuoses:

  • Quin tipus d’error és (ArithmeticException),
  • Què ha passat (/ by zero)
  • On (fitxer Prova.java, línia 4). Aprendre a llegir la traça de pila és una de les habilitats més útils que desenvoluparàs.

Algunes excepcions habituals que et trobaràs aviat:

Excepció Causa típica
ArithmeticException dividir un enter per zero
NullPointerException usar una variable que val null
ArrayIndexOutOfBoundsException accedir a una posició inexistent d’un array (Capítol 9)
NumberFormatException convertir a nombre un text que no ho és ("hola")
InputMismatchException l’usuari escriu text quan Scanner esperava un nombre

8.3 Capturar excepcions amb try-catch

Per evitar que una excepció aturi el programa, l’envoltem amb un bloc try-catch.

La idea és: “intenta (try) fer això; i si falla, captura’l (catch) i reacciona”.

try {
    // Codi que pot fallar.
} catch (TipusExcepcio e) {
    // Què fem si falla.
}


Exemple: demanar un nombre sense que el programa falli si l’usuari escriu lletres:

Scanner sc = new Scanner(System.in);
try {
    System.out.print("Introdueix un nombre: ");
    int n = sc.nextInt();
    System.out.println("El doble és " + (n * 2));
} catch (InputMismatchException e) {
    System.out.println("Això no era un nombre!");
}

Si l’usuari escriu 42, tot va bé. Si escriu hola, en comptes d’aturar-se, el programa entra al catch i mostra un missatge amigable.

Aquest és el flux que segueix Java:

flowchart TD
    A([Entrem al try]) --> B["Executem el codi"]
    B --> C{"Hi ha<br/>excepció?"}
    C -->|No| D["Saltem el catch"]
    C -->|Sí| E["Aturem el try<br/>i saltem al catch"]
    E --> F["Executem el catch"]
    D --> G([Continuem])
    F --> G

Capturar diferents excepcions

Un mateix try pot tenir diversos catch per reaccionar de manera diferent segons el tipus d’error:

try {
    int[] notes = {5, 7, 9};
    int posicio = sc.nextInt();
    System.out.println(100 / notes[posicio]);
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Posició fora de l'array!");
} catch (ArithmeticException e) {
    System.out.println("No es pot dividir per zero!");
}
AlertaError típic: capturar Exception per a tot

És temptador escriure catch (Exception e) per capturar-ho tot d’una sola vegada. Funciona, però amaga informació: no saps quin error ha passat i pots enmascarar bugs greus. Captura sempre el tipus més concret que esperes. Deixa Exception només com a última xarxa de seguretat.

8.4 Bloc finally

A vegades hi ha codi que s’ha d’executar tant si hi ha error com si no: tancar un fitxer, alliberar una connexió… per a això existeix el bloc finally, que s’executa sempre, al final:

try {
    // Codi que pot fallar
} catch (Exception e) {
    // Gestió de l'error
} finally {
    // S'executa SEMPRE, hi hagi hagut error o no
    System.out.println("Neteja feta.");
}
NotaAnalogia

El finally és com apagar el llum en sortir d’una habitació: tant si has fet la feina com si t’ha interromput una trucada, en sortir apagues el llum igualment.

8.5 Excepcions comprovades i no comprovades

Java divideix les excepcions en dues categories:

  • No comprovades (unchecked): hereten de RuntimeException. Solen ser errors de programació (dividir per zero, null…). El compilador no t’obliga a gestionar-les.
  • Comprovades (checked): la resta (com IOException). Representen problemes externs previsibles (un fitxer que no existeix). El compilador t’obliga a gestionar-les: o les captures amb try-catch, o declares que el mètode les pot llançar amb throws.

Aquesta és la jerarquia simplificada de classes d’excepcions a Java:

%%{init: {"class": {"nodeSpacing": 100, "rankSpacing": 80}}}%%
classDiagram
    direction TB
    Throwable <|-- Error
    Throwable <|-- Exception
    Exception <|-- IOException
    Exception <|-- RuntimeException

    class Throwable throwableCls
    class Error errorCls
    class Error {
        <<No es capturen>>
    }
    class Exception checkedCls
    class Exception {
        <<checked>>
    }
    class IOException checkedCls
    class RuntimeException uncheckedCls
    class RuntimeException {
        <<unchecked>>
    }

NotaError vs Exception

Al capdamunt de tot hi ha Throwable, del qual pengen Error i Exception. Els Error (com quedar-se sense memòria) són problemes tan greus que normalment no s’han d’intentar capturar. Tu treballaràs gairebé sempre amb Exception i les seves filles.

8.6 Llançar excepcions amb throw

Java llença excepcions quan detecta una anomalia, però com a programador tu també pots fer-ho quan detectes una situació que el teu codi considera invàlida.

Es fa amb la paraula clau throw:

public static void comprovarEdat(int edat) {
    if (edat < 0) {
        throw new IllegalArgumentException("L'edat no pot ser negativa: " + edat);
    }
    System.out.println("Edat vàlida: " + edat);
}

Si algú crida comprovarEdat(-5), es llança una IllegalArgumentException amb el nostre missatge.

Així obliguem qui utilitza el nostre mètode a passar-nos dades correctes.

ConsellNo confonguis throw amb throws
  • throw (sense s): llança una excepció ara mateix, dins d’un mètode.
  • throws (amb s): declara a la capçalera d’un mètode que aquest pot llançar una excepció comprovada, per avisar-ne qui el crida.
public static void llegirFitxer(String nom) throws IOException {
    // ... codi que pot llançar IOException
}
ConsellMini-repte

Escriu un mètode arrelQuadrada(double x) que llanci una IllegalArgumentException si x és negatiu, i que retorni Math.sqrt(x) en cas contrari.

8.7 Assercions

Una asserció és una comprovació que fem durant el desenvolupament per verificar que una suposició nostra és certa.

Es fa amb la paraula clau assert:

int edat = calcularEdat();
assert edat >= 0 : "L'edat mai hauria de ser negativa aquí";

Si la condició és falsa, es llança un AssertionError amb el missatge.

Les assercions serveixen per caçar errors de programació nostra, no per validar dades de l’usuari.

AlertaLes assercions estan desactivades per defecte

A Java, les assercions no s’executen si no les actives explícitament amb l’opció -ea (enable assertions) en executar el programa.

Mai les has de fer servir per validar dades reals de l’usuari (per a això, if + throw). Fes-les servir només com a ajuda de desenvolupament.

8.8 Depuració amb el debugger

Quan un programa no fa el que esperes, tens dues opcions.

  • La ràpida però pobra és omplir el codi de System.out.println per veure valors.
  • La professional és fer servir el debugger (depurador), una eina integrada a IntelliJ IDEA i VS Code que et permet aturar el programa i mirar-hi a dins.

Les tres eines fonamentals del debugger:

  1. Punt de ruptura (breakpoint). Fas clic al marge esquerre, al costat d’una línia. El programa s’aturarà just abans d’executar-la.
  2. Execució pas a pas: un cop aturat, avances instrucció per instrucció.
    • Step Over. Executa la línia actual i passa a la següent.
    • Step Into. Si la línia crida un mètode, hi entra per veure’l per dins.
    • Step Out. Acaba el mètode actual i torna a qui l’ha cridat.
  3. Inspecció de variables. Mentre està aturat, veus el valor de totes les variables en aquell instant.

flowchart LR
    A["Poso un<br/>breakpoint"] --> B["Executo en<br/>mode debug"]
    B --> C["El programa<br/>s'atura al breakpoint"]
    C --> D["Inspecciono<br/>les variables"]
    D --> E["Avanço<br/>pas a pas"]
    E --> F{"Ja he trobat<br/>l'error?"}
    F -->|No| D
    F -->|Sí| G["El corregeixo"]

ConsellEl debugger t’estalvia hores

Al principi fa mandra aprendre’l, però un cop hi agafes el punt, trobar un error lògic passa de trigar una hora a trigar dos minuts.

En comptes d’endevinar què fa el programa, ho veus. Val moltíssim la pena invertir-hi una tarda.

8.9 Documentar amb Javadoc

Un codi ben documentat és més fàcil de mantenir i de compartir.

Java té un sistema estàndard de comentaris, el Javadoc, que s’escriu amb /** ... */ just abans de classes i mètodes.

Permet generar documentació web automàticament.

/**
 * Calcula lrea d'un rectangle.
 *
 * @param base   la base del rectangle (en cm), ha de ser positiva
 * @param altura l'altura del rectangle (en cm), ha de ser positiva
 * @return lrea del rectangle en cm²
 */
public static double areaRectangle(double base, double altura) {
    return base * altura;
}

Les etiquetes més habituals són:

Etiqueta Per a què serveix
@param Descriu un paràmetre del mètode
@return Descriu què retorna el mètode
@throws Indica quines excepcions pot llançar
@author Qui ha escrit el codi
@since Des de quina versió existeix
NotaComentari normal vs Javadoc

Un comentari // així o /* així */ és per a tu i el teu equip mentre llegiu el codi.

Un comentari Javadoc /** així */ descriu què fa i com s’usa un mètode o classe de cara a fora, i les eines el converteixen en documentació navegable.

Reserva el Javadoc per a la interfície pública del teu codi.

8.10 Resum

  • Hi ha tres famílies d’errors: de compilació, d’execució (excepcions) i lògics (els més difícils de trobar).
  • Una excepció és un objecte que Java llança quan alguna cosa va malament en execució; si no es captura, atura el programa i mostra la traça de pila.
  • try-catch captura excepcions i permet reaccionar-hi; pots tenir diversos catch.
  • finally s’executa sempre, hi hagi error o no.
  • Les excepcions comprovades (com IOException) el compilador t’obliga a gestionar-les; les no comprovades (RuntimeException i filles) no.
  • throw llança una excepció; throws declara que un mètode en pot llançar.
  • assert valida suposicions durant el desenvolupament (desactivat per defecte).
  • El debugger (breakpoints, pas a pas, inspecció) és l’eina professional per trobar errors lògics.
  • Javadoc (/** ... */ amb @param, @return…) documenta el codi de manera estàndard.

8.11 Per practicar

  1. Divisió segura. Escriu un programa que demani dos enters i mostri la divisió, capturant la ArithmeticException si el divisor és 0.
  2. Lector robust. Fes un bucle que demani un nombre i, si l’usuari escriu text, capturi la InputMismatchException i torni a demanar-lo.
  3. Validació amb throw. Crea un mètode retirar(double saldo, double import) que llanci una excepció si l’import supera el saldo.
  4. Traça de pila. Provoca a posta una NullPointerException i una ArrayIndexOutOfBoundsException, i aprèn a llegir el missatge que apareix.
  5. Debug guiat. Escriu un bucle que sumi malament (amb un error lògic a posta) i troba’l amb el debugger, no llegint el codi.
  6. Documenta. Afegeix comentaris Javadoc complets a tres mètodes que hagis escrit en capítols anteriors.