A la lliçó anterior vas seguir el viatge d'una excepció des del punt en què es llança fins que arriba a la JVM sense que ningú l'atengui: el programa imprimeix un stack trace i mor. Ara interceptaràs aquest viatge. El bloc try-catch és el mecanisme amb què un mètode diu: «això que faré pot fallar, i si falla d'aquesta manera concreta, jo sé què fer».
És la construcció més usada del mòdul i també la més maltractada. Escriure try { ... } catch (Exception e) { e.printStackTrace(); } és tan fàcil que s'ha convertit en un reflex automàtic, i és gairebé sempre la pitjor decisió possible: captura massa, no arregla res i deixa el programa corrent amb l'estat trencat. Aquesta lliçó et dona la versió correcta: què es protegeix exactament dins d'un try, quin codi se salta quan salta l'excepció, com interrogar l'objecte capturat, per què l'ordre de diversos catch no és una qüestió d'estil sinó un requisit del compilador, i —el més important— què es pot fer de debò dins d'un catch més enllà d'imprimir l'error.
Al final tindràs la primera victòria tangible del mòdul: el menú interactiu que vas escriure a 02-06 deixarà de morir-se quan Marta Ruiz escrigui "dotze" on el programa esperava 12.
Contingut
- Sintaxi i semàntica del
try-catch - Què es protegeix i què se salta
- L'objecte excepció i els seus mètodes
- Múltiples blocs
catchi la regla d'ordre - Multi-catch amb
| - Blocs
tryimbricats - Abast de les variables declarades dins del
try - Què es pot fer dins d'un
catch - Estratègia 1: recuperar-se amb un valor per defecte
- Estratègia 2: reintentar
- Estratègia 3: traduir a una altra excepció
- Estratègia 4: registrar i rellançar
- Antipatrons, amb demostració
- El cost real d'una excepció enfront d'un
if - BiblioTech: el menú que ja no es trenca
- Errors Comuns i Consells
- Exercicis
- Sintaxi i semàntica del
try-catch
try-catchLa forma mínima:
try {
// Codi PROTEGIT: pot llancar excepcions
} catch (TipusDExcepcio e) {
// GESTOR: s'executa nomes si el try ha llancat una excepcio
// compatible amb TipusDExcepcio
}Tres regles de sintaxi que el compilador imposa:
- Les claus són obligatòries, fins i tot amb una sola sentència. A diferència d'
ifofor, aquí no hi ha versió sense claus. - Un
tryno pot anar sol. Ha de portar almenys uncatch, o unfinally(06-05), o ser untry-with-resources(06-06).try { ... }a seques no compila. - El paràmetre del
catches declara com el d'un mètode: tipus i nom. Per convenció s'anomenae,exo alguna cosa descriptiva comfalladaonoTrobat.
Un exemple complet amb BiblioTech:
package com.nexussoftware.bibliotech.presentacio;
public class PrimerTryCatch {
public static void main(String[] args) {
String entrada = "dotze"; // el que va escriure l'usuari
System.out.println("1. Abans del try");
try {
System.out.println("2. Dins del try, abans de convertir");
int dies = Integer.parseInt(entrada); // (*) llanca NumberFormatException
System.out.println("3. Dies convertits: " + dies); // NO s'executa
} catch (NumberFormatException e) {
System.out.println("4. Capturada: " + e.getMessage());
}
System.out.println("5. Despres del try-catch: el programa SEGUEIX VIU");
}
}Sortida:
1. Abans del try 2. Dins del try, abans de convertir 4. Capturada: For input string: "dotze" 5. Despres del try-catch: el programa SEGUEIX VIU
Compara això amb el que passava a 06-01 sense try: el programa moria i el missatge 5 no apareixia mai. Ara l'excepció s'ha aturat aquí: no continua pujant per la pila, el fil no mor, i l'execució continua normalment després del bloc.
La semàntica exacta, punt per punt:
- Si el
tryacaba sense excepció, tots elscatchs'ignoren completament. L'execució segueix a la línia posterior a l'últim bloc. - Si el
tryllança una excepció, l'execució deltrys'abandona en aquell instant —les línies següents no s'executen— i la JVM busca entre elscatchel primer el tipus del qual sigui compatible amb l'excepció llançada. - Si en troba un, executa el seu cos i després continua després del bloc complet. L'excepció queda consumida: deixa de propagar-se.
- Si no en troba cap de compatible, l'excepció continua pujant per la pila com si el
tryno existís, buscant gestor al mètode que ha cridat.
Aquest últim punt és crucial i s'oblida sovint: un try-catch que no captura el tipus llançat no fa absolutament res.
try {
Integer.parseInt("dotze"); // llanca NumberFormatException
} catch (ArithmeticException e) { // tipus INCOMPATIBLE
System.out.println("Mai no arriba aqui");
}
// La NumberFormatException es propaga igualment: el programa mor
- Què es protegeix i què se salta
La regla és simple però té conseqüències que convé veure dibuixades: s'abandona tot el que quedi del try a partir del punt de fallada, per profund que estigui.
package com.nexussoftware.bibliotech.presentacio;
import java.util.ArrayList;
import java.util.List;
public class QueEsSalta {
public static void main(String[] args) {
List<String> registrades = new ArrayList<>();
String[] entrades = { "15", "20", "dotze", "30" };
try {
for (String entrada : entrades) {
int dies = Integer.parseInt(entrada);
registrades.add(entrada + " -> " + dies + " dies");
System.out.println("Processada: " + entrada);
}
System.out.println("TOTES processades"); // no s'executa
} catch (NumberFormatException e) {
System.out.println("Fallada a: " + e.getMessage());
}
System.out.println("Registrades: " + registrades);
}
}Sortida:
Processada: 15 Processada: 20 Fallada a: For input string: "dotze" Registrades: [15 -> 15 dies, 20 -> 20 dies]
Fixa't en el que ha passat: l'excepció ha saltat dins del bucle, que era dins del try. S'ha abandonat la iteració en curs, s'ha abandonat el bucle sencer, s'ha abandonat la resta del try (inclòs el println("TOTES processades")) i s'ha saltat directament al catch. L'entrada "30", que era perfectament vàlida, no s'ha processat mai.
Això il·lustra una decisió de disseny que hauràs de prendre constantment: on posar el try. Si el que vols és que una dada dolenta no impedeixi processar les altres, el try ha d'estar dins del bucle, no fora:
for (String entrada : entrades) {
try {
int dies = Integer.parseInt(entrada);
registrades.add(entrada + " -> " + dies + " dies");
} catch (NumberFormatException e) {
System.out.println(" Descartada entrada invalida: " + entrada);
// continue implicit: la seguent iteracio segueix normalment
}
}
// Resultat: [15 -> 15 dies, 20 -> 20 dies, 30 -> 30 dies]Posició del try |
Semàntica | Quan usar-la |
|---|---|---|
| Fora del bucle | La primera fallada avorta tot el procés | Tot o res: una importació transaccional |
| Dins del bucle | Cada element falla per separat; els altres continuen | Processos tolerants: importar 1000 registres i descartar els trencats |
El flux complet, dibuixat:
flowchart TB
A["Entra al bloc try"] --> B["Executa sentencia"]
B --> C{"Llanca excepcio?"}
C -->|"no, i queden sentencies"| B
C -->|"no, i el try acaba"| G["Salta TOTS els catch"]
C -->|"si"| D["Abandona la resta del try<br/>(bucles i crides inclosos)"]
D --> E{"Hi ha un catch<br/>de tipus compatible?"}
E -->|"si"| F["Executa aquest catch<br/>L'excepcio queda consumida"]
E -->|"no"| H["L'excepcio SEGUEIX pujant<br/>per la pila de crides"]
F --> I["Continua despres del bloc"]
G --> I
I un matís important: el try protegeix també tot el que passi dins dels mètodes que es cridin des d'ell, per molts nivells que hi hagi. Si gestor.prestar(...) crida cataleg.cercar(...) que crida index.get(...) i allà es llança una excepció, el teu catch la rep. Això és precisament el que fa útil el mecanisme: pots gestionar en un sol lloc les fallades d'un subarbre sencer de crides.
- L'objecte excepció i els seus mètodes
El paràmetre del catch és una referència a l'objecte excepció, amb tot el seu estat disponible. Aquests són els seus mètodes i per a què serveix cadascun:
| Mètode | Què retorna | Ús típic |
|---|---|---|
getMessage() |
El missatge de detall, o null si no en té |
Compondre un missatge de log |
getLocalizedMessage() |
Igual, tret que la classe el sobreescrigui amb traducció | Missatges internacionalitzats |
toString() |
classe.completa: missatge |
Log compacte d'una línia |
getClass().getSimpleName() |
El nom curt de la classe | Distingir el tipus en un log |
printStackTrace() |
(void) Imprimeix el trace a System.err |
Només depuració local, mai producció |
getStackTrace() |
StackTraceElement[] amb els marcs |
Anàlisi programàtica de l'origen |
getCause() |
L'excepció que la va provocar, o null |
Baixar a la causa arrel |
getSuppressed() |
Throwable[] d'excepcions suprimides |
Només amb try-with-resources (06-06) |
initCause(Throwable) |
Estableix la causa a posteriori | Casos rars; millor el constructor (06-03) |
Un catch que els fa servir tots, perquè vegis la diferència entre ells:
package com.nexussoftware.bibliotech.presentacio;
public class AnatomiaDelCatch {
public static void main(String[] args) {
try {
calcularMultaEmpleat("EMP-004", "quinze");
} catch (NumberFormatException e) {
System.out.println("getMessage() : " + e.getMessage());
System.out.println("toString() : " + e);
System.out.println("getClass().getName(): " + e.getClass().getName());
System.out.println("getClass().getSimpleName(): " + e.getClass().getSimpleName());
System.out.println("getCause() : " + e.getCause());
System.out.println("getSuppressed().length: " + e.getSuppressed().length);
System.out.println("\n-- Marcs de la pila (getStackTrace) --");
StackTraceElement[] marcs = e.getStackTrace();
for (int i = 0; i < Math.min(4, marcs.length); i++) {
StackTraceElement m = marcs[i];
System.out.printf(" [%d] %s.%s (%s:%d)%n",
i, m.getClassName(), m.getMethodName(),
m.getFileName(), m.getLineNumber());
}
// Localitzar el nostre propi codi dins del trace
for (StackTraceElement m : marcs) {
if (m.getClassName().startsWith("com.nexussoftware")) {
System.out.println("\nPrimer marc propi: "
+ m.getMethodName() + " a la linia " + m.getLineNumber());
break;
}
}
}
}
static void calcularMultaEmpleat(String idEmpleat, String diesText) {
int dies = Integer.parseInt(diesText);
System.out.println("Multa: " + (dies * 0.25));
}
}Sortida:
getMessage() : For input string: "quinze" toString() : java.lang.NumberFormatException: For input string: "quinze" getClass().getName(): java.lang.NumberFormatException getClass().getSimpleName(): NumberFormatException getCause() : null getSuppressed().length: 0 -- Marcs de la pila (getStackTrace) -- [0] java.lang.NumberFormatException.forInputString (NumberFormatException.java:67) [1] java.lang.Integer.parseInt (Integer.java:665) [2] java.lang.Integer.parseInt (Integer.java:781) [3] com.nexussoftware.bibliotech.presentacio.AnatomiaDelCatch.calcularMultaEmpleat (AnatomiaDelCatch.java:38) Primer marc propi: calcularMultaEmpleat a la linia 38
Dues advertències sobre aquests mètodes:
getMessage() pot retornar null. Ho vas veure a 06-01: UnsupportedOperationException i ConcurrentModificationException solen llançar-se sense missatge. Si concatenes directament obtindràs "Error: null", que és pitjor que res. Protegeix-te:
O simplement fes servir e.toString(), que sempre inclou almenys el nom de la classe.
printStackTrace() no és logging. Escriu a System.err sense marca de temps, sense nivell, sense nom del fil, sense possibilitat de filtrar ni desactivar, i sense cap relació amb el sistema de registre de l'aplicació. Serveix per depurar a la teva màquina i per res més. A 06-07 el substituiràs per logger.log(Level.SEVERE, missatge, e), que fa el mateix però de manera utilitzable.
- Múltiples blocs
catch i la regla d'ordre
catch i la regla d'ordreUn mateix try pot portar diversos catch, cadascun per a un tipus diferent. La JVM els avalua en ordre de dalt a baix i executa el primer compatible, no el més específic.
package com.nexussoftware.bibliotech.presentacio;
import java.util.ArrayList;
import java.util.List;
public class DiversosCatch {
public static void main(String[] args) {
processar(args.length > 0 ? args[0] : "1");
}
static void processar(String cas) {
List<String> cataleg = new ArrayList<>(List.of("Java Eficac", "Patrons de Disseny"));
try {
switch (cas) {
case "1" -> System.out.println(Integer.parseInt("dotze")); // NumberFormat
case "2" -> System.out.println(cataleg.get(9)); // IndexOutOfBounds
case "3" -> System.out.println(30 / 0); // Arithmetic
default -> System.out.println(((String) null).length()); // NullPointer
}
} catch (NumberFormatException e) {
System.out.println("Entrada no numerica: " + e.getMessage());
} catch (IndexOutOfBoundsException e) {
System.out.println("Index fora de rang: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("Error aritmetic: " + e.getMessage());
} catch (RuntimeException e) {
// Xarxa de seguretat per a la resta de no comprovades
System.out.println("Una altra fallada en execucio: " + e);
}
}
}I ara la regla que no és opcional:
Els
catchs'han d'ordenar de la subclasse a la superclasse. Si uncatchd'un tipus més general precedeix un altre de més específic, el codi NO COMPILA.
Comprova-ho:
try {
Integer.parseInt("dotze");
} catch (RuntimeException e) { // MES GENERAL primer
System.out.println("General");
} catch (NumberFormatException e) { // error de compilacio
System.out.println("Especific");
}El compilador rebutja el codi:
error: exception NumberFormatException has already been caught
} catch (NumberFormatException e) {
^El raonament és transparent: NumberFormatException és una RuntimeException, així que el primer catch ja l'hauria atrapada. El segon bloc seria codi inabastable, i Java rebutja el codi inabastable en temps de compilació en comptes de deixar-te creure que funciona.
És una de les poques vegades en què el compilador et protegeix d'un error de lògica, i convé agrair-ho: en llenguatges sense aquesta comprovació, un catch ample col·locat per descuit al principi s'empassa silenciosament tots els gestors específics que vénen després.
La jerarquia rellevant per ordenar bé:
flowchart TB
RE["RuntimeException<br/>(el mes general dels habituals)"]
RE --> IAE["IllegalArgumentException"]
RE --> ISE["IllegalStateException"]
RE --> IOB["IndexOutOfBoundsException"]
RE --> NPE["NullPointerException"]
IAE --> NFE["NumberFormatException<br/>(el mes especific)"]
IOB --> AIOB["ArrayIndexOutOfBoundsException"]
N1["Ordre CORRECTE dels catch:<br/>NumberFormatException,<br/>despres IllegalArgumentException,<br/>despres RuntimeException"]
Compte amb el parany d'aquest diagrama: NumberFormatException descendeix d'IllegalArgumentException, no directament de RuntimeException. Per això aquest ordre tampoc no compila:
catch (IllegalArgumentException e) { ... }
catch (NumberFormatException e) { ... } // ja capturada per l'anteriorÉs un parentiu que sorprèn molt. Si captures IllegalArgumentException per a les teves validacions de domini (06-03), tingues present que també estàs capturant totes les fallades de conversió numèrica, cosa que pot confondre dos problemes molt diferents.
- Multi-catch amb
|
|Quan dues o més excepcions es gestionen exactament igual, Java 7 permet combinar-les en un sol catch amb la barra vertical |:
try {
int dies = Integer.parseInt(entrada);
Material m = materials.get(dies);
processar(m);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
System.out.println("Entrada invalida (" + e.getClass().getSimpleName() + "): " + e.getMessage());
}Sense multi-catch hauries de duplicar el cos o extreure'l a un mètode privat. Amb ell, la intenció queda clara: aquestes dues fallades signifiquen el mateix per a mi.
El multi-catch té dues regles pròpies que cal conèixer:
Regla 1: els tipus no poden estar relacionats per herència.
// NO COMPILA
catch (NumberFormatException | IllegalArgumentException e) { ... }
// error: Alternatives in a multi-catch statement cannot be related by subclassing
// Alternative NumberFormatException is a subclass of IllegalArgumentExceptionTé sentit: si captures IllegalArgumentException, la NumberFormatException ja hi està inclosa. Anomenar-la un altre cop seria redundant i probablement indicaria un malentès sobre la jerarquia.
Regla 2: la variable és implícitament final.
try {
// ...
} catch (NumberFormatException | ArithmeticException e) {
e = new RuntimeException("una altra cosa"); // NO COMPILA
// error: multi-catch parameter e may not be assigned
}En un catch normal sí que pots reassignar el paràmetre (encara que gairebé mai no hauries de fer-ho); en un multi-catch no. La raó és tècnica: el tipus estàtic d'e és el supertipus comú més proper de les alternatives, i permetre la reassignació complicaria l'anàlisi de tipus del compilador. A la pràctica, la restricció no molesta: reassignar el paràmetre d'un catch és una mala idea de totes maneres.
Quin tipus té e en un multi-catch. És l'avantpassat comú més proper de totes les alternatives. Amb NumberFormatException | IndexOutOfBoundsException, l'avantpassat comú és RuntimeException, així que dins del bloc només pots cridar els mètodes de RuntimeException (que són els de Throwable). Si necessites un mètode específic d'una d'elles, necessites catch separats o un instanceof:
} catch (NumberFormatException | java.io.IOException e) {
// Aqui el tipus comu es Exception: nomes metodes de Throwable
System.out.println(e.getMessage());
// Si necessites alguna cosa especifica:
if (e instanceof java.io.IOException io) {
System.out.println("Fallada d'E/S, es reintentara mes tard");
}
}Quan fer servir cada forma:
| Situació | Forma |
|---|---|
| Mateix tractament per a diversos tipus no emparentats | Multi-catch A | B |
| Tractament diferent per tipus | catch separats, de l'específic al general |
| Un tipus i tots els seus descendents | Un sol catch de la superclasse |
- Blocs
try imbricats
try imbricatsUn try pot contenir un altre try, i la cerca de gestor procedeix de dins cap a fora: primer els catch del try intern, i només si cap no és compatible es proven els de l'extern.
package com.nexussoftware.bibliotech.presentacio;
public class TryImbricats {
public static void main(String[] args) {
String[] linies = { "LIB-0001;15", "LIB-0002;dotze", "LIB-0003" };
try {
System.out.println("== Importacio del cataleg ==");
for (String linia : linies) {
String[] parts = linia.split(";");
String referencia = parts[0];
// TRY INTERN: un dia invalid no ha d'avortar la importacio
int dies;
try {
dies = Integer.parseInt(parts[1]);
} catch (NumberFormatException e) {
System.out.println(" " + referencia + ": dies invalids, s'usa 15 per defecte");
dies = 15;
}
System.out.println(" " + referencia + " -> " + dies + " dies");
}
System.out.println("Importacio acabada");
} catch (ArrayIndexOutOfBoundsException e) {
// TRY EXTERN: una linia sense ';' SI que avorta la importacio (format corrupte)
System.out.println("FITXER CORRUPTE: falta un camp. " + e.getMessage());
}
}
}Sortida:
== Importacio del cataleg == LIB-0001 -> 15 dies LIB-0002: dies invalids, s'usa 15 per defecte LIB-0002 -> 15 dies FITXER CORRUPTE: falta un camp. Index 1 out of bounds for length 1
Aquest exemple mostra exactament quan té sentit imbricar: quan dues fallades diferents mereixen dos àmbits de recuperació diferents. Un número mal escrit és recuperable al lloc (fem servir un valor per defecte i seguim); una línia sense el separador significa que el fitxer sencer és sospitós i cal avortar.
Dit això, la imbricació és fàcil d'abusar. Dos o tres nivells fan el codi il·legible. Alternatives gairebé sempre preferibles:
- Extreure el
tryintern a un mètode propi amb nom descriptiu (llegirDiesODefecte(String)), cosa que a més documenta la política de recuperació. - Fer servir
try-with-resources(06-06) si la imbricació venia de tancar recursos.
- Abast de les variables declarades dins del
try
tryUn detall de sintaxi que causa errors de compilació constants a qui comença:
try {
int dies = Integer.parseInt(entrada); // declarada DINS del try
} catch (NumberFormatException e) {
System.out.println("Fallada");
}
System.out.println(dies); // NO COMPILA: cannot find symbolLes claus del try són un bloc com qualsevol altre: el que es declara a dins només existeix a dins. Si necessites la variable després, declara-la fora:
int dies; // declarada fora
try {
dies = Integer.parseInt(entrada); // assignada dins
} catch (NumberFormatException e) {
dies = DIES_PRESTEC; // valor per defecte al catch
}
System.out.println("Dies: " + dies); // compila i funcionaI aquí hi ha una subtilesa que sí que convé entendre bé. Aquest codi no compila:
int dies;
try {
dies = Integer.parseInt(entrada);
} catch (NumberFormatException e) {
System.out.println("Fallada"); // el catch NO assigna dies
}
System.out.println(dies); // error: variable dies might not have been initializedEl compilador aplica anàlisi d'assignació definitiva: per fer servir dies ha d'estar segur que s'ha assignat en tots els camins possibles. Si el try falla i el catch no assigna res, dies es queda sense valor. El compilador ho detecta i s'hi nega.
Tens tres sortides legals:
// A) Inicialitzar a la declaracio
int dies = DIES_PRESTEC;
try { dies = Integer.parseInt(entrada); } catch (NumberFormatException e) { /* es queda el default */ }
// B) Assignar tambe al catch
int dies;
try { dies = Integer.parseInt(entrada); } catch (NumberFormatException e) { dies = DIES_PRESTEC; }
// C) Sortir del metode al catch (return o throw): llavors no hi ha cami sense assignar
int dies;
try { dies = Integer.parseInt(entrada); } catch (NumberFormatException e) { return; }
System.out.println(dies); // compila: si arribem aqui, dies te valorL'opció B és la més explícita i la que millor documenta la intenció: es veu d'un cop d'ull quin és el valor de recuperació. L'A és més compacta però pot confondre qui llegeix, perquè el valor per defecte queda lluny del catch.
- Què es pot fer dins d'un
catch
catchAquesta és la pregunta que separa la gestió d'excepcions real del ritual buit. Un catch no hi és per "fer alguna cosa amb l'error": hi és per prendre una decisió. I les decisions sensates són quatre.
| Estratègia | Quan | Risc |
|---|---|---|
| Recuperar-se amb un valor per defecte | Existeix una alternativa raonable i el programa pot continuar correctament | Amagar un problema real sota un valor plausible |
| Reintentar | La fallada és transitòria (xarxa, bloqueig, fitxer ocupat) | Bucle infinit si no es limita |
| Traduir a una altra excepció | La fallada ha de pujar, però amb el vocabulari d'aquesta capa | Perdre la causa original |
| Registrar i rellançar | Aquí només es pot documentar; la decisió és de dalt | Registrar dues vegades el mateix error |
I una cinquena, que gairebé mai no és correcta però existeix: no fer res, quan la fallada és genuïnament irrellevant. Requereix un comentari obligatori que expliqui per què.
El que no és una estratègia: e.printStackTrace() a seques. Això no decideix res; només escup text i deixa el programa continuar com si res no hagués passat, amb l'estat possiblement trencat.
Vegem les quatre amb codi real.
- Estratègia 1: recuperar-se amb un valor per defecte
La més freqüent a la frontera amb l'usuari o amb la configuració. La fallada té una alternativa raonable i documentada:
package com.nexussoftware.bibliotech.presentacio;
/** Conversio tolerant per a l'entrada per consola de BiblioTech. */
public final class EntradaSegura {
private EntradaSegura() { } // classe d'utilitat: no s'instancia
/**
* Converteix text a enter. Si no es convertible, retorna el valor per defecte.
*
* Aquesta es la versio "tolerant", pensada per a la CAPA DE PRESENTACIO,
* on una entrada dolenta de l'usuari es una cosa normal i no una fallada del sistema.
* A la capa de domini la politica sera la contraria: llancar (06-03).
*/
public static int aEnterODefecte(String text, int perDefecte) {
if (text == null || text.isBlank()) {
return perDefecte;
}
try {
return Integer.parseInt(text.trim());
} catch (NumberFormatException e) {
// Recuperacio: el valor per defecte es una resposta VALIDA aqui.
// S'informa l'usuari perque no cregui que la seva dada s'ha usat.
System.out.println(" [avis] '" + text + "' no es un numero. S'usa " + perDefecte);
return perDefecte;
}
}
public static double aDecimalODefecte(String text, double perDefecte) {
if (text == null || text.isBlank()) {
return perDefecte;
}
try {
// S'accepta la coma decimal, molt habitual en teclejar en catala
return Double.parseDouble(text.trim().replace(',', '.'));
} catch (NumberFormatException e) {
System.out.println(" [avis] '" + text + "' no es un decimal. S'usa " + perDefecte);
return perDefecte;
}
}
public static void main(String[] args) {
System.out.println(aEnterODefecte("15", 15)); // 15
System.out.println(aEnterODefecte("dotze", 15)); // avis + 15
System.out.println(aEnterODefecte(" 20 ", 15)); // 20 (trim)
System.out.println(aEnterODefecte(null, 15)); // 15
System.out.println(aDecimalODefecte("0,25", 0.25)); // 0.25 (coma admesa)
}
}La condició perquè aquesta estratègia sigui legítima: el valor per defecte ha de ser una resposta correcta, no una tapadora. Retornar 15 quan l'usuari escriu "dotze" en un camp de dies de préstec és defensable, perquè 15 és el termini estàndard de BiblioTech i a més se l'avisa. Retornar 0 com a saldo de multa quan falla el càlcul no ho és: estaria inventant una dada de negoci.
- Estratègia 2: reintentar
Quan la fallada és transitòria —una lectura de xarxa que es talla, un fitxer temporalment bloquejat, un servei que respon tard—, reintentar és la resposta correcta. Amb tres condicions innegociables: límit d'intents, espera entre ells i fallada definitiva en esgotar-los.
package com.nexussoftware.bibliotech.servei;
/**
* Reintent amb espera creixent (backoff) per a una operacio transitoria.
*
* Nota: l'operacio se simula. El cas real (llegir un fitxer, cridar un
* servei) arriba als moduls 7 i 9; el que importa aqui es l'ESTRUCTURA.
*/
public class SincronitzadorCataleg {
private static final int MAX_INTENTS = 4;
private static final long ESPERA_BASE_MS = 200;
private int crides = 0;
/** Simula una operacio que falla les dues primeres vegades i despres funciona. */
private String descarregarCatalegRemot() {
crides++;
if (crides < 3) {
throw new IllegalStateException("Servidor no disponible (intent " + crides + ")");
}
return "3 materials sincronitzats";
}
public String sincronitzar() {
IllegalStateException ultimaFallada = null;
for (int intent = 1; intent <= MAX_INTENTS; intent++) {
try {
String resultat = descarregarCatalegRemot();
System.out.println("OK a l'intent " + intent + ": " + resultat);
return resultat; // exit: se surt del bucle
} catch (IllegalStateException e) {
ultimaFallada = e; // es guarda per a la fallada definitiva
System.out.println("Intent " + intent + " fallit: " + e.getMessage());
if (intent == MAX_INTENTS) {
break; // no esperar despres de l'ultim intent
}
long espera = ESPERA_BASE_MS * (1L << (intent - 1)); // 200, 400, 800 ms
System.out.println(" Esperant " + espera + " ms abans de reintentar...");
try {
Thread.sleep(espera);
} catch (InterruptedException ie) {
// Bona practica del modul 8: restaurar el marcador d'interrupcio
// i abandonar. Mai no s'ha d'empassar una InterruptedException.
Thread.currentThread().interrupt();
throw new IllegalStateException("Sincronitzacio interrompuda", ie);
}
}
}
// Esgotats els intents: fallar de manera clara, conservant la causa (06-03)
throw new IllegalStateException(
"No s'ha pogut sincronitzar el cataleg despres de " + MAX_INTENTS + " intents",
ultimaFallada);
}
public static void main(String[] args) {
System.out.println(new SincronitzadorCataleg().sincronitzar());
}
}Sortida:
Intent 1 fallit: Servidor no disponible (intent 1) Esperant 200 ms abans de reintentar... Intent 2 fallit: Servidor no disponible (intent 2) Esperant 400 ms abans de reintentar... OK a l'intent 3: 3 materials sincronitzats 3 materials sincronitzats
Quatre detalls que fan que aquest patró sigui correcte i no una font de desastres:
- Límit explícit (
MAX_INTENTS). Unwhile (true)amb reintents és una bomba: si el servei està caigut de debò, el programa gira per sempre consumint CPU. - Espera creixent (exponential backoff): 200, 400, 800 ms. Reintentar immediatament en bucle agreuja la caiguda del servei remot, perquè li afegeixes càrrega justament quan està en apurs.
- En esgotar els intents, es falla. No es retorna
nullni un valor buit fingint èxit. I es conserva l'última fallada com a causa, perquè el stack trace mostri elCaused by:. - Només es reintenten fallades transitòries. Reintentar una
NumberFormatExceptiono unaIllegalArgumentExceptionés absurd: la dada continuarà sent invàlida al quart intent. La regla: reintenta el que depèn de l'entorn, mai el que depèn de les dades.
- Estratègia 3: traduir a una altra excepció
Quan la fallada ha de continuar pujant, però expressada amb el vocabulari de la teva capa. Un Cataleg no hauria d'obligar la capa de presentació a entendre d'índexs de mapes ni de formats de fitxer.
// A la capa de servei de BiblioTech
public Material carregarMaterialDesDeLinia(String linia) {
try {
String[] camps = linia.split(";");
String referencia = camps[0];
int any = Integer.parseInt(camps[2]);
return new Llibre(referencia, camps[1], any);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
// TRADUCCIO: qui crida no te per que saber que a dins
// hi havia un split ni un parseInt. El que li importa es que
// aquesta linia del cataleg esta mal formada.
throw new IllegalArgumentException(
"Linia de cataleg mal formada: '" + linia + "'", e); // <-- la causa, conservada
}
}La paraula clau és el segon argument del constructor: e. És la causa, i és el que produeix el Caused by: del stack trace que vas estudiar a 06-01. Sense ell, la informació de què ha fallat exactament desapareix.
La tècnica completa —encadenament d'excepcions, traducció entre capes, quan embolcallar i quan rellançar— és el cor de la lliçó 06-03, i les excepcions de domini a les quals traduiràs (MaterialNoTrobatException i companyia) arriben a 06-04. Aquí n'hi ha prou que retinguis la forma i el motiu.
- Estratègia 4: registrar i rellançar
Quan en aquest punt només pots deixar constància, però la decisió de què fer correspon a una capa superior:
public void processarDevolucio(String referencia, int dia) {
try {
registre.retornar(referencia, dia);
} catch (RuntimeException e) {
// Es deixa constancia AMB el context que nomes es coneix aqui...
System.err.println("Fallada en retornar " + referencia + " el dia " + dia);
// ...i es deixa pujar perque a dalt decideixin.
throw e; // rellancar la MATEIXA excepcio
}
}throw e; rellança l'objecte original intacte: mateixa classe, mateix missatge, mateixa pila. No s'afegeix un marc nou al stack trace, així que la línia de la fallada original es conserva.
El perill d'aquest patró té nom: el log duplicat. Si cada capa registra i rellança, una sola fallada apareix cinc vegades al registre amb cinc stack traces gairebé idèntics, i trobar la primera ocurrència es torna una tasca arqueològica. La regla que aplicaràs a 06-07:
Registra on gestiones. Si vas a rellançar, no registris el stack trace complet: com a molt, afegeix context.
- Antipatrons, amb demostració
Els tres que fan més mal, amb la demostració de per què.
El catch buit
package com.nexussoftware.bibliotech.presentacio;
import java.util.ArrayList;
import java.util.List;
public class DemoCatchBuit {
static List<String> registrats = new ArrayList<>();
/** VERSIO DOLENTA: el catch buit destrueix la informacio de la fallada. */
static void registrarMalament(String linia) {
try {
String[] parts = linia.split(";");
int copies = Integer.parseInt(parts[1]);
for (int i = 0; i < copies; i++) {
registrats.add(parts[0] + "-" + i);
}
} catch (Exception e) {
// buit a proposit
}
}
public static void main(String[] args) {
registrarMalament("LIB-0001;2");
registrarMalament("LIB-0002;dos"); // falla... i NINGU no se n'assabenta
registrarMalament("LIB-0003"); // falla... i NINGU no se n'assabenta
registrarMalament("LIB-0004;1");
System.out.println("Registrats: " + registrats);
System.out.println("Total esperat: 6 exemplars. Total real: " + registrats.size());
}
}Sortida:
Això és exactament el que passa en producció: el programa acaba sense error, amb un resultat incorrecte i sense la més mínima pista de què ha passat. No hi ha stack trace, no hi ha log, no hi ha codi de sortida diferent de zero. Una fallada així pot viure mesos en un sistema, corrompent dades a poc a poc, fins que algú nota que els números no quadren. I per llavors no queda cap rastre a seguir.
Un catch buit és pitjor que no capturar res. Sense catch, el programa cau sorollosament i saps exactament on. Amb ell, menteixes.
El catch (Exception e) massa ample
// MALAMENT: la xarxa s'endu peixos que no voliem pescar
try {
Material m = cataleg.cercarPerReferencia(referencia);
int dies = Integer.parseInt(entradaUsuari);
gestor.prestar(m, empleat, dies);
} catch (Exception e) {
System.out.println("Entrada invalida, torneu-ho a provar");
}El missatge diu "entrada invàlida", però aquell catch atrapa també un NullPointerException per un bug teu, un IllegalStateException perquè el registre està corrupte i qualsevol fallada de programació de la resta del bloc. L'usuari rebrà "entrada invàlida" davant d'un defecte greu del sistema, i tornarà a teclejar la seva dada correcta una vegada i una altra preguntant-se què fa malament.
La versió correcta captura el que sap gestionar i deixa pujar la resta:
try {
Material m = cataleg.cercarPerReferencia(referencia);
int dies = Integer.parseInt(entradaUsuari);
gestor.prestar(m, empleat, dies);
} catch (NumberFormatException e) {
System.out.println("El nombre de dies no es valid: " + entradaUsuari);
}
// Un NullPointerException puja i es veu. Es un bug, i s'ha de notar.La regla: captura el tipus més específic que sàpigues gestionar. Un catch (Exception e) només és legítim a la frontera d'errors de l'aplicació —el gestor global del main—, que és on de debò cal impedir que alguna cosa escapi sense registrar-se. Això ho muntaràs a 06-07.
Fer servir excepcions per al flux normal
// PROHIBIT: recorrer provocant l'excepcio de final
try {
int i = 0;
while (true) {
System.out.println(materials.get(i++).getTitol());
}
} catch (IndexOutOfBoundsException fi) {
// "ja hem acabat"
}A més de ser fosc, és lent. I això té números.
- El cost real d'una excepció enfront d'un
if
ifLa part cara d'una excepció no és llançar-la ni capturar-la: és construir-la, perquè el constructor de Throwable crida fillInStackTrace(), que recorre i copia la pila de crides completa. Com més profunda sigui la pila, més costa.
package com.nexussoftware.bibliotech.presentacio;
/**
* Comparativa illustrativa: comprovar amb if enfront de provocar i capturar.
* ATENCIO: es un mesurament casola. Per mesurar de debo cal JMH (11-07),
* perque el JIT distorsiona els micro-benchmarks manuals.
*/
public class CostDeLesExcepcions {
private static final int VOLTES = 1_000_000;
public static void main(String[] args) {
String[] entrades = new String[VOLTES];
for (int i = 0; i < VOLTES; i++) {
entrades[i] = (i % 2 == 0) ? String.valueOf(i) : "no-numero"; // 50% invalides
}
// A) Comprovar abans amb una validacio manual
long t0 = System.nanoTime();
int validsA = 0;
for (String s : entrades) {
if (esEnterSimple(s)) { validsA++; }
}
long tA = (System.nanoTime() - t0) / 1_000_000;
// B) Intentar i capturar l'excepcio
long t1 = System.nanoTime();
int validsB = 0;
for (String s : entrades) {
try {
Integer.parseInt(s);
validsB++;
} catch (NumberFormatException e) {
// ignorat a proposit per al mesurament
}
}
long tB = (System.nanoTime() - t1) / 1_000_000;
// C) Excepcio SENSE stack trace (veure 06-04): quant costa nomes el mecanisme
long t2 = System.nanoTime();
int validsC = 0;
for (String s : entrades) {
try {
if (!esEnterSimple(s)) { throw SENSE_TRACA; }
validsC++;
} catch (RuntimeException e) {
// ignorat
}
}
long tC = (System.nanoTime() - t2) / 1_000_000;
System.out.println("A) if previ : " + tA + " ms (" + validsA + ")");
System.out.println("B) try/catch amb parseInt : " + tB + " ms (" + validsB + ")");
System.out.println("C) excepcio sense stack trace : " + tC + " ms (" + validsC + ")");
}
/** Excepcio preconstruida i SENSE stack trace: gairebe gratis de llancar. */
private static final RuntimeException SENSE_TRACA =
new RuntimeException("no numeric", null, false, false);
private static boolean esEnterSimple(String s) {
if (s == null || s.isEmpty()) { return false; }
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (i == 0 && (c == '-' || c == '+') && s.length() > 1) { continue; }
if (c < '0' || c > '9') { return false; }
}
return true;
}
}Resultats típics (una màquina qualsevol; els teus variaran, però l'ordre de magnitud es manté):
A) if previ : 12 ms (500000) B) try/catch amb parseInt : 890 ms (500000) C) excepcio sense stack trace : 18 ms (500000)
| Enfocament | Temps relatiu | Interpretació |
|---|---|---|
A) if previ |
1x | El cost base |
| B) Excepció completa | ~70x | Dominat per fillInStackTrace |
| C) Excepció sense traça | ~1,5x | El mecanisme de throw/catch en si és barat |
Les conclusions correctes —i una d'incorrecta que cal descartar:
- El
throw/catchen si no és car. El car és capturar la pila en construir l'objecte. La comparació A contra C ho demostra. - Per tant, fer servir excepcions per al que és excepcional no té cost apreciable. Si un error passa un cop cada mil operacions, aquests 900 nanosegons són irrellevants.
- Fer-les servir per al flux normal sí que en té. Un milió d'excepcions en un bucle és un problema mesurable.
- Conclusió incorrecta que NO has de treure: "les excepcions són lentes, millor retornar
false". L'eix de la decisió és la claredat i la correcció, no aquests nanosegons. Només en un bucle molt calent amb fallades molt freqüents la diferència comença a importar, i per a aquest cas rar existeix el truc del constructorwritableStackTrace = falseque veuràs a 06-04.
Un avís metodològic: aquest benchmark casolà és orientatiu. El JIT pot eliminar codi mort, alinear mètodes i esbiaixar el resultat. Per mesurar de debò s'usa JMH (11-07). El que sí que és sòlid aquí és la comparació relativa entre B i C, perquè aïlla el factor que domina.
- BiblioTech: el menú que ja no es trenca
És hora de cobrar la promesa. Aquest era el problema del menú de 02-06:
// Versio del modul 2: l'usuari escriu "dotze" i l'aplicacio MOR
System.out.print("Opcio: ");
int opcio = Integer.parseInt(scanner.nextLine()); // NumberFormatException -> adeuI aquesta és la versió amb gestió d'errors. Fixa't que la política és diferent segons el tipus de dada: l'opció del menú es reintenta (l'usuari hi és, pot corregir), i les dades numèriques amb valor raonable es recuperen amb un defecte.
package com.nexussoftware.bibliotech.presentacio;
import java.util.Scanner;
/**
* Menu de BiblioTech resistent a l'entrada de l'usuari.
*
* Politica d'errors d'aquesta classe (capa de presentacio):
* - Opcio del menu invalida -> reintentar fins que sigui valida.
* - Dada numerica invalida -> reintentar amb limit; si s'esgota, cancellar l'operacio.
* - Qualsevol altra fallada -> es deixa pujar (encara). A 06-07 es gestionara a la frontera.
*/
public class MenuBiblioTech {
private static final int MAX_REINTENTS = 3;
private static final int DIES_PRESTEC = 15;
private final Scanner scanner = new Scanner(System.in);
public void arrencar() {
boolean sortir = false;
while (!sortir) {
mostrarMenu();
int opcio = llegirOpcio(0, 4);
switch (opcio) {
case 1 -> llistarCataleg();
case 2 -> prestarMaterial();
case 3 -> retornarMaterial();
case 4 -> calcularMulta();
case 0 -> { sortir = true; System.out.println("Fins aviat."); }
default -> System.out.println("Opcio no contemplada.");
}
}
}
private void mostrarMenu() {
System.out.println("""
===== BiblioTech - Nexus Software =====
1. Llistar cataleg
2. Prestar material
3. Retornar material
4. Calcular multa
0. Sortir
=======================================""");
}
/**
* Llegeix una opcio valida, reintentant indefinidament.
*
* El reintent sense limit es acceptable AQUI perque hi ha una persona a l'altra
* banda que pot corregir. En un proces per lots seria un bucle infinit:
* la mateixa linia invalida es rellegiria per sempre.
*/
private int llegirOpcio(int min, int max) {
while (true) {
System.out.print("Opcio: ");
String linia = scanner.nextLine();
try {
int valor = Integer.parseInt(linia.trim());
if (valor < min || valor > max) {
System.out.println(" Ha d'estar entre " + min + " i " + max + ". Reintenteu.");
continue; // dada valida com a numero, invalida com a opcio
}
return valor;
} catch (NumberFormatException e) {
// RECUPERACIO PER REINTENT: s'informa amb la dada culpable
System.out.println(" '" + linia + "' no es un numero. Escriviu un digit de "
+ min + " a " + max + ".");
}
}
}
/**
* Llegeix un enter amb limit de reintents.
* Retorna -1 si l'usuari esgota els intents, perque qui el cridi cancelli.
*
* (Aquest -1 encara es un "codi de retorn" dels que va criticar 06-01. Es
* acceptable dins d'una mateixa classe de presentacio; a la capa de
* servei se substituira per excepcions de domini a 06-04.)
*/
private int llegirEnterAmbLimit(String peticio, int min, int max) {
for (int intent = 1; intent <= MAX_REINTENTS; intent++) {
System.out.print(peticio + ": ");
String linia = scanner.nextLine();
try {
int valor = Integer.parseInt(linia.trim());
if (valor < min || valor > max) {
System.out.printf(" Fora de rang [%d..%d]. Intent %d de %d.%n",
min, max, intent, MAX_REINTENTS);
continue;
}
return valor;
} catch (NumberFormatException e) {
System.out.printf(" '%s' no es un numero. Intent %d de %d.%n",
linia, intent, MAX_REINTENTS);
}
}
System.out.println(" Massa intents fallits. Operacio cancellada.");
return -1;
}
private double llegirDecimalODefecte(String peticio, double perDefecte) {
System.out.print(peticio + " [" + perDefecte + "]: ");
String linia = scanner.nextLine().trim();
if (linia.isEmpty()) {
return perDefecte; // Enter directe: accepta el valor proposat
}
try {
return Double.parseDouble(linia.replace(',', '.'));
} catch (NumberFormatException e) {
System.out.println(" Valor no numeric. S'usa " + perDefecte);
return perDefecte;
}
}
private void llistarCataleg() {
System.out.println(" (cataleg: 3 materials)");
}
private void prestarMaterial() {
System.out.print("Referencia del material: ");
String referencia = scanner.nextLine().trim();
int dies = llegirEnterAmbLimit("Dies de prestec (1-30)", 1, 30);
if (dies == -1) {
return; // cancellat: no es toca l'estat
}
System.out.println(" Prestat " + referencia + " durant " + dies + " dies.");
}
private void retornarMaterial() {
System.out.print("Referencia a retornar: ");
String referencia = scanner.nextLine().trim();
int dia = llegirEnterAmbLimit("Dia de devolucio (1-365)", 1, 365);
if (dia == -1) {
return;
}
System.out.println(" Retornat " + referencia + " el dia " + dia + ".");
}
private void calcularMulta() {
int diesRetard = llegirEnterAmbLimit("Dies de retard (0-200)", 0, 200);
if (diesRetard == -1) {
return;
}
double tarifa = llegirDecimalODefecte("Tarifa diaria", 0.25);
double multa = Math.min(diesRetard * tarifa, 20.0); // MULTA_MAXIMA
System.out.printf(" Multa: %.2f EUR (termini estandard %d dies)%n", multa, DIES_PRESTEC);
}
public static void main(String[] args) {
new MenuBiblioTech().arrencar();
}
}Sessió d'exemple:
===== BiblioTech - Nexus Software ===== 1. Llistar cataleg ... Opcio: dos 'dos' no es un numero. Escriviu un digit de 0 a 4. Opcio: 9 Ha d'estar entre 0 i 4. Reintenteu. Opcio: 4 Dies de retard (0-200): molts 'molts' no es un numero. Intent 1 de 3. Dies de retard (0-200): 22 Tarifa diaria [0.25]: Multa: 5.50 EUR (termini estandard 15 dies)
Tres decisions de disseny que val la pena assenyalar, perquè són les que separen aquest codi del "posar un try per si de cas":
- La política d'error depèn del context, no del tipus d'excepció. La mateixa
NumberFormatExceptiones tracta de tres maneres diferents: reintent infinit per a l'opció del menú (hi ha un humà), reintent limitat per a les dades (evita quedar-se encallat), i valor per defecte per a la tarifa (hi ha un valor raonable). - La cancel·lació deixa l'estat intacte. Quan
llegirEnterAmbLimitesgota els intents,prestarMaterialfareturnabans de tocar res. No hi ha préstec a mitges. És un avançament de la coherència d'estat que resoldràs del tot a 06-05. - Es captura només
NumberFormatException, noException. Si dins del menú aparegués unNullPointerExceptionper un bug, pujaria i es veuria. Amagar-lo sota "entrada invàlida" seria mentir a l'usuari i a tu mateix.
Queda una fragilitat evident que aquesta lliçó no pot resoldre: la capa de servei (GestorPrestecs, Cataleg) continua retornant null i false. Per arreglar això cal llançar excepcions, no només capturar-les. És la lliçó següent.
Errors Comuns i Consells
Capturar un tipus que el try no pot llançar. Amb excepcions comprovades, el compilador ho rebutja (exception X is never thrown in body of corresponding try statement). Amb les no comprovades, compila silenciosament i el catch no s'executa mai: és codi mort que dona falsa sensació de seguretat.
Posar catch (Exception e) per costum. Atrapa fallades que no saps gestionar i converteix bugs en missatges tranquil·litzadors. Captura el tipus més específic i deixa pujar la resta.
Invertir l'ordre dels catch. El general abans que l'específic no compila: exception X has already been caught. Recorda que NumberFormatException descendeix d'IllegalArgumentException, un parentiu que sorprèn.
Posar el try al voltant del bucle quan volies que cada element fallés per separat. La primera fallada avorta la resta del procés. Si vols tolerància element a element, el try va dins.
Fer servir una variable declarada dins del try després del bloc. No existeix fora. I si la declares fora però només l'assignes dins del try, el compilador exigirà que el catch també l'assigni, o que surti del mètode.
e.printStackTrace() com a gestió. No és gestió, és un abocament de text a System.err. En producció es perd, no es pot filtrar i no porta ni marca de temps ni context. A 06-07 el substitueixes per un Logger.
Concatenar e.getMessage() sense comprovar null. Diverses excepcions estàndard no porten missatge, i obtindràs "Error: null". Fes servir e.toString() o un valor alternatiu.
Reintentar una fallada que no és transitòria. Reintentar una NumberFormatException amb la mateixa dada és girar en el buit. Reintenta el que depèn de l'entorn; mai el que depèn de les dades.
Reintentar sense límit en un procés automàtic. Amb un humà davant és acceptable; en un procés per lots és un bucle infinit garantit.
Empassar-se una InterruptedException. Si captures una interrupció i no fas res, trenques el mecanisme de cancel·lació de fils del mòdul 8. El mínim és Thread.currentThread().interrupt() abans de sortir.
Consell: un catch que no pren cap decisió és un catch mal posat. Recuperar-se, reintentar, traduir o registrar i rellançar. Si no fas cap de les quatre, probablement no havies de capturar allà.
Consell: quan informis l'usuari, inclou la dada culpable. "'dotze' no es un numero" orienta; "Entrada invalida" no. El cost és el mateix.
Consell: comenta tot catch que quedi buit a propòsit. Si de debò la fallada és irrellevant, escriu per què. Un catch buit sense comentari és indistingible d'un oblit, i el següent que llegeixi el codi no sabrà si arreglar-lo.
Exercicis
Exercici 1: importador tolerant del catàleg
Escriu ImportadorCataleg a com.nexussoftware.bibliotech.servei, que rebi un String[] de línies amb el format referencia;titol;any;exemplars i produeixi un informe d'importació.
Requisits:
- Processa cada línia de manera independent: un error en una no ha d'impedir processar les altres.
- Tracta de manera diferent almenys tres tipus d'error:
- Falta algun camp →
ArrayIndexOutOfBoundsException→ es descarta la línia amb motiu. - L'any o els exemplars no són numèrics →
NumberFormatException→ es descarta amb motiu. - L'any està fora de
[1450, 2100]o els exemplars són<= 0→ no hi ha excepció: valida amb unifi descarta amb motiu.
- Falta algun camp →
- Fes servir multi-catch on el tractament sigui idèntic, i
catchseparats on no ho sigui. - Retorna un
record InformeImportacio(int acceptades, int rebutjades, List<String> motius). - Afegeix un comptador de línies processades que sigui correcte fins i tot quan fallen.
Prova amb almenys: una línia correcta, una sense camps suficients, una amb any "mil nou-cents", una amb any 1200, una amb 0 exemplars i una línia buida.
Exercici 2: caça de l'antipatró
Aquest mètode existeix de debò al BiblioTech heretat. Té sis defectes relacionats amb la gestió d'excepcions. Troba'ls, explica el mal que fa cadascun i reescriu el mètode correctament.
public double calcularMultaTotal(List<String> referencies, Map<String, Integer> diesRetard) {
double total = 0;
try {
for (String ref : referencies) {
try {
int dies = diesRetard.get(ref);
if (dies > 0) {
total += dies * 0.25;
}
} catch (Exception e) {
}
}
int mitjana = (int) total / referencies.size();
System.out.println("Mitjana: " + mitjana);
} catch (Throwable t) {
t.printStackTrace();
return 0;
}
return total;
}Exercici 3: lector de configuració amb política per clau
Escriu ConfiguracioBiblioTech, que llegeixi paràmetres d'un Map<String, String> (simulant un fitxer de propietats, que arriba a 07-07) i apliqui una política d'error diferent segons la criticitat de cada paràmetre:
llegirEnterObligatori(clau): si falta o no és numèric, llançaIllegalStateExceptionamb un missatge que identifiqui la clau i el valor trobat, conservant la causa quan n'hi hagi.llegirEnterOpcional(clau, perDefecte): si falta o no és numèric, retorna el defecte i informa per consola.llegirDecimalAmbRang(clau, min, max, perDefecte): a més de convertir, valida el rang; fora de rang o no numèric, retorna el defecte avisant del motiu concret (distingeix "no numèric" de "fora de rang").llegirBoolea(clau, perDefecte): accepta"true"/"false"/"si"/"no"/"1"/"0"sense distingir majúscules; qualsevol altra cosa, defecte amb avís.
Al main, carrega una configuració amb valors vàlids, invàlids i absents —incloent-hi dies.prestec=15, tarifa.diaria=zero, multa.maxima=999, llindar.lleu absent i avisos.actius=SI— i demostra les quatre polítiques. Mostra també què passa quan falta un paràmetre obligatori, capturant-lo al main i mostrant un missatge digne.
Solucions
Solució 1
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.List;
/**
* Importa linies de cataleg tolerant errors linia a linia.
*
* Clau del disseny: el try va DINS del bucle, perque una linia
* defectuosa no avorti la importacio completa.
*/
public class ImportadorCataleg {
/** Resultat de la importacio (record: 04-07). */
public record InformeImportacio(int processades, int acceptades,
int rebutjades, List<String> motius) {
public String resum() {
StringBuilder sb = new StringBuilder();
sb.append("=== INFORME D'IMPORTACIO ===\n");
sb.append("Processades: ").append(processades).append('\n');
sb.append("Acceptades : ").append(acceptades).append('\n');
sb.append("Rebutjades : ").append(rebutjades).append('\n');
if (!motius.isEmpty()) {
sb.append("--- Motius de rebuig ---\n");
for (String m : motius) {
sb.append(" * ").append(m).append('\n');
}
}
return sb.toString();
}
}
private static final int ANY_MIN = 1450;
private static final int ANY_MAX = 2100;
private final List<String> acceptats = new ArrayList<>();
public InformeImportacio importar(String[] linies) {
List<String> motius = new ArrayList<>();
int processades = 0;
int acceptades = 0;
for (String linia : linies) {
processades++; // s'incrementa SEMPRE, falli o no
// Validacio previa: una linia buida no mereix ni intentar-se.
// Regla general: si es pot comprovar amb un if barat, no usis excepcio.
if (linia == null || linia.isBlank()) {
motius.add("Linia " + processades + ": buida");
continue;
}
try {
String[] camps = linia.split(";");
// Si falten camps, aquest acces llanca ArrayIndexOutOfBoundsException
String referencia = camps[0].trim();
String titol = camps[1].trim();
int any = Integer.parseInt(camps[2].trim());
int exemplars = Integer.parseInt(camps[3].trim());
// Validacions de negoci: NO llancen, es comproven amb if.
// Un valor fora de rang no es una fallada tecnica, es una dada incorrecta.
if (any < ANY_MIN || any > ANY_MAX) {
motius.add("Linia " + processades + " (" + referencia + "): any fora de rang ["
+ ANY_MIN + ".." + ANY_MAX + "]: " + any);
continue;
}
if (exemplars <= 0) {
motius.add("Linia " + processades + " (" + referencia
+ "): exemplars ha de ser positiu, era " + exemplars);
continue;
}
if (referencia.isEmpty() || titol.isEmpty()) {
motius.add("Linia " + processades + ": referencia o titol buits");
continue;
}
acceptats.add(referencia + " | " + titol + " | " + any + " | " + exemplars);
acceptades++;
} catch (ArrayIndexOutOfBoundsException e) {
// CATCH ESPECIFIC 1: falten camps. Missatge diferent de l'altre cas.
motius.add("Linia " + processades + ": falten camps (se n'esperaven 4). Contingut: '"
+ linia + "'");
} catch (NumberFormatException e) {
// CATCH ESPECIFIC 2: camps numerics mal escrits.
// e.getMessage() ja diu quin era el text ofensiu.
motius.add("Linia " + processades + ": camp numeric invalid -> " + e.getMessage());
}
}
return new InformeImportacio(processades, acceptades, processades - acceptades, motius);
}
public List<String> getAcceptats() {
return List.copyOf(acceptats); // copia immutable: encapsulament (03-07)
}
public static void main(String[] args) {
String[] linies = {
"LIB-0001;Java Eficac;2018;3", // correcta
"LIB-0002;Patrons de Disseny", // falten camps
"LIB-0003;Refactoritzacio;mil nou-cents;2", // any no numeric
"LIB-0004;Llibre Antic;1200;1", // any fora de rang
"LIB-0005;Sense Exemplars;2020;0", // exemplars invalids
"", // buida
"LIB-0006;Java Concurrent;2021;dos", // exemplars no numerics
"LIB-0007;Clean Code;2008;5" // correcta
};
ImportadorCataleg importador = new ImportadorCataleg();
InformeImportacio informe = importador.importar(linies);
System.out.println(informe.resum());
System.out.println("--- Acceptats ---");
importador.getAcceptats().forEach(a -> System.out.println(" " + a));
}
}Sortida:
=== INFORME D'IMPORTACIO === Processades: 8 Acceptades : 2 Rebutjades : 6 --- Motius de rebuig --- * Linia 2: falten camps (se n'esperaven 4). Contingut: 'LIB-0002;Patrons de Disseny' * Linia 3: camp numeric invalid -> For input string: "mil nou-cents" * Linia 4 (LIB-0004): any fora de rang [1450..2100]: 1200 * Linia 5 (LIB-0005): exemplars ha de ser positiu, era 0 * Linia 6: buida * Linia 7: camp numeric invalid -> For input string: "dos" --- Acceptats --- LIB-0001 | Java Eficac | 2018 | 3 LIB-0007 | Clean Code | 2008 | 5
Nota de disseny: fixa't que no s'ha fet servir multi-catch. En escriure els missatges es va veure que "falten camps" i "camp numèric invàlid" mereixen textos diferents, així que els catch separats són l'elecció correcta. El multi-catch hauria estat apropiat si el motiu fos simplement "linia mal formada" en tots dos casos. És la regla de l'apartat 5 en acció: multi-catch quan el tractament és idèntic, no quan els tipus són semblants.
Solució 2
Els sis defectes:
| # | Defecte | Mal |
|---|---|---|
| 1 | catch (Exception e) { } buit al bucle intern |
Cada referència absent al mapa es descarta en silenci. El total surt malament i ningú no se n'assabenta |
| 2 | catch (Exception e) massa ample |
Atrapa el NullPointerException de l'autounboxing, però també qualsevol bug de programació |
| 3 | diesRetard.get(ref) desempaquetat sense comprovar |
Si la clau no existeix, get retorna null, s'intenta intValue() i salta NullPointerException. És exactament la fallada que el catch buit amaga |
| 4 | catch (Throwable t) |
Atrapa OutOfMemoryError i StackOverflowError, dels quals no es pot recuperar |
| 5 | t.printStackTrace() com a gestió |
No és logging; en producció es perd |
| 6 | return 0 després de la fallada |
Retorna una quantitat de multa inventada. Un zero indistingible de "no hi ha multa". A més, referencies.size() pot ser 0 i llançar ArithmeticException, que aquell catch(Throwable) dissimula |
Hi ha un setè problema de fons, més greu que els sis: el mètode calcula una mitjana que no retorna ni usa, només la imprimeix. Aquesta barreja de càlcul i presentació és el que obliga a posar el println a dins i el que fa que la divisió per zero es coli en un mètode de càlcul.
Versió corregida:
package com.nexussoftware.bibliotech.servei;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class CalculadoraMultes {
private static final double TARIFA_DIARIA = 0.25;
private static final double MULTA_MAXIMA = 20.0;
/**
* Suma la multa de les referencies indicades.
*
* Politica d'errors:
* - Arguments nuls: fallada de programacio, es llanca (fail-fast, 06-03).
* - Referencia sense dada de retard: es considera 0 dies, comprovant-ho
* amb un if. No es una fallada: es un cas normal (encara no ha vencut).
* - No es captura RES aqui: aquest metode no sap que fer amb una fallada.
* Qui el cridi decidira.
*/
public double calcularMultaTotal(List<String> referencies, Map<String, Integer> diesRetard) {
Objects.requireNonNull(referencies, "La llista de referencies no pot ser nulla");
Objects.requireNonNull(diesRetard, "El mapa de dies de retard no pot ser nul");
double total = 0;
for (String ref : referencies) {
// getOrDefault evita el desempaquetatge de null: es un if disfressat,
// i es MILLOR que capturar el NullPointerException que provocaria get().
int dies = diesRetard.getOrDefault(ref, 0);
if (dies > 0) {
double multa = Math.min(dies * TARIFA_DIARIA, MULTA_MAXIMA);
total += multa;
}
}
return total;
}
/**
* Mitjana de multa per material. Separada del calcul total: cada metode, una cosa.
* La llista buida es comprova explicitament: es un cas previsible, no una fallada.
*/
public double calcularMultaMitjana(List<String> referencies, Map<String, Integer> diesRetard) {
Objects.requireNonNull(referencies, "La llista de referencies no pot ser nulla");
if (referencies.isEmpty()) {
return 0.0; // mitjana d'un conjunt buit: 0 es la resposta correcta
}
return calcularMultaTotal(referencies, diesRetard) / referencies.size();
}
public static void main(String[] args) {
CalculadoraMultes calc = new CalculadoraMultes();
List<String> refs = List.of("LIB-0001", "LIB-0002", "LIB-0003", "LIB-0004");
Map<String, Integer> retards = Map.of(
"LIB-0001", 4, // 1.00 EUR
"LIB-0002", 0, // sense retard
"LIB-0003", 120); // 30.00 -> limitat a 20.00
// LIB-0004 no apareix al mapa: getOrDefault retorna 0
System.out.printf("Total: %.2f EUR%n", calc.calcularMultaTotal(refs, retards));
System.out.printf("Mitjana: %.2f EUR%n", calc.calcularMultaMitjana(refs, retards));
System.out.printf("Mitjana de llista buida: %.2f EUR%n",
calc.calcularMultaMitjana(List.of(), retards));
}
}Sortida:
Canvis clau i la seva raó:
- Zero
try-catch. El mètode no sap què fer amb una fallada, així que no en captura cap. És la millor decisió possible en molts mètodes de servei, i per això elcatchque hi havia abans era un mer ritual. getOrDefaulten comptes de capturar elNullPointerException. Comprovar és més barat, més clar i més ràpid que provocar i atrapar (apartat 14).Objects.requireNonNullper als arguments: fallada ràpida amb missatge útil, en comptes d'unNullPointerExceptionopac tres línies més avall. Es desenvolupa a 06-03.- La mitjana se separa del total i la llista buida es comprova explícitament: la divisió per zero deixa de ser possible.
- No hi ha
printlna dins: el càlcul no imprimeix. Presentació i lògica, separades (03-08).
Solució 3
package com.nexussoftware.bibliotech.servei;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Lectura de configuracio amb politica d'error PER PARAMETRE.
*
* La llico de disseny: no existeix "la forma correcta" de gestionar un error.
* Depen de si el programa pot continuar sense aquesta dada.
*/
public class ConfiguracioBiblioTech {
private final Map<String, String> propietats;
private static final Set<String> CERTS = Set.of("true", "si", "cert", "1", "s", "yes");
private static final Set<String> FALSOS = Set.of("false", "no", "0", "n");
public ConfiguracioBiblioTech(Map<String, String> propietats) {
// Copia defensiva: ningu no modificara la configuracio per l'esquena (03-07)
this.propietats = new HashMap<>(propietats);
}
/**
* POLITICA A: parametre OBLIGATORI.
* Sense ell, el programa no pot funcionar correctament: es llanca.
* Es conserva la causa quan existeix, per no perdre el detall (06-03).
*/
public int llegirEnterObligatori(String clau) {
String valor = propietats.get(clau);
if (valor == null) {
throw new IllegalStateException(
"Falta el parametre obligatori '" + clau + "' a la configuracio");
}
try {
return Integer.parseInt(valor.trim());
} catch (NumberFormatException e) {
// TRADUCCIO amb causa: qui crida no ha de saber que a dins
// hi havia un parseInt, pero el stack trace conservara el detall.
throw new IllegalStateException(
"El parametre obligatori '" + clau + "' ha de ser un enter, i val '"
+ valor + "'", e);
}
}
/**
* POLITICA B: parametre OPCIONAL.
* Hi ha un valor per defecte raonable: es recupera i s'informa.
*/
public int llegirEnterOpcional(String clau, int perDefecte) {
String valor = propietats.get(clau);
if (valor == null) {
System.out.println(" [config] '" + clau + "' absent. S'usa " + perDefecte);
return perDefecte;
}
try {
return Integer.parseInt(valor.trim());
} catch (NumberFormatException e) {
System.out.println(" [config] '" + clau + "' = '" + valor
+ "' no es un enter. S'usa " + perDefecte);
return perDefecte;
}
}
/**
* POLITICA C: valor per defecte AMB validacio de rang.
* Distingeix dos motius de rebuig diferents, perque per diagnosticar
* no es el mateix "has escrit lletres" que "has escrit un numero absurd".
*/
public double llegirDecimalAmbRang(String clau, double min, double max, double perDefecte) {
String valor = propietats.get(clau);
if (valor == null) {
System.out.println(" [config] '" + clau + "' absent. S'usa " + perDefecte);
return perDefecte;
}
double convertit;
try {
convertit = Double.parseDouble(valor.trim().replace(',', '.'));
} catch (NumberFormatException e) {
System.out.println(" [config] '" + clau + "' = '" + valor
+ "' no es un decimal. S'usa " + perDefecte);
return perDefecte;
}
// La validacio de rang va FORA del try: no es una fallada de conversio,
// i posar-la a dins faria que una futura fallada de l'if es confongues.
if (convertit < min || convertit > max) {
System.out.printf(" [config] '%s' = %.2f fora de [%.2f..%.2f]. S'usa %.2f%n",
clau, convertit, min, max, perDefecte);
return perDefecte;
}
return convertit;
}
/**
* POLITICA D: boolea tolerant.
* No hi ha excepcio que capturar: no existeix cap Boolean.parseBoolean que falli
* (retorna false davant de qualsevol cosa, que es justament el problema).
* Es resol amb conjunts i validacio explicita.
*/
public boolean llegirBoolea(String clau, boolean perDefecte) {
String valor = propietats.get(clau);
if (valor == null) {
System.out.println(" [config] '" + clau + "' absent. S'usa " + perDefecte);
return perDefecte;
}
String normalitzat = valor.trim().toLowerCase();
if (CERTS.contains(normalitzat)) { return true; }
if (FALSOS.contains(normalitzat)) { return false; }
System.out.println(" [config] '" + clau + "' = '" + valor
+ "' no es un boolea reconeixible. S'usa " + perDefecte);
return perDefecte;
}
// ------------------------------------------------------------------
public static void main(String[] args) {
Map<String, String> props = new HashMap<>();
props.put("dies.prestec", "15");
props.put("tarifa.diaria", "zero"); // invalid
props.put("multa.maxima", "999"); // fora de rang
props.put("avisos.actius", "SI"); // valid, en majuscules
props.put("max.prestecs", "3");
// "llindar.lleu" deliberadament absent
ConfiguracioBiblioTech config = new ConfiguracioBiblioTech(props);
System.out.println("== Lectura de configuracio de BiblioTech ==");
int dies = config.llegirEnterObligatori("dies.prestec");
System.out.println("DIES_PRESTEC = " + dies);
double tarifa = config.llegirDecimalAmbRang("tarifa.diaria", 0.0, 5.0, 0.25);
System.out.println("TARIFA_DIARIA = " + tarifa);
double maxima = config.llegirDecimalAmbRang("multa.maxima", 0.0, 100.0, 20.0);
System.out.println("MULTA_MAXIMA = " + maxima);
int llindar = config.llegirEnterOpcional("llindar.lleu", 7);
System.out.println("LLINDAR_LLEU = " + llindar);
boolean avisos = config.llegirBoolea("avisos.actius", false);
System.out.println("AVISOS_ACTIUS = " + avisos);
// Demostracio de la politica A quan el parametre obligatori falta
System.out.println("\n== Parametre obligatori absent ==");
try {
config.llegirEnterObligatori("port.servidor");
} catch (IllegalStateException e) {
System.out.println("ERROR DE CONFIGURACIO: " + e.getMessage());
System.out.println("Causa subjacent: " + e.getCause());
}
// Demostracio de la politica A quan el valor es invalid: hi ha causa
System.out.println("\n== Parametre obligatori invalid ==");
try {
config.llegirEnterObligatori("tarifa.diaria");
} catch (IllegalStateException e) {
System.out.println("ERROR DE CONFIGURACIO: " + e.getMessage());
System.out.println("Causa subjacent: " + e.getCause());
}
}
}Sortida:
== Lectura de configuracio de BiblioTech == DIES_PRESTEC = 15 [config] 'tarifa.diaria' = 'zero' no es un decimal. S'usa 0.25 TARIFA_DIARIA = 0.25 [config] 'multa.maxima' = 999.00 fora de [0.00..100.00]. S'usa 20.00 MULTA_MAXIMA = 20.0 [config] 'llindar.lleu' absent. S'usa 7 LLINDAR_LLEU = 7 AVISOS_ACTIUS = true == Parametre obligatori absent == ERROR DE CONFIGURACIO: Falta el parametre obligatori 'port.servidor' a la configuracio Causa subjacent: null == Parametre obligatori invalid == ERROR DE CONFIGURACIO: El parametre obligatori 'tarifa.diaria' ha de ser un enter, i val 'zero' Causa subjacent: java.lang.NumberFormatException: For input string: "zero"
El que demostra l'exercici:
- La mateixa excepció, quatre polítiques diferents.
NumberFormatExceptiones converteix en unIllegalStateExceptionque avorta l'arrencada, o en un avís amb valor per defecte, segons com de crític sigui el paràmetre. La política la marca el context, no el tipus d'excepció. - La causa es conserva en traduir (l'últim bloc de sortida), i es perd completament quan no hi havia excepció original (el cas del paràmetre absent, amb
Causa subjacent: null). Aquesta diferència es veu al stack trace com la presència o absència deCaused by:. - No tot error necessita una excepció.
llegirBooleano captura res: fa servir conjunts i validació explícita, perquèBoolean.parseBooleanno llança —simplement retornafalsedavant de qualsevol brossa, que és una fallada silenciosa pitjor que una excepció.
Conclusió
Ja saps gestionar una excepció, no només llegir-la. Domines la sintaxi i la semàntica exactes de try-catch: què es protegeix —tot el bloc, inclosos els mètodes que es cridin des d'ell, per profunds que siguin—, què s'abandona quan salta l'excepció —tot el que quedi del try, bucles inclosos— i què passa si cap catch no és compatible: l'excepció continua pujant com si el try no existís. I saps que la posició del try respecte a un bucle és una decisió de disseny amb dues semàntiques diferents: fora per a "tot o res", dins per tolerar element a element.
Coneixes l'objecte capturat i els seus mètodes: getMessage —que pot ser null—, toString, getStackTrace per inspeccionar els marcs, getCause per baixar a la causa arrel i getSuppressed, que prendrà sentit a 06-06. I saps que printStackTrace() no és gestió d'errors, sinó una eina de depuració local que a 06-07 substituiràs per un Logger.
Tens la regla d'ordre dels múltiples catch —de la subclasse a la superclasse, amb l'error de compilació exception X has already been caught si la inverteixes— i saps que NumberFormatException descendeix d'IllegalArgumentException, un parentiu que trenca més d'un ordre aparentment correcte. Manegues el multi-catch amb | i les seves dues regles: els tipus no poden estar emparentats per herència, i la variable és implícitament final. Saps quan imbricar try —dos àmbits de recuperació diferents— i quan extreure un mètode al seu lloc. I coneixes l'abast de les variables del try i l'anàlisi d'assignació definitiva que obliga a assignar també al catch, o a sortir del mètode.
El més important: saps què es pot fer dins d'un catch. Recuperar-se amb un valor per defecte que sigui una resposta legítima i no una tapadora; reintentar amb límit, espera creixent i fallada definitiva en esgotar-los, i només quan la fallada depèn de l'entorn i no de les dades; traduir al vocabulari de la teva capa conservant la causa; i registrar i rellançar amb throw e; quan la decisió correspon a algú de dalt, sense caure en el log duplicat. Un catch que no fa cap de les quatre és un catch mal posat.
I tens la demostració dels antipatrons: el catch buit, que fa que un importador processi 3 exemplars en comptes de 6 sense deixar el més mínim rastre i que és literalment pitjor que no capturar res; el catch (Exception e) massa ample, que converteix els teus propis bugs en missatges tranquil·litzadors per a l'usuari; i les excepcions com a flux de control, amb els números que les condemnen: unes 70 vegades més lent que un if, amb fillInStackTrace com a responsable —i amb el contrast revelador que una excepció sense stack trace amb prou feines costa un 50% més que l'if, cosa que prova que el mecanisme de throw/catch en si és barat i que les excepcions per al que és excepcional no tenen cost apreciable.
BiblioTech ha guanyat la seva primera defensa real: MenuBiblioTech ja no mor quan Marta Ruiz escriu "dotze". L'opció del menú es reintenta indefinidament perquè hi ha una persona davant; les dades numèriques es reintenten amb límit i cancel·len l'operació sense tocar l'estat; la tarifa accepta el valor per defecte només prement Enter. I tot això capturant exclusivament NumberFormatException, de manera que un bug real continuaria sortint a la llum en comptes de disfressar-se d'"entrada invàlida".
Però la capa de servei continua mentint. Cataleg.registrar continua retornant el seu false mut, cercarPerReferencia continua retornant null, i els constructors del mòdul 3 continuen corregint les dades invàlides amb avisos per consola en comptes de rebutjar-les. Capturar no n'hi ha prou: cal assenyalar la fallada en el moment en què es detecta.
Això és la lliçó següent, Throw i throws: com llançar una excepció explícitament amb throw i per què el codi posterior és inabastable; com declarar-la amb throws i què obliga això a qui crida; la validació d'arguments amb IllegalArgumentException, IllegalStateException i Objects.requireNonNull, que per fi jubilarà els avisos per consola dels constructors del mòdul 3; el principi de fallar ràpid; l'encadenament d'excepcions amb la causa —i la demostració exacta de la informació que es perd si no la conserves—; la traducció entre capes perquè la capa de servei no filtri les excepcions de la capa de dades; i les regles de throws en la sobreescriptura de mètodes. En acabar-la, Cataleg.cercarPerReferencia deixarà de retornar null per sempre.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
