La lliçó anterior va acabar amb un codi que funcionava però que deixava a la vista el seu propi límit: un catch amb quatre tipus estàndard units per |, un IllegalArgumentException fet servir per a tres coses diferents i un catch (IllegalStateException e) incapaç de distingir "el material no està disponible" de "l'empleat ha assolit el seu límit" sense llegir un missatge de text lliure.
Aquest és el sostre de les excepcions estàndard. IllegalArgumentException descriu una categoria tècnica: "l'argument no val". No diu res del negoci. I l'únic lloc on viu la informació concreta —quina referència estava duplicada, quin era el límit, quants dies feia que estava prestat— és una cadena de text que ningú no hauria de parsejar.
En aquesta lliçó construeixes el vocabulari propi de BiblioTech. En acabar, un catch (LimitPrestecsExceditException e) no només capturarà exactament aquella fallada i cap altra, sinó que podrà preguntar e.getLimit() i e.getIdEmpleat() per decidir si ofereix una reserva, si avisa el responsable o si suggereix retornar alguna cosa primer. La diferència entre llegir un missatge i consultar una dada és la diferència entre un log i un programa que reacciona.
Contingut
- Per què crear excepcions pròpies
- Com es crea una excepció
- Comprovada o no comprovada: el criteri d'elecció
- Els quatre constructors canònics
- Afegir camps propis i accessors
- Dissenyar una jerarquia de domini
- L'arrel comuna i el que permet
- Convencions de nomenclatura
- Convencions de missatge
- Serialització i
serialVersionUID - Quan NO crear una excepció pròpia
- Nota avançada: excepcions sense stack trace
- BiblioTech: la refactorització completa
- Errors Comuns i Consells
- Exercicis
- Per què crear excepcions pròpies
Dues raons, i la segona és la que de debò canvia el codi.
Raó 1: anomenar la fallada en el llenguatge del domini
Compara aquestes dues captures:
// Amb excepcions estandard
try {
gestor.prestar("LIB-0001", "EMP-001", 12);
} catch (IllegalStateException e) {
// Que ha passat? El material esta prestat? L'empleat ha arribat al limit?
// El prestec ja estava retornat? Cal llegir el MISSATGE per saber-ho.
if (e.getMessage().contains("limit")) { // fragil, horrible, i es trenca en traduir
// ...
}
}
// Amb excepcions del domini
try {
gestor.prestar("LIB-0001", "EMP-001", 12);
} catch (MaterialNoDisponibleException e) {
oferirReserva(e.getReferencia(), e.getDiaPrevistDevolucio());
} catch (LimitPrestecsExceditException e) {
mostrarPrestecsActius(e.getIdEmpleat());
}A la segona versió, el tipus de l'excepció és la decisió. No cal interpretar res: el compilador encamina cada fallada al seu gestor. I el codi es llegeix com el negoci: "si el material no està disponible, ofereix una reserva".
Aquell e.getMessage().contains("limit") de la primera versió no és un exemple exagerat. Apareix en codi real, i és una bomba: el dia que algú millori el missatge o el tradueixi a l'anglès, la lògica deixa de funcionar sense cap error de compilació i sense cap test que falli, si els tests no cobrien aquell camí.
Raó 2: transportar dades estructurades
Aquesta és la raó decisiva. Una excepció estàndard només duu un String:
Perquè qui captura pugui fer alguna cosa amb aquestes dades, hauria d'extreure-les del text. Amb una excepció pròpia, les dades van en camps:
throw new LimitPrestecsExceditException(empleat, Empleat.MAX_PRESTECS_SIMULTANIS);
// I qui captura:
} catch (LimitPrestecsExceditException e) {
String emp = e.getIdEmpleat(); // l'identificador, no el nom en una cadena
int limit = e.getLimit(); // el numero, no un text
int actuals = e.getPrestecsActuals();
System.out.printf("%s te %d de %d prestecs.%n",
emp, actuals, limit);
oferirDevolucioAnticipada(emp); // es pot USAR la dada
}La regla que resumeix totes dues raons:
El missatge és per a les persones. Els camps són per al programa. Una excepció ben dissenyada ofereix els dos.
- Com es crea una excepció
Una excepció és una classe normal que estén Exception o RuntimeException. En la seva forma mínima:
package com.nexussoftware.bibliotech.domini;
/** Versio minima: nomes el nom i el missatge. */
public class MaterialNoTrobatException extends RuntimeException {
public MaterialNoTrobatException(String missatge) {
super(missatge);
}
}I ja està. Amb aquestes cinc línies pots escriure:
...i capturar-la de manera específica. Però aquesta versió mínima desaprofita el que fa útils les excepcions pròpies: no transporta dades i no ofereix el constructor amb causa. Als apartats següents es completa.
El que no s'ha de fer:
// MALAMENT: estendre Throwable directament
public class LaMevaExcepcio extends Throwable { } // legal, pero mai correcte
// MALAMENT: estendre Error
public class LaMevaExcepcio extends Error { } // Error es per a fallades de la JVM
// MALAMENT: estendre una excepcio estandard molt especifica sense motiu
public class LaMevaExcepcio extends NumberFormatException { } // hereta una semantica alienaEstendre directament de Throwable produeix una excepció que no és ni Error ni Exception, així que s'escapa de tots els catch (Exception e) habituals sense ser un error de la JVM. No té cap utilitat pràctica.
- Comprovada o no comprovada: el criteri d'elecció
La decisió més important en crear una excepció, i la que més s'equivoca.
Estendre Exception (comprovada) |
Estendre RuntimeException (no comprovada) |
|
|---|---|---|
| El compilador obliga a | Capturar o declarar a cada crida | Res |
| Efecte a les signatures | Les contamina en tota la cadena | Cap |
| Efecte a les lambdes | No es pot llançar des de Function, Consumer, etc. |
Sense problema |
| Risc | Que la gent la silenciï amb catch buits |
Que ningú no la capturi i arribi a l'usuari |
| Quan triar-la | Qui crida gairebé sempre pot i ha de fer alguna cosa diferent de propagar | És una regla de negoci o un error de programació |
La pregunta que decideix:
Farà qui crida alguna cosa diferent de "propagar" en la majoria dels casos?
- Sí, gairebé sempre → comprovada. Exemple real: "el fitxer de catàleg no existeix", on a dalt es decideix arrencar amb catàleg buit.
- No, gairebé mai → no comprovada. Exemple: "aquesta referència no existeix al catàleg", on el normal és deixar que pugi fins a la capa de presentació.
Aplicat a BiblioTech, amb el raonament explícit de cada decisió:
| Excepció | Tipus | Raó |
|---|---|---|
MaterialNoTrobatException |
No comprovada | Obligar a un try a cada cerca seria insuportable, i el normal és que pugi a presentació |
ReferenciaDuplicadaException |
No comprovada | És prevenible: existeix cataleg.existeix(ref) per comprovar-ho abans |
LimitPrestecsExceditException |
No comprovada | Regla de negoci consultable amb potPrendrePrestat() |
MaterialNoDisponibleException |
No comprovada | Ídem, consultable amb estaDisponible() |
PrestecJaRetornatException |
No comprovada | Error de seqüència: no hauria de passar amb el codi ben escrit |
CatalegNoAccessibleException |
Comprovada | Fallada externa (disc, permisos) i l'aplicació sí que té una alternativa: arrencar buida |
Observa el patró: totes les regles de negoci són no comprovades; l'única comprovada és la que representa una fallada de l'entorn. És la postura pragmàtica que vas anunciar a 06-01, i coincideix amb el que fan Spring, Hibernate i pràcticament tot l'ecosistema modern.
Un matís important que reforça la decisió: una excepció comprovada en un mètode d'una interfície obliga totes les seves implementacions i tots els seus usuaris. Si Prestable.prestar() declarés throws MaterialNoDisponibleException, cada lambda, cada forEach i cada implementació futura ho carregaria. La regla de sobreescriptura de 06-03 ho fa irreversible: un cop publicada, no la pots treure sense trencar ningú... però tampoc no la pots afegir després sense trencar-ho tot.
- Els quatre constructors canònics
Throwable defineix quatre constructors públics, i una excepció ben dissenyada els ofereix tots —o almenys els tres primers:
package com.nexussoftware.bibliotech.domini;
/** Excepcio amb els quatre constructors canonics. */
public class BiblioTechException extends RuntimeException {
/** 1. Sense missatge ni causa. Poc util, pero completa el contracte. */
public BiblioTechException() {
super();
}
/** 2. Amb missatge. El mes usat en ORIGINAR una fallada. */
public BiblioTechException(String missatge) {
super(missatge);
}
/** 3. Amb missatge i causa. El que has d'usar en EMBOLCALLAR (06-03). */
public BiblioTechException(String missatge, Throwable causa) {
super(missatge, causa);
}
/** 4. Nomes causa: el missatge es deriva de causa.toString(). */
public BiblioTechException(Throwable causa) {
super(causa);
}
}Per què convé oferir-los tots:
| Constructor | Es necessita quan |
|---|---|
() |
Poques vegades. El demana algun framework per reflexió (10-03) |
(String) |
Origines la fallada tu, amb un missatge explicatiu |
(String, Throwable) |
Embolcalles una altra excepció afegint context. Imprescindible |
(Throwable) |
Embolcalles sense res a afegir. Poc recomanable: gairebé sempre tens alguna cosa millor a dir que causa.toString() |
La conseqüència pràctica d'ometre el tercer és que algú acabarà perdent una causa. Si la teva MaterialNoTrobatException només accepta un String, qui necessiti embolcallar una fallada de baix nivell haurà de triar entre fer servir un altre tipus d'excepció o descartar la causa. I ja saps com acaba això.
Throwable té a més un cinquè constructor protected, que veuràs a l'apartat 12:
protected Throwable(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace)
- Afegir camps propis i accessors
Aquí hi ha la potència real. Una excepció és un objecte: pot tenir l'estat que vulguis.
package com.nexussoftware.bibliotech.domini;
/**
* El material sollicitat no existeix al cataleg.
*
* Transporta la referencia cercada perque qui la capturi pugui
* oferir-la en un missatge, buscar alternatives o registrar-la, sense
* haver de parsejar el missatge de text.
*/
public class MaterialNoTrobatException extends BiblioTechException {
private static final long serialVersionUID = 1L;
/** Els camps d'una excepcio han de ser final: una excepcio es immutable. */
private final String referenciaCercada;
private final int midaCataleg;
public MaterialNoTrobatException(String referenciaCercada, int midaCataleg) {
// El missatge es construeix a partir de les dades: una sola font de veritat
super("No existeix cap material amb la referencia '" + referenciaCercada
+ "' (el cataleg te " + midaCataleg + " materials)");
this.referenciaCercada = referenciaCercada;
this.midaCataleg = midaCataleg;
}
/** Variant per a quan s'embolcalla una altra fallada. */
public MaterialNoTrobatException(String referenciaCercada, int midaCataleg,
Throwable causa) {
super("No existeix cap material amb la referencia '" + referenciaCercada
+ "' (el cataleg te " + midaCataleg + " materials)", causa);
this.referenciaCercada = referenciaCercada;
this.midaCataleg = midaCataleg;
}
public String getReferenciaCercada() { return referenciaCercada; }
public int getMidaCataleg() { return midaCataleg; }
/** Metode de conveniencia: l'excepcio pot aportar logica util. */
public boolean catalegBuit() {
return midaCataleg == 0;
}
}I el que això permet al punt de captura:
try {
Material m = cataleg.obtenirPerReferencia(entradaUsuari);
gestor.prestar(m.getReferencia(), "EMP-001", dia);
} catch (MaterialNoTrobatException e) {
if (e.catalegBuit()) {
System.out.println("El cataleg esta buit. Heu carregat el fitxer de materials?");
} else {
System.out.println("No s'ha trobat '" + e.getReferenciaCercada() + "'.");
// Es poden usar les DADES per ajudar: suggeriments per prefix
List<Material> semblants = cataleg.cercarPerPrefix(
e.getReferenciaCercada().substring(0, 3));
if (!semblants.isEmpty()) {
System.out.println("Potser voliem dir algun d'aquests?");
semblants.forEach(m -> System.out.println(" " + m.getReferencia()));
}
}
}Tres regles de disseny per als camps d'una excepció:
finalsempre. Una excepció és un objecte immutable que descriu un fet ocorregut. Ningú no l'hauria de poder modificar després.- Construeix el missatge a partir dels camps, al
super(...). Així el text i les dades no es contradiuen mai. - Compte amb guardar objectes grans. Si guardes el
Materialcomplet en comptes de la seva referència, l'excepció manté viva aquella referència mentre existeixi, i impedeix que el recol·lector s'endugui l'objecte. Amb unMaterialés irrellevant; amb una col·lecció sencera o un flux obert, és una fuita de memòria. Guarda identificadors, no grafs d'objectes.
- Dissenyar una jerarquia de domini
Una excepció solta ja aporta. Una jerarquia aporta molt més, perquè permet capturar a diferents nivells de granularitat segons convingui a cada capa.
Aquesta és la de BiblioTech:
flowchart TB
RE["RuntimeException<br/>(del JDK)"]
RE --> BT["BiblioTechException<br/>ARREL del domini (abstracta)"]
BT --> CAT["CatalegException<br/>fallades del cataleg"]
BT --> PRE["PrestecException<br/>fallades de prestec"]
CAT --> MNE["MaterialNoTrobatException<br/>+ getReferenciaCercada()"]
CAT --> RDE["ReferenciaDuplicadaException<br/>+ getValorDuplicat()<br/>+ getTitolExistent()"]
PRE --> MND["MaterialNoDisponibleException<br/>+ getReferencia()<br/>+ getDiaPrevistDevolucio()"]
PRE --> LPE["LimitPrestecsExceditException<br/>+ getIdEmpleat()<br/>+ getLimit()"]
PRE --> PYD["PrestecJaRetornatException<br/>+ getReferenciaPrestec()<br/>+ getDiaDevolucioOriginal()"]
Les tres decisions d'aquest disseny:
Una arrel abstracta comuna, BiblioTechException. Permet catch (BiblioTechException e) per atrapar qualsevol fallada del domini d'una sola vegada —el que farà la frontera d'errors a 06-07— sense arrossegar els NullPointerException que sí que són bugs. És abstracta perquè ningú no l'hauria de llançar directament: seria tan poc informativa com el false mut que estem eliminant.
Dos nivells intermedis, CatalegException i PrestecException, per a les captures de granularitat mitjana: "qualsevol fallada del catàleg" sense haver d'enumerar les seves tres filles.
Fulles concretes amb dades, que són les que es llancen i les que porten els camps útils.
La granularitat de captura que això permet, de més ampla a més fina:
// Nivell 1: qualsevol fallada del domini BiblioTech (frontera d'errors)
catch (BiblioTechException e) { ... }
// Nivell 2: qualsevol fallada relacionada amb prestecs
catch (PrestecException e) { ... }
// Nivell 3: aquesta fallada concreta, amb les seves dades
catch (LimitPrestecsExceditException e) { usar(e.getLimit()); }I recorda la regla d'ordre de 06-02: si combines diversos nivells al mateix try, del més específic al més general, o no compila.
try {
gestor.prestar(referencia, idEmpleat, dia);
} catch (LimitPrestecsExceditException e) { // fulla
oferirDevolucioAnticipada(e.getIdEmpleat());
} catch (PrestecException e) { // branca
System.out.println("No s'ha pogut completar el prestec: " + e.getMessage());
} catch (BiblioTechException e) { // arrel
System.out.println("Fallada de BiblioTech: " + e.getMessage());
}Un avís sobre la mida de la jerarquia: no la facis més profunda del que faràs servir. Tres nivells és un màxim raonable per a una aplicació d'aquesta mida. Una jerarquia de sis nivells amb quaranta classes, la meitat de les quals ningú no captura mai per separat, és pur cerimonial. La prova pràctica: si mai no escriuràs un catch d'un nivell intermedi, aquell nivell sobra.
- L'arrel comuna i el que permet
Val la pena aturar-se a veure per què l'arrel comuna és la peça més valuosa de la jerarquia. Aquests són els tres usos que la justifiquen:
1. La frontera d'errors del main (que muntaràs a 06-07):
try {
aplicacio.arrencar();
} catch (BiblioTechException e) {
// Fallada ESPERADA del domini: missatge net per a l'usuari
System.out.println("Operacio no completada: " + e.getMessage());
} catch (RuntimeException e) {
// Fallada INESPERADA: es un bug. Missatge generic + registre complet
System.out.println("Error intern. Consulteu el registre (incidencia " + id + ").");
logger.log(Level.SEVERE, "Fallada no controlada", e);
}Sense l'arrel comuna no podries distingir "l'usuari ha demanat una cosa impossible" de "tenim un bug". Amb ella, el primer cas produeix un missatge amable i el segon una incidència registrada.
2. Comportament compartit. L'arrel pot aportar mètodes a tota la família:
public abstract class BiblioTechException extends RuntimeException {
/** Codi estable per al log i per a la documentacio de suport. */
public String getCodi() {
return getClass().getSimpleName().replace("Exception", "").toUpperCase();
}
/**
* Es una fallada que l'usuari pot corregir per si mateix?
* Les subclasses ho redefineixen quan no sigui aixi.
*/
public boolean esCorregiblePerUsuari() {
return true;
}
/** Missatge apte per mostrar en pantalla, sense detalls interns. */
public String getMissatgePerUsuari() {
return getMessage();
}
}3. Un punt únic d'evolució. Si demà vols que totes les excepcions de BiblioTech duguin un identificador d'operació per correlacionar logs, l'afegeixes a l'arrel i l'hereten les set.
- Convencions de nomenclatura
Quatre regles que segueix tot l'ecosistema Java:
| Regla | Bé | Malament |
|---|---|---|
Sufix Exception |
MaterialNoTrobatException |
MaterialNoTrobat, ErrorMaterial |
| Descriu el problema, no la solució | LimitPrestecsExceditException |
HaDeRetornarAlgunaCosaException |
| Específica, no genèrica | ReferenciaDuplicadaException |
DadesInvalidesException |
| Sense el prefix del projecte a les fulles | MaterialNoTrobatException |
BiblioTechMaterialNoTrobatException |
Sobre l'última: el prefix està bé a l'arrel (BiblioTechException) perquè allà identifica la família. Repetir-lo a cada fulla és soroll, ja que el paquet compleix aquesta funció.
I una nota sobre Error com a sufix: no el facis servir. En Java, Error significa "fallada irrecuperable de la JVM". Anomenar MaterialError una cosa que estén RuntimeException és enganyós per a qualsevol que conegui la jerarquia.
- Convencions de missatge
Un bon missatge d'excepció respon tres preguntes:
- Què ha passat, en llenguatge del domini.
- Amb quina dada concreta.
- Què s'esperava, quan no sigui obvi.
| Missatge | Valoració |
|---|---|
"Error" |
Inútil |
"Referencia invalida" |
Falta la dada |
"Referencia invalida: LIB-99" |
Falta el que s'esperava |
"La referencia ha de tenir el format LIB-NNNN, i era: 'LIB-99'" |
Complet |
Aplicat a la jerarquia:
// Que + dada + esperat
"L'empleat EMP-001 (Marta Ruiz) te 3 prestecs actius i el limit es 3"
// Que + dada + context util per actuar
"El material LIB-0001 ('Java Eficac') esta prestat a EMP-002 des del dia 12"
// Que + les dues dades que colliseixen
"Ja existeix un material amb la referencia LIB-0001: 'Java Eficac'"Tres coses que no han d'anar en un missatge d'excepció:
- Dades sensibles: contrasenyes, tokens, números de targeta, dades personals que no siguin estrictament necessàries. El missatge acabarà en un log, i possiblement en un correu automàtic o a la pantalla d'algú. A 06-07 hi ha una advertència formal sobre això.
- La solució suggerida, quan depèn del context.
"El material no esta disponible. Retorneu-lo primer."pressuposa que qui llegeix és qui el té prestat, i no té per què ser-ho. El suggeriment és cosa de la capa de presentació, que sí que coneix l'usuari. - Puntuació final ni salts de línia. El missatge es concatena en logs i en stack traces; un salt de línia trenca el format d'una línia per esdeveniment del qual depenen els sistemes d'anàlisi.
- Serialització i
serialVersionUID
serialVersionUIDThrowable implementa java.io.Serializable, cosa que significa que totes les excepcions són serialitzables: es poden convertir en bytes i enviar-se per xarxa o guardar-se en disc. És el que permet que una excepció llançada en un servidor arribi a un client remot.
D'aquí en surten dues conseqüències pràctiques.
1. L'avís del compilador sobre serialVersionUID. Si compiles amb -Xlint:serial, veuràs:
warning: [serial] serializable class MaterialNoTrobatException has no definition of serialVersionUID
És una advertència menor però es resol amb una línia:
public class MaterialNoTrobatException extends BiblioTechException {
private static final long serialVersionUID = 1L;
// ...
}Aquell número identifica la versió de la classe a efectes de serialització. Si no el declares, Java el calcula automàticament a partir de l'estructura de la classe, i canvia cada vegada que modifiques un camp o un mètode, cosa que trenca la compatibilitat amb bytes serialitzats anteriorment. Declarant-lo explícitament controles tu quan canvia. La mecànica completa de la serialització és matèria de 07-05.
2. Els camps propis han de ser serialitzables. Si la teva excepció guarda un objecte d'una classe que no implementa Serializable, la serialització fallarà amb NotSerializableException:
// PROBLEMATIC si l'excepcio viatja per xarxa i Empleat no es Serializable
private final Empleat empleat;
// SEGUR: String i int sempre son serialitzables
private final String idEmpleat;
private final int limit;És un argument més a favor de guardar identificadors en comptes d'objectes complets, a més del de la memòria que ja has vist. En una aplicació de consola com BiblioTech això no arriba a manifestar-se, però és el tipus de decisió que evita un problema el dia que l'aplicació creixi.
- Quan NO crear una excepció pròpia
Crear excepcions té cost: més classes, més coses a documentar, més coses a aprendre. La regla:
Si ja existeix una excepció estàndard la semàntica de la qual és exactament la teva i no necessites transportar dades, fes-la servir.
Les estàndard que cobreixen la majoria de casos:
| Excepció estàndard | Semàntica | Exemple a BiblioTech |
|---|---|---|
IllegalArgumentException |
Argument invàlid, sense significat de negoci | dia < 1; format de referència incorrecte |
IllegalStateException |
L'objecte no està en condicions, sense significat de negoci | Iterador ja tancat |
NullPointerException |
Argument obligatori nul | Objects.requireNonNull en qualsevol constructor |
UnsupportedOperationException |
Operació no suportada per aquesta implementació | add sobre un catàleg de només lectura |
NoSuchElementException |
No hi ha més elements, o no s'ha trobat | Iteració sobre la cua de reserves buida |
IndexOutOfBoundsException |
Índex fora de rang | Posició invàlida en un llistat |
La prova de decisió, en tres preguntes:
- La fallada té un nom en el llenguatge del negoci? Si Marta Ruiz diria "aquell llibre ja està prestat", mereix una excepció pròpia. Si diria "el programa s'ha equivocat", no.
- Necessita transportar dades estructurades? Si qui captura voldrà l'objecte o el número, pròpia. Si només mostrarà el missatge, estàndard.
- Algú la capturarà de manera específica? Si a tot el sistema sempre acabarà en un
catchgenèric, la pròpia no aporta res.
Aplicat a BiblioTech, aquesta és la barreja resultant, que és el normal en un projecte sa:
// ESTANDARD: validacio tecnica sense significat de negoci
Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
if (dia < 1) {
throw new IllegalArgumentException("El dia ha de ser 1 o posterior, i era: " + dia);
}
if (!referencia.matches("LIB-\\d{4}")) {
throw new IllegalArgumentException("Format de referencia invalid: " + referencia);
}
// PROPIA: regla de negoci amb nom i amb dades
if (!material.estaDisponible()) {
throw new MaterialNoDisponibleException(referencia, material.getTitol(),
material.getDiaPrevistDevolucio());
}Un error de principiant freqüent a l'altre extrem: crear NomNulException, AnyInvalidException, TitolBuitException... una excepció per camp. Això és substituir un vocabulari estàndard i conegut per un de propi que ningú no coneix, sense guanyar res. Les validacions tècniques d'arguments fan servir excepcions estàndard; les regles de negoci fan servir les teves.
- Nota avançada: excepcions sense stack trace
A 06-02 vas mesurar que el car d'una excepció no és llançar-la ni capturar-la, sinó construir-la, perquè el constructor de Throwable crida fillInStackTrace() per capturar la pila completa.
Per al 99,9% dels casos això és irrellevant: si alguna cosa falla un cop cada mil operacions, el cost no es nota. Però existeix un cas legítim en què sí que importa: una excepció usada com a senyal de control molt freqüent en un bucle calent, on el stack trace no es llegeix mai.
Throwable ofereix un constructor protected amb dos paràmetres extra:
protected Throwable(String message, Throwable cause,
boolean enableSuppression, // admet excepcions suprimides? (06-06)
boolean writableStackTrace) // captura la pila?Amb writableStackTrace = false, no es captura la pila i l'excepció es construeix gairebé de franc:
package com.nexussoftware.bibliotech.servei;
/**
* Excepcio de senyal, sense stack trace.
*
* ADVERTENCIA: usar NOMES quan es compleixin les tres condicions:
* 1. Es llanca en un bucle molt calent (milers de vegades per segon).
* 2. Es captura immediatament al mateix metode o molt a prop.
* 3. El stack trace no aporta RES, perque el punt de llancament es unic.
*
* Si no es compleixen les tres, aquesta optimitzacio nomes aconsegueix que una
* fallada real sigui impossible de diagnosticar. Es la definicio d'optimitzacio prematura.
*/
public class CercaInterrompudaException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final int posicio;
public CercaInterrompudaException(int posicio) {
// missatge, causa, supressio, stackTrace
super("Cerca interrompuda a la posicio " + posicio, null, false, false);
this.posicio = posicio;
}
public int getPosicio() { return posicio; }
}I una variant encara més extrema, l'excepció preconstruïda com a constant:
/** Una unica instancia reutilitzada: zero assignacions de memoria. */
private static final CercaInterrompudaException SENYAL =
new CercaInterrompudaException(-1);
// ...
throw SENYAL; // no construeix resEls números de 06-02 avalen la tècnica: una excepció sense stack trace costava 1,5 vegades un if, enfront de les 70 vegades d'una de completa.
I ara l'advertència, que és més important que la tècnica:
Això és una optimització d'últim recurs. Una excepció sense stack trace que arribi a producció per un camí imprevist és impossible de diagnosticar: veuràs la classe i el missatge, i ni una sola línia d'on ha passat. Si a més la reutilitzes com a constant, el missatge serà sempre el mateix encara que el context sigui diferent.
El mateix JDK fa servir aquesta tècnica en llocs molt concrets i controlats, i les llibreries de programació reactiva l'empren per a senyals internes. En codi d'aplicació com BiblioTech, no la necessites. És aquí perquè la reconeguis quan la vegis i perquè entenguis d'on surt el cost d'una excepció.
- BiblioTech: la refactorització completa
La jerarquia sencera, llesta per usar. Primer l'arrel:
package com.nexussoftware.bibliotech.domini;
/**
* Arrel de totes les excepcions del domini de BiblioTech.
*
* Es abstracta perque ningu no l'ha de llancar directament: seria tan poc
* informativa com el 'false' mut que aquest modul esta eliminant.
*
* Es NO COMPROVADA (esten RuntimeException) perque representa regles de
* negoci que qui crida normalment no pot resoldre al lloc, i perque
* obligar a declarar-les contaminaria tota l'aplicacio, incloses les lambdes.
*/
public abstract class BiblioTechException extends RuntimeException {
private static final long serialVersionUID = 1L;
protected BiblioTechException(String missatge) {
super(missatge);
}
protected BiblioTechException(String missatge, Throwable causa) {
super(missatge, causa);
}
/**
* Codi estable de la fallada, derivat del nom de la classe.
* Serveix per al log i per a la documentacio de suport:
* MATERIALNOTROBAT, LIMITPRESTECSEXCEDIT, etc.
*/
public String getCodi() {
return getClass().getSimpleName()
.replace("Exception", "")
.toUpperCase();
}
/**
* Pot l'usuari corregir aixo per si mateix?
* Per defecte si; les subclasses que representin fallades del sistema ho redefineixen.
*/
public boolean esCorregiblePerUsuari() {
return true;
}
}Les dues branques intermèdies:
package com.nexussoftware.bibliotech.domini;
/** Fallades relacionades amb el cataleg de materials. */
public abstract class CatalegException extends BiblioTechException {
private static final long serialVersionUID = 1L;
protected CatalegException(String missatge) { super(missatge); }
protected CatalegException(String missatge, Throwable causa) { super(missatge, causa); }
}package com.nexussoftware.bibliotech.domini;
/** Fallades relacionades amb prestecs i devolucions. */
public abstract class PrestecException extends BiblioTechException {
private static final long serialVersionUID = 1L;
protected PrestecException(String missatge) { super(missatge); }
protected PrestecException(String missatge, Throwable causa) { super(missatge, causa); }
}I les cinc fulles concretes, cadascuna amb les seves dades:
package com.nexussoftware.bibliotech.domini;
/** No existeix cap material amb la referencia cercada. */
public class MaterialNoTrobatException extends CatalegException {
private static final long serialVersionUID = 1L;
private final String referenciaCercada;
private final int midaCataleg;
public MaterialNoTrobatException(String referenciaCercada, int midaCataleg) {
super("No existeix cap material amb la referencia '" + referenciaCercada
+ "' (el cataleg te " + midaCataleg + " materials)");
this.referenciaCercada = referenciaCercada;
this.midaCataleg = midaCataleg;
}
public String getReferenciaCercada() { return referenciaCercada; }
public int getMidaCataleg() { return midaCataleg; }
public boolean catalegBuit() { return midaCataleg == 0; }
}package com.nexussoftware.bibliotech.domini;
/** S'intenta registrar un material amb una referencia o un ISBN que ja existeixen. */
public class ReferenciaDuplicadaException extends CatalegException {
private static final long serialVersionUID = 1L;
/** Tipus d'identificador que colliseix. Un enum (04-07) evita cadenes magiques. */
public enum Tipus { REFERENCIA, ISBN }
private final Tipus tipus;
private final String valorDuplicat;
private final String titolExistent;
public ReferenciaDuplicadaException(Tipus tipus, String valorDuplicat, String titolExistent) {
super("Ja existeix un material amb " + (tipus == Tipus.ISBN ? "l'ISBN " : "la referencia ")
+ valorDuplicat + ": '" + titolExistent + "'");
this.tipus = tipus;
this.valorDuplicat = valorDuplicat;
this.titolExistent = titolExistent;
}
public Tipus getTipus() { return tipus; }
public String getValorDuplicat() { return valorDuplicat; }
public String getTitolExistent() { return titolExistent; }
}package com.nexussoftware.bibliotech.domini;
/** El material existeix pero esta prestat a un altre empleat. */
public class MaterialNoDisponibleException extends PrestecException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final String titol;
private final String idTitularActual;
private final int diaPrevistDevolucio;
public MaterialNoDisponibleException(String referencia, String titol,
String idTitularActual, int diaPrevistDevolucio) {
super("El material " + referencia + " ('" + titol + "') esta prestat a "
+ idTitularActual + " i s'espera la seva devolucio el dia " + diaPrevistDevolucio);
this.referencia = referencia;
this.titol = titol;
this.idTitularActual = idTitularActual;
this.diaPrevistDevolucio = diaPrevistDevolucio;
}
public String getReferencia() { return referencia; }
public String getTitol() { return titol; }
public String getIdTitularActual() { return idTitularActual; }
public int getDiaPrevistDevolucio() { return diaPrevistDevolucio; }
/** Dies que falten perque el material torni a estar lliure. */
public int diesEspera(int diaActual) {
return Math.max(0, diaPrevistDevolucio - diaActual);
}
}package com.nexussoftware.bibliotech.domini;
/** L'empleat ha assolit el maxim de prestecs simultanis. */
public class LimitPrestecsExceditException extends PrestecException {
private static final long serialVersionUID = 1L;
private final String idEmpleat;
private final String nomEmpleat;
private final int prestecsActuals;
private final int limit;
public LimitPrestecsExceditException(String idEmpleat, String nomEmpleat,
int prestecsActuals, int limit) {
super("L'empleat " + idEmpleat + " (" + nomEmpleat + ") te "
+ prestecsActuals + " prestecs actius i el limit es " + limit);
this.idEmpleat = idEmpleat;
this.nomEmpleat = nomEmpleat;
this.prestecsActuals = prestecsActuals;
this.limit = limit;
}
public String getIdEmpleat() { return idEmpleat; }
public String getNomEmpleat() { return nomEmpleat; }
public int getPrestecsActuals() { return prestecsActuals; }
public int getLimit() { return limit; }
/** Quants materials ha de retornar per poder prendre'n un altre en prestec. */
public int devolucionsNecessaries() {
return Math.max(1, prestecsActuals - limit + 1);
}
}package com.nexussoftware.bibliotech.domini;
/** S'intenta retornar un prestec que ja s'havia retornat. */
public class PrestecJaRetornatException extends PrestecException {
private static final long serialVersionUID = 1L;
private final String referenciaPrestec;
private final int diaDevolucioOriginal;
public PrestecJaRetornatException(String referenciaPrestec, int diaDevolucioOriginal) {
super("El prestec " + referenciaPrestec + " ja es va retornar el dia "
+ diaDevolucioOriginal);
this.referenciaPrestec = referenciaPrestec;
this.diaDevolucioOriginal = diaDevolucioOriginal;
}
public String getReferenciaPrestec() { return referenciaPrestec; }
public int getDiaDevolucioOriginal() { return diaDevolucioOriginal; }
/**
* Aquesta fallada gairebe sempre indica un problema de sequencia al codi
* o un doble clic de l'usuari, no una cosa que l'usuari pugui "corregir".
*/
@Override
public boolean esCorregiblePerUsuari() {
return false;
}
}I l'única comprovada de la família, que no penja de BiblioTechException precisament perquè la seva naturalesa és diferent:
package com.nexussoftware.bibliotech.servei;
/**
* El cataleg persistit no es pot llegir o escriure.
*
* Es COMPROVADA perque representa una fallada de l'entorn (disc, permisos,
* fitxer absent) de la qual l'aplicacio SI que te una alternativa raonable:
* arrencar amb el cataleg buit. Obligar a decidir-ho explicitament es
* justament el que es vol.
*
* No esten BiblioTechException perque aquella arrel es no comprovada i
* representa regles de negoci; aixo es una fallada d'infraestructura.
* El modul 7 desenvolupara la part d'E/S.
*/
public class CatalegNoAccessibleException extends Exception {
private static final long serialVersionUID = 1L;
private final String cami;
public CatalegNoAccessibleException(String cami, Throwable causa) {
super("No s'ha pogut accedir al cataleg a '" + cami + "'", causa);
this.cami = cami;
}
public String getCami() { return cami; }
}Ara el Cataleg refactoritzat:
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Material;
import com.nexussoftware.bibliotech.domini.MaterialNoTrobatException;
import com.nexussoftware.bibliotech.domini.ReferenciaDuplicadaException;
/** Cataleg de BiblioTech amb excepcions del domini. */
public class Cataleg {
private final List<Material> materials = new ArrayList<>();
private final Map<String, Material> indexPerReferencia = new HashMap<>();
private final Map<String, String> titolsPerIsbn = new HashMap<>();
/**
* Registra un material.
*
* @throws NullPointerException si el material es nul (fallada tecnica)
* @throws ReferenciaDuplicadaException si la referencia o l'ISBN ja existeixen
*/
public void registrar(Material material) {
// Validacio tecnica: excepcio ESTANDARD
Objects.requireNonNull(material, "El material a registrar no pot ser nul");
String referencia = material.getReferencia();
// Regla de negoci: excepcio PROPIA, amb dades
Material existent = indexPerReferencia.get(referencia);
if (existent != null) {
throw new ReferenciaDuplicadaException(
ReferenciaDuplicadaException.Tipus.REFERENCIA,
referencia, existent.getTitol());
}
if (material instanceof Llibre llibre) {
String titolAmbAquestIsbn = titolsPerIsbn.get(llibre.getIsbn());
if (titolAmbAquestIsbn != null) {
throw new ReferenciaDuplicadaException(
ReferenciaDuplicadaException.Tipus.ISBN,
llibre.getIsbn(), titolAmbAquestIsbn);
}
titolsPerIsbn.put(llibre.getIsbn(), llibre.getTitol());
}
materials.add(material);
indexPerReferencia.put(referencia, material);
}
/**
* Obte un material EXIGINT que existeixi. Mai no retorna null.
*
* @throws MaterialNoTrobatException si no existeix
*/
public Material obtenirPerReferencia(String referencia) {
Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
Material trobat = indexPerReferencia.get(referencia);
if (trobat == null) {
throw new MaterialNoTrobatException(referencia, materials.size());
}
return trobat;
}
/** Cerca ADMETENT que no existeixi. Mai no retorna null: retorna Optional. */
public Optional<Material> cercarPerReferencia(String referencia) {
if (referencia == null) { return Optional.empty(); }
return Optional.ofNullable(indexPerReferencia.get(referencia));
}
public List<Material> cercarPerPrefix(String prefix) {
List<Material> resultat = new ArrayList<>();
for (Material m : materials) {
if (m.getReferencia().startsWith(prefix)) {
resultat.add(m);
}
}
return resultat;
}
public boolean existeix(String referencia) {
return referencia != null && indexPerReferencia.containsKey(referencia);
}
public int mida() { return materials.size(); }
public List<Material> llistar() { return List.copyOf(materials); }
}I la demostració que mostra el que hem guanyat:
package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.servei.GestorPrestecs;
/**
* Demostra la diferencia entre capturar per TIPUS amb dades estructurades
* i capturar tipus estandard llegint el missatge.
*/
public class DemoExcepcionsDomini {
public static void main(String[] args) {
Cataleg cataleg = new Cataleg();
cataleg.registrar(new Llibre("LIB-0001", "Java Eficac", "Bloch", 2018, "978-0000000001"));
cataleg.registrar(new Llibre("LIB-0002", "Patrons de Disseny", "GoF", 1994, "978-0000000002"));
cataleg.registrar(new Llibre("LIB-0003", "Refactoritzacio", "Fowler", 1999, "978-0000000003"));
GestorPrestecs gestor = new GestorPrestecs(cataleg);
gestor.donarAlta(new Empleat("Marta Ruiz", "EMP-001"));
gestor.donarAlta(new Empleat("Diego Alonso", "EMP-002"));
// Marta esgota la seva quota
gestor.prestar("LIB-0001", "EMP-001", 10);
gestor.prestar("LIB-0002", "EMP-001", 10);
gestor.prestar("LIB-0003", "EMP-001", 10);
System.out.println("=== Fallades, cadascuna amb EL SEU gestor ===\n");
operar(gestor, cataleg, "LIB-9999", "EMP-001", 12); // no trobat
operar(gestor, cataleg, "LIB-0001", "EMP-002", 12); // no disponible
operar(gestor, cataleg, "LIB-0001", "EMP-001", 12); // no disponible (i limit)
System.out.println("\n=== Duplicats ===\n");
registrar(cataleg, new Llibre("LIB-0001", "Altre titol", "X", 2020, "978-0000000009"));
registrar(cataleg, new Llibre("LIB-0004", "Copia", "Bloch", 2018, "978-0000000001"));
System.out.println("\n=== Captura per l'ARREL: tot el domini d'una vegada ===\n");
try {
cataleg.obtenirPerReferencia("LIB-7777");
} catch (BiblioTechException e) {
System.out.println("Codi: " + e.getCodi());
System.out.println("Missatge: " + e.getMessage());
System.out.println("Corregible per l'usuari? " + e.esCorregiblePerUsuari());
}
}
/** Cada tipus de fallada es gestiona de manera DIFERENT usant les seves dades. */
private static void operar(GestorPrestecs gestor, Cataleg cataleg,
String referencia, String idEmpleat, int dia) {
System.out.println("--- prestar(" + referencia + ", " + idEmpleat + ") ---");
try {
gestor.prestar(referencia, idEmpleat, dia);
System.out.println(" OK");
} catch (MaterialNoTrobatException e) {
// Usa les DADES de l'excepcio per ajudar l'usuari
System.out.println(" No s'ha trobat '" + e.getReferenciaCercada() + "'.");
if (e.catalegBuit()) {
System.out.println(" El cataleg esta buit: carregueu primer els materials.");
} else {
var semblants = cataleg.cercarPerPrefix(
e.getReferenciaCercada().substring(0, 4));
System.out.println(" Materials amb prefix similar: " + semblants.size());
}
} catch (MaterialNoDisponibleException e) {
System.out.println(" '" + e.getTitol() + "' el te " + e.getIdTitularActual() + ".");
System.out.println(" Torna en " + e.diesEspera(dia) + " dies.");
System.out.println(" -> S'afegeix a la cua de reserves de " + e.getReferencia());
} catch (LimitPrestecsExceditException e) {
System.out.println(" " + e.getNomEmpleat() + " te "
+ e.getPrestecsActuals() + "/" + e.getLimit() + " prestecs.");
System.out.println(" Ha de retornar " + e.devolucionsNecessaries()
+ " material(s) abans de prendre'n un altre.");
}
}
private static void registrar(Cataleg cataleg, Material material) {
try {
cataleg.registrar(material);
System.out.println("Registrat: " + material.getReferencia());
} catch (ReferenciaDuplicadaException e) {
// El TIPUS del duplicat permet un missatge precis, sense parsejar text
String queColliseix = (e.getTipus() == ReferenciaDuplicadaException.Tipus.ISBN)
? "L'ISBN" : "La referencia";
System.out.println(queColliseix + " " + e.getValorDuplicat()
+ " ja la fa servir '" + e.getTitolExistent() + "'. Alta rebutjada.");
}
}
}Sortida:
=== Fallades, cadascuna amb EL SEU gestor === --- prestar(LIB-9999, EMP-001) --- No s'ha trobat 'LIB-9999'. Materials amb prefix similar: 0 --- prestar(LIB-0001, EMP-002) --- 'Java Eficac' el te EMP-001. Torna en 13 dies. -> S'afegeix a la cua de reserves de LIB-0001 --- prestar(LIB-0001, EMP-001) --- 'Java Eficac' el te EMP-001. Torna en 13 dies. -> S'afegeix a la cua de reserves de LIB-0001 === Duplicats === La referencia LIB-0001 ja la fa servir 'Java Eficac'. Alta rebutjada. L'ISBN 978-0000000001 ja la fa servir 'Java Eficac'. Alta rebutjada. === Captura per l'ARREL: tot el domini d'una vegada === Codi: MATERIALNOTROBAT Missatge: No existeix cap material amb la referencia 'LIB-7777' (el cataleg te 3 materials) Corregible per l'usuari? true
Compara aquesta sortida amb la de 06-03. Allà cada fallada produïa una línia de text que només servia per llegir-la. Aquí, cada fallada desencadena una reacció diferent i específica: buscar materials semblants, oferir una reserva amb els dies d'espera calculats, o dir-li a l'empleat quants materials ha de retornar. Tota aquesta lògica és possible perquè les dades vénen en camps, no en una cadena.
Errors Comuns i Consells
Crear una excepció per cada camp validat. NomNulException, AnyInvalidException, TitolBuitException... Substitueixes un vocabulari estàndard i conegut per un de propi que ningú no coneix. Les validacions tècniques fan servir IllegalArgumentException i NullPointerException; les regles de negoci, les teves.
Fer comprovades les excepcions de negoci. Contaminen les signatures de tota l'aplicació, impedeixen llançar-les des de lambdes i empenyen cap al catch buit. Reserva les comprovades per a les fallades de l'entorn on qui crida farà alguna cosa diferent de propagar.
Oblidar el constructor (String, Throwable). Sense ell, qui necessiti embolcallar una fallada haurà de descartar la causa o fer servir un altre tipus. És la via directa als stack traces mutilats de 06-03.
Camps no final. Una excepció descriu un fet ocorregut: ningú no l'hauria de poder modificar després de llançar-la.
Guardar objectes grans a l'excepció. Manté vives referències que el recol·lector no pot alliberar, i trenca la serialització si aquells objectes no són Serializable. Guarda identificadors.
Parsejar el missatge per prendre decisions. if (e.getMessage().contains("limit")) es trenca en silenci tan bon punt algú millori el text o el tradueixi. Si necessites aquella dada, posa-la en un camp.
Una jerarquia més profunda del que faràs servir. Si mai no escriuràs un catch d'un nivell intermedi, aquell nivell sobra. Tres nivells és un màxim raonable.
Arrel no abstracta. Si BiblioTechException és instanciable, algú acabarà llançant-la directament, i tornaràs al punt de partida: una fallada sense nom concret.
Noms sense el sufix Exception. Trenca la convenció universal de Java i confon qualsevol que llegeixi el codi. I Error com a sufix és pitjor: suggereix una fallada irrecuperable de la JVM.
Consell: construeix el missatge al super(...) a partir dels camps. Així el text i les dades no es poden contradir, i tot el format del missatge viu en un sol lloc.
Consell: afegeix mètodes de conveniència útils. diesEspera(diaActual), devolucionsNecessaries(), catalegBuit(). L'excepció sap coses sobre la fallada; deixa que les calculi ella en comptes de repetir l'aritmètica a cada catch.
Consell: declara serialVersionUID = 1L. Una línia que silencia l'avís del compilador i et dona control sobre la compatibilitat de serialització. La mecànica completa, a 07-05.
Consell: documenta cada excepció amb Javadoc i @throws als mètodes que la llancen. És l'única manera que qui faci servir la teva API sàpiga què esperar sense llegir la implementació.
Exercicis
Exercici 1: excepció amb dades i lògica
Crea MultaExcessivaException, que es llançarà quan un empleat acumuli una multa per sobre d'un llindar i no pugui prendre més préstecs fins a saldar-la.
Requisits:
- Estén
PrestecException. - Camps
final:idEmpleat,nomEmpleat,quantitatAcumulada(double),llindarPermes(double),materialsAmbMulta(unaList<String>de referències). - Constructors: el complet i un que a més accepti
Throwable causa. - El missatge es construeix a partir dels camps i inclou la quantitat amb dos decimals.
- Accessors per a tots els camps. La llista es retorna com a còpia immutable.
- Mètodes de conveniència:
double getExces(): quant sobrepassa el llindar.boolean superaLaMultaMaxima(): siquantitatAcumulada >= MULTA_MAXIMA(20.0).String getResum(): una línia per material amb multa.
- Redefineix
esCorregiblePerUsuari()retornanttrue(l'empleat pot pagar).
Escriu un main que la llanci amb tres materials en multa i la capturi, demostrant l'ús de tots els seus mètodes.
Exercici 2: redisseny d'una jerarquia mal feta
Un company ha escrit aquesta jerarquia per al mòdul de reserves de BiblioTech. Té set problemes de disseny. Identifica'ls, explica el mal de cadascun i reescriu la jerarquia correctament.
public class ErrorReserva extends Throwable {
public ErrorReserva() { }
}
public class ReservaNoValida extends ErrorReserva {
public String missatge;
public ReservaNoValida(String m) { this.missatge = m; }
}
public class ErrorDataReserva extends Exception {
public ErrorDataReserva(String m) { super(m); }
}
public class ReservaDuplicadaError extends ReservaNoValida {
public ReservaDuplicadaError() { super("Error"); }
}
// Us tipic al codi:
// try {
// cuaReserves.reservar(referencia, idEmpleat, dia);
// } catch (Throwable t) {
// if (t.getMessage() != null && t.getMessage().startsWith("Error")) {
// System.out.println("Fallada de reserva");
// }
// }Exercici 3: CuaReserves amb la seva pròpia família d'excepcions
Amplia el servei CuaReserves de BiblioTech amb una jerarquia pròpia i dades estructurades.
Requisits:
- Crea
ReservaException extends BiblioTechException(abstracta) i tres fulles:ReservaDuplicadaException: el mateix empleat ja té una reserva d'aquell material. Camps:referencia,idEmpleat,posicioActual.CuaReservesPlenaException: la cua d'aquell material ha assolit el màxim (MAX_RESERVES_PER_MATERIAL = 5). Camps:referencia,maxim,diaEstimatDisponibilitat.ReservaNoTrobadaException: s'intenta cancel·lar una reserva inexistent. Camps:referencia,idEmpleat.
CuaReservesmanté unMap<String, Deque<Reserva>>(una cua FIFO per material, com a 05-07) i ofereix:int reservar(String referencia, String idEmpleat, int dia): retorna la posició a la cua (1 = el següent).void cancellar(String referencia, String idEmpleat).Optional<Reserva> atendreSeguent(String referencia): treu el primer de la cua.int posicioDe(String referencia, String idEmpleat):-1si no hi és.
- Valida els arguments amb excepcions estàndard i les regles de negoci amb les pròpies.
- El
mainha de demostrar les tres fallades, una cua completa amb els tres empleats del projecte i l'atenció de la cua en ordre FIFO. Afegeix un gestor que capturi perReservaException(la branca) i mostrigetCodi().
Solucions
Solució 1
package com.nexussoftware.bibliotech.domini;
import java.util.List;
/**
* L'empleat ha acumulat una multa que li impedeix prendre mes prestecs
* fins a saldar-la.
*
* Transporta el desglossament complet perque la capa de presentacio pugui
* mostrar un rebut, i perque la de servei pugui decidir si aplica
* una excepcio a la regla.
*/
public class MultaExcessivaException extends PrestecException {
private static final long serialVersionUID = 1L;
public static final double MULTA_MAXIMA = 20.0;
private final String idEmpleat;
private final String nomEmpleat;
private final double quantitatAcumulada;
private final double llindarPermes;
private final List<String> materialsAmbMulta;
public MultaExcessivaException(String idEmpleat, String nomEmpleat,
double quantitatAcumulada, double llindarPermes,
List<String> materialsAmbMulta) {
super(construirMissatge(idEmpleat, nomEmpleat, quantitatAcumulada,
llindarPermes, materialsAmbMulta));
this.idEmpleat = idEmpleat;
this.nomEmpleat = nomEmpleat;
this.quantitatAcumulada = quantitatAcumulada;
this.llindarPermes = llindarPermes;
// Copia defensiva: l'excepcio ha de ser immutable encara que la llista
// original canvii despres (03-07)
this.materialsAmbMulta = List.copyOf(materialsAmbMulta);
}
public MultaExcessivaException(String idEmpleat, String nomEmpleat,
double quantitatAcumulada, double llindarPermes,
List<String> materialsAmbMulta, Throwable causa) {
super(construirMissatge(idEmpleat, nomEmpleat, quantitatAcumulada,
llindarPermes, materialsAmbMulta), causa);
this.idEmpleat = idEmpleat;
this.nomEmpleat = nomEmpleat;
this.quantitatAcumulada = quantitatAcumulada;
this.llindarPermes = llindarPermes;
this.materialsAmbMulta = List.copyOf(materialsAmbMulta);
}
/**
* El missatge es construeix en un metode estatic perque cal cridar-lo
* al super(...), abans que els camps existeixin.
*/
private static String construirMissatge(String idEmpleat, String nomEmpleat,
double quantitat, double llindar,
List<String> materials) {
return String.format(
"L'empleat %s (%s) acumula %.2f EUR de multa, per sobre del llindar de %.2f EUR, "
+ "en %d material(s)",
idEmpleat, nomEmpleat, quantitat, llindar, materials.size());
}
public String getIdEmpleat() { return idEmpleat; }
public String getNomEmpleat() { return nomEmpleat; }
public double getQuantitatAcumulada() { return quantitatAcumulada; }
public double getLlindarPermes() { return llindarPermes; }
/** Ja es immutable per List.copyOf, pero es retorna explicitament aixi. */
public List<String> getMaterialsAmbMulta() {
return materialsAmbMulta;
}
// ---------- Metodes de conveniencia ----------
/** Quant cal pagar com a minim per tornar a estar per sota del llindar. */
public double getExces() {
return Math.max(0.0, quantitatAcumulada - llindarPermes);
}
/** S'ha assolit el sostre absolut de multa del sistema? */
public boolean superaLaMultaMaxima() {
return quantitatAcumulada >= MULTA_MAXIMA;
}
/** Desglossament llegible, una linia per material. */
public String getResum() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("Multa de %s: %.2f EUR (llindar %.2f, exces %.2f)%n",
nomEmpleat, quantitatAcumulada, llindarPermes, getExces()));
for (String referencia : materialsAmbMulta) {
sb.append(" - ").append(referencia).append('\n');
}
if (superaLaMultaMaxima()) {
sb.append(String.format(" ATENCIO: s'ha assolit la multa maxima (%.2f EUR)%n",
MULTA_MAXIMA));
}
return sb.toString();
}
/** L'empleat ho pot resoldre pagant: es corregible per ell. */
@Override
public boolean esCorregiblePerUsuari() {
return true;
}
// ---------- Demostracio ----------
public static void main(String[] args) {
try {
throw new MultaExcessivaException(
"EMP-001", "Marta Ruiz",
23.75, 10.0,
List.of("LIB-0001", "LIB-0002", "REV-0007"));
} catch (MultaExcessivaException e) {
System.out.println("=== Captura especifica ===");
System.out.println("Codi : " + e.getCodi());
System.out.println("Missatge : " + e.getMessage());
System.out.println("Empleat : " + e.getIdEmpleat()
+ " (" + e.getNomEmpleat() + ")");
System.out.printf ("Quantitat acumulada : %.2f EUR%n", e.getQuantitatAcumulada());
System.out.printf ("Llindar permes : %.2f EUR%n", e.getLlindarPermes());
System.out.printf ("Exces a pagar : %.2f EUR%n", e.getExces());
System.out.println("Supera el maxim : " + e.superaLaMultaMaxima());
System.out.println("Materials : " + e.getMaterialsAmbMulta());
System.out.println("Corregible per l'usuari: " + e.esCorregiblePerUsuari());
System.out.println("\n=== Resum per al rebut ===");
System.out.print(e.getResum());
// Demostracio que la llista es IMMUTABLE
System.out.println("=== Immutabilitat ===");
try {
e.getMaterialsAmbMulta().add("LIB-9999");
} catch (UnsupportedOperationException uoe) {
System.out.println("La llista de materials no es pot modificar. Correcte.");
}
}
// Captura per la branca i per l'arrel
System.out.println("\n=== Captura per la branca PrestecException ===");
try {
throw new MultaExcessivaException("EMP-002", "Diego Alonso",
12.5, 10.0, List.of("LIB-0003"));
} catch (PrestecException e) {
System.out.println("Fallada de prestec [" + e.getCodi() + "]: " + e.getMessage());
}
}
}Sortida:
=== Captura especifica === Codi : MULTAEXCESSIVA Missatge : L'empleat EMP-001 (Marta Ruiz) acumula 23,75 EUR de multa, per sobre del llindar de 10,00 EUR, en 3 material(s) Empleat : EMP-001 (Marta Ruiz) Quantitat acumulada : 23,75 EUR Llindar permes : 10,00 EUR Exces a pagar : 13,75 EUR Supera el maxim : true Materials : [LIB-0001, LIB-0002, REV-0007] Corregible per l'usuari: true === Resum per al rebut === Multa de Marta Ruiz: 23,75 EUR (llindar 10,00, exces 13,75) - LIB-0001 - LIB-0002 - REV-0007 ATENCIO: s'ha assolit la multa maxima (20,00 EUR) === Immutabilitat === La llista de materials no es pot modificar. Correcte. === Captura per la branca PrestecException === Fallada de prestec [MULTAEXCESSIVA]: L'empleat EMP-002 (Diego Alonso) acumula 12,50 EUR de multa, per sobre del llindar de 10,00 EUR, en 1 material(s)
Solució 2
Els set problemes:
| # | Problema | Mal |
|---|---|---|
| 1 | ErrorReserva extends Throwable |
No és ni Error ni Exception: s'escapa de tot catch (Exception e) i de tot catch (RuntimeException e) sense ser una fallada de la JVM. A més, en ser comprovada, obliga a throws Throwable per tota l'aplicació |
| 2 | Noms sense sufix Exception |
ErrorReserva, ReservaNoValida, ReservaDuplicadaError trenquen la convenció. I Error suggereix una fallada irrecuperable de la JVM |
| 3 | public String missatge en comptes de fer servir super(m) |
Camp públic, mutable i duplicat: getMessage() retornarà null mentre el text viu en un altre lloc. Els stack traces sortiran sense missatge |
| 4 | ErrorDataReserva extends Exception, fora de la jerarquia |
Trenca la família: no es pot capturar amb l'arrel comuna. I és comprovada sense motiu |
| 5 | ReservaDuplicadaError amb missatge fix "Error" |
No diu quin material, ni quin empleat, ni quina posició. Zero valor diagnòstic |
| 6 | Cap classe transporta dades | Tot l'estat de la fallada es perd. Qui captura només pot llegir text |
| 7 | L'ús: catch (Throwable t) + getMessage().startsWith("Error") |
Captura OutOfMemoryError, i la lògica depèn d'un prefix de text que es trenca tan bon punt algú millori el missatge. A més, getMessage() pot ser null |
Versió corregida:
package com.nexussoftware.bibliotech.domini;
/** Arrel de les fallades del subsistema de reserves. */
public abstract class ReservaException extends BiblioTechException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final String idEmpleat;
protected ReservaException(String missatge, String referencia, String idEmpleat) {
super(missatge);
this.referencia = referencia;
this.idEmpleat = idEmpleat;
}
protected ReservaException(String missatge, String referencia, String idEmpleat,
Throwable causa) {
super(missatge, causa);
this.referencia = referencia;
this.idEmpleat = idEmpleat;
}
/** Dades comunes a tota la familia: es declaren una sola vegada, a l'arrel. */
public String getReferencia() { return referencia; }
public String getIdEmpleat() { return idEmpleat; }
}package com.nexussoftware.bibliotech.domini;
/** L'empleat ja te una reserva activa d'aquell material. */
public class ReservaDuplicadaException extends ReservaException {
private static final long serialVersionUID = 1L;
private final int posicioActual;
public ReservaDuplicadaException(String referencia, String idEmpleat, int posicioActual) {
super("L'empleat " + idEmpleat + " ja te una reserva de " + referencia
+ " a la posicio " + posicioActual + " de la cua",
referencia, idEmpleat);
this.posicioActual = posicioActual;
}
public int getPosicioActual() { return posicioActual; }
}package com.nexussoftware.bibliotech.domini;
/** El dia de la reserva no es valid per a aquell material. */
public class DiaReservaInvalidException extends ReservaException {
private static final long serialVersionUID = 1L;
private final int diaSollicitat;
private final int diaMinim;
public DiaReservaInvalidException(String referencia, String idEmpleat,
int diaSollicitat, int diaMinim) {
super("La reserva de " + referencia + " es va sollicitar per al dia " + diaSollicitat
+ ", pero el material no estara disponible fins al dia " + diaMinim,
referencia, idEmpleat);
this.diaSollicitat = diaSollicitat;
this.diaMinim = diaMinim;
}
public int getDiaSollicitat() { return diaSollicitat; }
public int getDiaMinim() { return diaMinim; }
public int getDiesEspera() { return Math.max(0, diaMinim - diaSollicitat); }
}I l'ús corregit:
try {
int posicio = cuaReserves.reservar(referencia, idEmpleat, dia);
System.out.println("Reservat. Posicio a la cua: " + posicio);
} catch (ReservaDuplicadaException e) {
// Es fan servir les DADES, no el text
System.out.println("Ja tenia una reserva de " + e.getReferencia()
+ ", a la posicio " + e.getPosicioActual() + ". No s'ha duplicat.");
} catch (DiaReservaInvalidException e) {
System.out.println("Aquell material no estara lliure fins d'aqui a "
+ e.getDiesEspera() + " dies.");
System.out.println("Voleu reservar-lo per al dia " + e.getDiaMinim() + "? (s/n)");
} catch (ReservaException e) {
// Xarxa de seguretat de la BRANCA, no de Throwable
System.out.println("No s'ha pogut completar la reserva [" + e.getCodi() + "]: "
+ e.getMessage());
}Els canvis, en resum: arrel abstracta dins de BiblioTechException, sufix Exception a tots els noms, missatges construïts amb super(...) i amb les dades concretes, camps comuns declarats a l'arrel, camps específics a cada fulla, tot no comprovat per ser regles de negoci, i captures per tipus en comptes de per prefix del missatge.
Solució 3
package com.nexussoftware.bibliotech.domini;
/** Arrel de les fallades de la cua de reserves. */
public abstract class ReservaException extends BiblioTechException {
private static final long serialVersionUID = 1L;
protected ReservaException(String missatge) { super(missatge); }
}package com.nexussoftware.bibliotech.domini;
public class ReservaDuplicadaException extends ReservaException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final String idEmpleat;
private final int posicioActual;
public ReservaDuplicadaException(String referencia, String idEmpleat, int posicioActual) {
super("L'empleat " + idEmpleat + " ja te reservat " + referencia
+ " (posicio " + posicioActual + " de la cua)");
this.referencia = referencia;
this.idEmpleat = idEmpleat;
this.posicioActual = posicioActual;
}
public String getReferencia() { return referencia; }
public String getIdEmpleat() { return idEmpleat; }
public int getPosicioActual() { return posicioActual; }
}package com.nexussoftware.bibliotech.domini;
public class CuaReservesPlenaException extends ReservaException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final int maxim;
private final int diaEstimatDisponibilitat;
public CuaReservesPlenaException(String referencia, int maxim,
int diaEstimatDisponibilitat) {
super("La cua de reserves de " + referencia + " esta plena (" + maxim
+ " reserves). Disponibilitat estimada: dia " + diaEstimatDisponibilitat);
this.referencia = referencia;
this.maxim = maxim;
this.diaEstimatDisponibilitat = diaEstimatDisponibilitat;
}
public String getReferencia() { return referencia; }
public int getMaxim() { return maxim; }
public int getDiaEstimatDisponibilitat() { return diaEstimatDisponibilitat; }
@Override
public boolean esCorregiblePerUsuari() {
return false; // l'usuari no pot fer res: la cua esta plena
}
}package com.nexussoftware.bibliotech.domini;
public class ReservaNoTrobadaException extends ReservaException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final String idEmpleat;
public ReservaNoTrobadaException(String referencia, String idEmpleat) {
super("L'empleat " + idEmpleat + " no te cap reserva de " + referencia);
this.referencia = referencia;
this.idEmpleat = idEmpleat;
}
public String getReferencia() { return referencia; }
public String getIdEmpleat() { return idEmpleat; }
}package com.nexussoftware.bibliotech.servei;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.nexussoftware.bibliotech.domini.CuaReservesPlenaException;
import com.nexussoftware.bibliotech.domini.ReservaDuplicadaException;
import com.nexussoftware.bibliotech.domini.ReservaException;
import com.nexussoftware.bibliotech.domini.ReservaNoTrobadaException;
/**
* Cua FIFO de reserves per material (recupera l'ArrayDeque de 05-07).
*
* Politica d'errors:
* - Arguments invalids -> excepcions ESTANDARD (NPE, IllegalArgument).
* - Regles de negoci -> excepcions PROPIES amb dades.
*/
public class CuaReserves {
public static final int MAX_RESERVES_PER_MATERIAL = 5;
private static final int DIES_ESTIMATS_PER_TORN = 15;
/** Reserva d'un empleat sobre un material (record, 04-07). */
public record Reserva(String referencia, String idEmpleat, int dia) { }
private final Map<String, Deque<Reserva>> cuesPerMaterial = new HashMap<>();
/**
* Afegeix una reserva al final de la cua del material.
*
* @return posicio a la cua, sent 1 el seguent a ser ates
* @throws NullPointerException si algun argument es nul
* @throws IllegalArgumentException si el dia es menor que 1
* @throws ReservaDuplicadaException si aquell empleat ja va reservar aquell material
* @throws CuaReservesPlenaException si la cua va assolir el maxim
*/
public int reservar(String referencia, String idEmpleat, int dia) {
// Validacio tecnica: excepcions estandard
Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
Objects.requireNonNull(idEmpleat, "L'identificador de l'empleat no pot ser nul");
if (dia < 1) {
throw new IllegalArgumentException("El dia ha de ser 1 o posterior, i era: " + dia);
}
Deque<Reserva> cua = cuesPerMaterial.computeIfAbsent(referencia, r -> new ArrayDeque<>());
// Regla de negoci 1: no duplicar
int jaHiEs = posicioA(cua, idEmpleat);
if (jaHiEs > 0) {
throw new ReservaDuplicadaException(referencia, idEmpleat, jaHiEs);
}
// Regla de negoci 2: cua plena
if (cua.size() >= MAX_RESERVES_PER_MATERIAL) {
int diaEstimat = dia + cua.size() * DIES_ESTIMATS_PER_TORN;
throw new CuaReservesPlenaException(referencia, MAX_RESERVES_PER_MATERIAL, diaEstimat);
}
cua.addLast(new Reserva(referencia, idEmpleat, dia));
return cua.size();
}
/**
* Cancella la reserva d'un empleat sobre un material.
*
* @throws ReservaNoTrobadaException si no hi havia aquella reserva
*/
public void cancellar(String referencia, String idEmpleat) {
Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
Objects.requireNonNull(idEmpleat, "L'identificador de l'empleat no pot ser nul");
Deque<Reserva> cua = cuesPerMaterial.get(referencia);
if (cua == null) {
throw new ReservaNoTrobadaException(referencia, idEmpleat);
}
// removeIf sobre un Deque: forma segura d'eliminar mentre s'itera (05-04)
boolean eliminada = cua.removeIf(r -> r.idEmpleat().equals(idEmpleat));
if (!eliminada) {
throw new ReservaNoTrobadaException(referencia, idEmpleat);
}
if (cua.isEmpty()) {
cuesPerMaterial.remove(referencia); // no deixar cues buides al mapa
}
}
/**
* Treu el primer de la cua. Retorna Optional buit si no hi ha ningu
* esperant: aqui l'absencia es un cas NORMAL, no una fallada.
*/
public Optional<Reserva> atendreSeguent(String referencia) {
Deque<Reserva> cua = cuesPerMaterial.get(referencia);
if (cua == null || cua.isEmpty()) {
return Optional.empty();
}
Reserva primera = cua.pollFirst();
if (cua.isEmpty()) {
cuesPerMaterial.remove(referencia);
}
return Optional.of(primera);
}
/** Posicio de l'empleat a la cua d'aquell material, o -1 si no hi es. */
public int posicioDe(String referencia, String idEmpleat) {
Deque<Reserva> cua = cuesPerMaterial.get(referencia);
if (cua == null) { return -1; }
int p = posicioA(cua, idEmpleat);
return (p > 0) ? p : -1;
}
/** Retorna la posicio 1..n, o 0 si no hi es. */
private int posicioA(Deque<Reserva> cua, String idEmpleat) {
int posicio = 1;
for (Iterator<Reserva> it = cua.iterator(); it.hasNext(); posicio++) {
if (it.next().idEmpleat().equals(idEmpleat)) {
return posicio;
}
}
return 0;
}
public int midaCua(String referencia) {
Deque<Reserva> cua = cuesPerMaterial.get(referencia);
return (cua == null) ? 0 : cua.size();
}
// ------------------------------------------------------------------
public static void main(String[] args) {
CuaReserves cues = new CuaReserves();
String LLIBRE = "LIB-0001";
System.out.println("=== Reserves correctes ===");
System.out.println("Marta -> posicio " + cues.reservar(LLIBRE, "EMP-001", 10));
System.out.println("Diego -> posicio " + cues.reservar(LLIBRE, "EMP-002", 11));
System.out.println("Nuria -> posicio " + cues.reservar(LLIBRE, "EMP-003", 12));
System.out.println("\n=== Fallades ===");
intentar("Reserva duplicada", () -> cues.reservar(LLIBRE, "EMP-001", 13));
intentar("Cancellar inexistent", () -> cues.cancellar(LLIBRE, "EMP-009"));
intentar("Dia invalid", () -> cues.reservar(LLIBRE, "EMP-004", 0));
intentar("Referencia nulla", () -> cues.reservar(null, "EMP-004", 5));
// Omplir la cua fins al maxim
cues.reservar(LLIBRE, "EMP-004", 13);
cues.reservar(LLIBRE, "EMP-005", 14);
System.out.println("\nCua de " + LLIBRE + ": " + cues.midaCua(LLIBRE)
+ "/" + MAX_RESERVES_PER_MATERIAL);
intentar("Cua plena", () -> cues.reservar(LLIBRE, "EMP-006", 15));
System.out.println("\n=== Atencio FIFO ===");
Optional<Reserva> seguent;
while ((seguent = cues.atendreSeguent(LLIBRE)).isPresent()) {
Reserva r = seguent.get();
System.out.println(" Ates " + r.idEmpleat() + " (va reservar el dia " + r.dia() + ")");
}
System.out.println("Cua buida: " + (cues.midaCua(LLIBRE) == 0));
}
/** Gestor que captura per la BRANCA i usa getCodi() de l'arrel. */
private static void intentar(String descripcio, Runnable accio) {
try {
accio.run();
System.out.println("[" + descripcio + "] -> no ha fallat (inesperat)");
} catch (ReservaException e) {
System.out.println("[" + descripcio + "] [" + e.getCodi() + "] " + e.getMessage()
+ " | corregible: " + e.esCorregiblePerUsuari());
} catch (NullPointerException | IllegalArgumentException e) {
System.out.println("[" + descripcio + "] [TECNIC] "
+ e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}Sortida:
=== Reserves correctes === Marta -> posicio 1 Diego -> posicio 2 Nuria -> posicio 3 === Fallades === [Reserva duplicada] [RESERVADUPLICADA] L'empleat EMP-001 ja te reservat LIB-0001 (posicio 1 de la cua) | corregible: true [Cancellar inexistent] [RESERVANOTROBADA] L'empleat EMP-009 no te cap reserva de LIB-0001 | corregible: true [Dia invalid] [TECNIC] IllegalArgumentException: El dia ha de ser 1 o posterior, i era: 0 [Referencia nulla] [TECNIC] NullPointerException: La referencia no pot ser nulla Cua de LIB-0001: 5/5 [Cua plena] [CUARESERVESPLENA] La cua de reserves de LIB-0001 esta plena (5 reserves). Disponibilitat estimada: dia 90 | corregible: false === Atencio FIFO === Ates EMP-001 (va reservar el dia 10) Ates EMP-002 (va reservar el dia 11) Ates EMP-003 (va reservar el dia 12) Ates EMP-004 (va reservar el dia 13) Ates EMP-005 (va reservar el dia 14) Cua buida: true
Fixa't en la separació neta del gestor: un catch per a les regles de negoci —capturat per la branca ReservaException, que a més pot fer servir getCodi() i esCorregiblePerUsuari() de l'arrel— i un altre per a les fallades tècniques amb els tipus estàndard. Els primers són missatges per a l'usuari; els segons són bugs que a 06-07 acabaran al registre amb un identificador d'incidència.
Conclusió
Ja saps crear excepcions que parlin el llenguatge del teu domini. Coneixes les dues raons que ho justifiquen: anomenar la fallada —perquè el tipus sigui la decisió i desapareguin els fràgils e.getMessage().contains("limit")— i, sobretot, transportar dades estructurades, perquè el missatge és per a les persones i els camps són per al programa.
Saps com se'n crea una: estenent Exception o RuntimeException, mai Throwable ni Error directament. I tens el criteri d'elecció entre comprovada i no comprovada, amb la pregunta que ho decideix —farà qui crida alguna cosa diferent de propagar en la majoria dels casos?— i la seva aplicació a BiblioTech: totes les regles de negoci, no comprovades; l'única comprovada, CatalegNoAccessibleException, perquè representa una fallada de l'entorn davant de la qual l'aplicació sí que té una alternativa. Saps a més que una comprovada en una interfície lliga totes les seves implementacions presents i futures, per la regla de sobreescriptura de 06-03.
Domines els quatre constructors canònics i per què el tercer, (String, Throwable), és imprescindible: sense ell, algú acabarà perdent una causa. Saps afegir camps propis —sempre final, amb el missatge construït a partir d'ells al super(...), i guardant identificadors en comptes de grafs d'objectes— i mètodes de conveniència que calculin el que qui captura necessitarà: diesEspera, devolucionsNecessaries, catalegBuit.
Has dissenyat una jerarquia de domini de tres nivells amb una arrel abstracta comuna, i entens què habilita aquella arrel: capturar tot el domini d'una vegada a la frontera d'errors —distingint "l'usuari ha demanat una cosa impossible" de "tenim un bug"—, compartir comportament com getCodi() i esCorregiblePerUsuari(), i disposar d'un punt únic d'evolució. Amb l'advertència de no fer-la més profunda del que faràs servir: si mai no escriuràs un catch d'un nivell intermedi, aquell nivell sobra.
Coneixes les convencions: sufix Exception, noms que descriuen el problema i no la solució, específics i sense el prefix del projecte a les fulles; i missatges que responen què ha passat, amb quina dada i què s'esperava, sense dades sensibles, sense solucions suggerides que depenguin del context i sense salts de línia. Saps declarar serialVersionUID i per què —totes les excepcions són Serializable, i 07-05 desenvolupa la mecànica—, i saps quan NO crear una excepció pròpia: si ja existeix una d'estàndard amb la teva mateixa semàntica i no necessites transportar dades, fes-la servir. Les validacions tècniques fan servir IllegalArgumentException, NullPointerException i IllegalStateException; només les regles de negoci mereixen classes pròpies. I tens la nota avançada sobre writableStackTrace = false per a les excepcions de control molt freqüents, amb l'advertència que una excepció sense stack trace en producció és impossible de diagnosticar.
BiblioTech té ara el seu vocabulari complet: BiblioTechException abstracta com a arrel, CatalegException i PrestecException com a branques, i cinc fulles amb dades —MaterialNoTrobatException amb la referència cercada i la mida del catàleg, ReferenciaDuplicadaException amb el tipus de col·lisió i el títol existent, MaterialNoDisponibleException amb el titular actual i el dia previst de devolució, LimitPrestecsExceditException amb el límit i els préstecs actuals, i PrestecJaRetornatException amb el dia de la devolució original—, més la comprovada CatalegNoAccessibleException. I la demostració ho prova: cada fallada desencadena ara una reacció diferent i específica —buscar materials semblants, oferir una reserva amb els dies d'espera calculats, dir-li a l'empleat quants materials ha de retornar— en comptes d'imprimir una línia de text.
Queden dues fragilitats de la llista del mòdul 5. La quarta: les operacions poden fallar a mitges deixant l'estat incoherent. Si GestorPrestecs.prestar marca el material com a prestat i després falla en anotar-lo al registre, el material queda bloquejat sense cap préstec que ho justifiqui. Cap excepció, per ben dissenyada que estigui, no arregla això per si sola: cal un mecanisme que garanteixi que cert codi s'executi passi el que passi.
Això és la lliçó següent, Bloc finally: què garanteix exactament finally —que s'executa amb i sense excepció, i també quan el try o el catch fan return, break o continue—, amb la traça completa de l'ordre d'execució; les combinacions try-catch-finally i try-finally sense catch, i per a què serveix aquesta última; el propòsit clàssic d'alliberar recursos; el parany del return dins de finally, que descarta el valor i fins i tot l'excepció pendent; el finally que llança la seva pròpia excepció i fa desaparèixer l'original —el problema que motiva directament el try-with-resources de 06-06—; els dos casos en què finally no s'executa; i el verbós patró manual de tancament anterior a Java 7. Amb l'aplicació que resol la quarta fragilitat: GestorPrestecs garantint que Cataleg i RegistrePrestecs queden coherents encara que l'operació falli a mitges.
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
