La lección anterior terminó con un código que funcionaba pero que dejaba a la vista su propio límite: un catch con cuatro tipos estándar unidos por |, un IllegalArgumentException usado para tres cosas distintas y un catch (IllegalStateException e) incapaz de distinguir "el material no está disponible" de "el empleado ha alcanzado su límite" sin leer un mensaje de texto libre.
Ese es el techo de las excepciones estándar. IllegalArgumentException describe una categoría técnica: "el argumento no vale". No dice nada del negocio. Y el único sitio donde vive la información concreta —qué referencia estaba duplicada, cuál era el límite, cuántos días llevaba prestado— es una cadena de texto que nadie debería parsear.
En esta lección construyes el vocabulario propio de BiblioTech. Al terminar, un catch (LimitePrestamosExcedidoException e) no solo capturará exactamente ese fallo y ningún otro, sino que podrá preguntar e.getLimite() y e.getEmpleado() para decidir si ofrece una reserva, si avisa al responsable o si sugiere devolver algo primero. La diferencia entre leer un mensaje y consultar un dato es la diferencia entre un log y un programa que reacciona.
Contenido
- Por qué crear excepciones propias
- Cómo se crea una excepción
- Comprobada o no comprobada: el criterio de elección
- Los cuatro constructores canónicos
- Añadir campos propios y accesores
- Diseñar una jerarquía de dominio
- La raíz común y lo que permite
- Convenciones de nomenclatura
- Convenciones de mensaje
- Serialización y
serialVersionUID - Cuándo NO crear una excepción propia
- Nota avanzada: excepciones sin stack trace
- BiblioTech: la refactorización completa
- Errores Comunes y Consejos
- Ejercicios
- Por qué crear excepciones propias
Dos razones, y la segunda es la que de verdad cambia el código.
Razón 1: nombrar el fallo en el lenguaje del dominio
Compara estas dos capturas:
// Con excepciones estandar
try {
gestor.prestar("LIB-0001", "EMP-001", 12);
} catch (IllegalStateException e) {
// ¿Que ha pasado? ¿El material esta prestado? ¿El empleado ha llegado al limite?
// ¿El prestamo ya estaba devuelto? Hay que leer el MENSAJE para saberlo.
if (e.getMessage().contains("limite")) { // frágil, horrible, y se rompe al traducir
// ...
}
}
// Con excepciones del dominio
try {
gestor.prestar("LIB-0001", "EMP-001", 12);
} catch (MaterialNoDisponibleException e) {
ofrecerReserva(e.getReferencia(), e.getDiaPrevistoDevolucion());
} catch (LimitePrestamosExcedidoException e) {
mostrarPrestamosActivos(e.getEmpleado());
}En la segunda versión, el tipo de la excepción es la decisión. No hace falta interpretar nada: el compilador enruta cada fallo a su manejador. Y el código se lee como el negocio: "si el material no está disponible, ofrece una reserva".
Ese e.getMessage().contains("limite") de la primera versión no es un ejemplo exagerado. Aparece en código real, y es una bomba: el día que alguien mejore el mensaje o lo traduzca al inglés, la lógica deja de funcionar sin ningún error de compilación y sin ningún test que falle, si los tests no cubrían ese camino.
Razón 2: transportar datos estructurados
Esta es la razón decisiva. Una excepción estándar solo lleva un String:
throw new IllegalStateException(
"El empleado EMP-001 (Marta Ruiz) ya tiene 3 prestamos, el maximo es 3");Para que quien captura pueda hacer algo con esos datos, tendría que extraerlos del texto. Con una excepción propia, los datos van en campos:
throw new LimitePrestamosExcedidoException(empleado, Empleado.MAX_PRESTAMOS_SIMULTANEOS);
// Y quien captura:
} catch (LimitePrestamosExcedidoException e) {
Empleado emp = e.getEmpleado(); // el objeto, no su nombre en una cadena
int limite = e.getLimite(); // el numero, no un texto
int actuales = e.getPrestamosActuales();
System.out.printf("%s tiene %d de %d prestamos.%n",
emp.getNombre(), actuales, limite);
ofrecerDevolucionAnticipada(emp); // se puede USAR el objeto
}La regla que resume ambas razones:
El mensaje es para las personas. Los campos son para el programa. Una excepción bien diseñada ofrece los dos.
- Cómo se crea una excepción
Una excepción es una clase normal que extiende Exception o RuntimeException. En su forma mínima:
package com.nexussoftware.bibliotech.dominio;
/** Version minima: solo el nombre y el mensaje. */
public class MaterialNoEncontradoException extends RuntimeException {
public MaterialNoEncontradoException(String mensaje) {
super(mensaje);
}
}Y ya está. Con esas cinco líneas puedes escribir:
...y capturarla de forma específica. Pero esta versión mínima desaprovecha lo que hace útiles las excepciones propias: no transporta datos y no ofrece el constructor con causa. En los apartados siguientes se completa.
Lo que no hay que hacer:
// MAL: extender Throwable directamente
public class MiExcepcion extends Throwable { } // legal, pero nunca correcto
// MAL: extender Error
public class MiExcepcion extends Error { } // Error es para fallos de la JVM
// MAL: extender una excepcion estandar muy especifica sin motivo
public class MiExcepcion extends NumberFormatException { } // hereda una semantica ajenaExtender directamente de Throwable produce una excepción que no es ni Error ni Exception, así que se escapa de todos los catch (Exception e) habituales sin ser un error de la JVM. No tiene ninguna utilidad práctica.
- Comprobada o no comprobada: el criterio de elección
La decisión más importante al crear una excepción, y la que más se equivoca.
Extender Exception (comprobada) |
Extender RuntimeException (no comprobada) |
|
|---|---|---|
| El compilador obliga a | Capturar o declarar en cada llamada | Nada |
| Efecto en las firmas | Las contamina en toda la cadena | Ninguno |
| Efecto en las lambdas | No se puede lanzar desde Function, Consumer, etc. |
Sin problema |
| Riesgo | Que la gente la silencie con catch vacíos |
Que nadie la capture y llegue al usuario |
| Cuándo elegirla | El llamador casi siempre puede y debe hacer algo distinto de propagar | Es una regla de negocio o un error de programación |
La pregunta que decide:
¿Va a hacer el llamador algo distinto de "propagar" en la mayoría de los casos?
- Sí, casi siempre → comprobada. Ejemplo real: "el fichero de catálogo no existe", donde arriba se decide arrancar con catálogo vacío.
- No, casi nunca → no comprobada. Ejemplo: "esa referencia no existe en el catálogo", donde lo normal es dejar que suba hasta la capa de presentación.
Aplicado a BiblioTech, con el razonamiento explícito de cada decisión:
| Excepción | Tipo | Razón |
|---|---|---|
MaterialNoEncontradoException |
No comprobada | Obligar a un try en cada búsqueda sería insoportable, y lo normal es que suba a presentación |
ReferenciaDuplicadaException |
No comprobada | Es prevenible: existe catalogo.existe(ref) para comprobarlo antes |
LimitePrestamosExcedidoException |
No comprobada | Regla de negocio consultable con puedeTomarPrestado() |
MaterialNoDisponibleException |
No comprobada | Idem, consultable con estaDisponible() |
PrestamoYaDevueltoException |
No comprobada | Error de secuencia: no debería ocurrir con el código bien escrito |
CatalogoNoAccesibleException |
Comprobada | Fallo externo (disco, permisos) y la aplicación sí tiene una alternativa: arrancar vacía |
Observa el patrón: todas las reglas de negocio son no comprobadas; la única comprobada es la que representa un fallo del entorno. Es la postura pragmática que anunciaste en 06-01, y coincide con lo que hacen Spring, Hibernate y prácticamente todo el ecosistema moderno.
Un matiz importante que refuerza la decisión: una excepción comprobada en un método de una interfaz obliga a todas sus implementaciones y a todos sus usuarios. Si Prestable.prestar() declarase throws MaterialNoDisponibleException, cada lambda, cada forEach y cada implementación futura cargaría con ello. La regla de sobrescritura de 06-03 lo hace irreversible: una vez publicada, no puedes quitarla sin romper a nadie... pero tampoco puedes añadirla después sin romper a todos.
- Los cuatro constructores canónicos
Throwable define cuatro constructores públicos, y una excepción bien diseñada los ofrece todos —o al menos los tres primeros:
package com.nexussoftware.bibliotech.dominio;
/** Excepcion con los cuatro constructores canonicos. */
public class BiblioTechException extends RuntimeException {
/** 1. Sin mensaje ni causa. Poco util, pero completa el contrato. */
public BiblioTechException() {
super();
}
/** 2. Con mensaje. El mas usado al ORIGINAR un fallo. */
public BiblioTechException(String mensaje) {
super(mensaje);
}
/** 3. Con mensaje y causa. El que debes usar al ENVOLVER (06-03). */
public BiblioTechException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
/** 4. Solo causa: el mensaje se deriva de causa.toString(). */
public BiblioTechException(Throwable causa) {
super(causa);
}
}Por qué conviene ofrecerlos todos:
| Constructor | Se necesita cuando |
|---|---|
() |
Rara vez. Lo pide algún framework por reflexión (10-03) |
(String) |
Originas el fallo tú, con un mensaje explicativo |
(String, Throwable) |
Envuelves otra excepción añadiendo contexto. Imprescindible |
(Throwable) |
Envuelves sin nada que añadir. Poco recomendable: casi siempre tienes algo mejor que decir que causa.toString() |
La consecuencia práctica de omitir el tercero es que alguien acabará perdiendo una causa. Si tu MaterialNoEncontradoException solo acepta un String, quien necesite envolver un fallo de bajo nivel tendrá que elegir entre usar otro tipo de excepción o descartar la causa. Y ya sabes cómo acaba eso.
Throwable tiene además un quinto constructor protected, que verás en el apartado 12:
protected Throwable(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace)
- Añadir campos propios y accesores
Aquí está la potencia real. Una excepción es un objeto: puede tener el estado que quieras.
package com.nexussoftware.bibliotech.dominio;
/**
* El material solicitado no existe en el catalogo.
*
* Transporta la referencia buscada para que quien la capture pueda
* ofrecerla en un mensaje, buscar alternativas o registrarla, sin
* tener que parsear el mensaje de texto.
*/
public class MaterialNoEncontradoException extends BiblioTechException {
private static final long serialVersionUID = 1L;
/** Los campos de una excepcion deben ser final: una excepcion es inmutable. */
private final String referenciaBuscada;
private final int tamanoCatalogo;
public MaterialNoEncontradoException(String referenciaBuscada, int tamanoCatalogo) {
// El mensaje se construye a partir de los datos: una sola fuente de verdad
super("No existe ningun material con la referencia '" + referenciaBuscada
+ "' (el catalogo tiene " + tamanoCatalogo + " materiales)");
this.referenciaBuscada = referenciaBuscada;
this.tamanoCatalogo = tamanoCatalogo;
}
/** Variante para cuando se envuelve otro fallo. */
public MaterialNoEncontradoException(String referenciaBuscada, int tamanoCatalogo,
Throwable causa) {
super("No existe ningun material con la referencia '" + referenciaBuscada
+ "' (el catalogo tiene " + tamanoCatalogo + " materiales)", causa);
this.referenciaBuscada = referenciaBuscada;
this.tamanoCatalogo = tamanoCatalogo;
}
public String getReferenciaBuscada() { return referenciaBuscada; }
public int getTamanoCatalogo() { return tamanoCatalogo; }
/** Metodo de conveniencia: la excepcion puede aportar logica util. */
public boolean catalogoVacio() {
return tamanoCatalogo == 0;
}
}Y lo que eso permite en el punto de captura:
try {
Material m = catalogo.obtenerPorReferencia(entradaUsuario);
gestor.prestar(m.getReferencia(), "EMP-001", dia);
} catch (MaterialNoEncontradoException e) {
if (e.catalogoVacio()) {
System.out.println("El catalogo esta vacio. ¿Ha cargado el fichero de materiales?");
} else {
System.out.println("No se encontro '" + e.getReferenciaBuscada() + "'.");
// Se pueden usar los DATOS para ayudar: sugerencias por prefijo
List<Material> parecidos = catalogo.buscarPorPrefijo(
e.getReferenciaBuscada().substring(0, 3));
if (!parecidos.isEmpty()) {
System.out.println("¿Quiza queria decir alguno de estos?");
parecidos.forEach(m -> System.out.println(" " + m.getReferencia()));
}
}
}Tres reglas de diseño para los campos de una excepción:
finalsiempre. Una excepción es un objeto inmutable que describe un hecho ocurrido. Nadie debería poder modificarlo después.- Construye el mensaje a partir de los campos, en el
super(...). Así el texto y los datos nunca se contradicen. - Cuidado con guardar objetos grandes. Si guardas el
Materialcompleto en lugar de su referencia, la excepción mantiene viva esa referencia mientras exista, e impide que el recolector se lleve el objeto. Con unMateriales irrelevante; con una colección entera o un flujo abierto, es una fuga de memoria. Guarda identificadores, no grafos de objetos.
- Diseñar una jerarquía de dominio
Una excepción suelta ya aporta. Una jerarquía aporta mucho más, porque permite capturar a distintos niveles de granularidad según convenga a cada capa.
Esta es la de BiblioTech:
flowchart TB
RE["RuntimeException<br/>(del JDK)"]
RE --> BT["BiblioTechException<br/>RAIZ del dominio (abstracta)"]
BT --> CAT["CatalogoException<br/>fallos del catalogo"]
BT --> PRE["PrestamoException<br/>fallos de prestamo"]
CAT --> MNE["MaterialNoEncontradoException<br/>+ getReferenciaBuscada()"]
CAT --> RDE["ReferenciaDuplicadaException<br/>+ getReferencia()<br/>+ getTituloExistente()"]
PRE --> MND["MaterialNoDisponibleException<br/>+ getReferencia()<br/>+ getDiaPrevistoDevolucion()"]
PRE --> LPE["LimitePrestamosExcedidoException<br/>+ getIdEmpleado()<br/>+ getLimite()"]
PRE --> PYD["PrestamoYaDevueltoException<br/>+ getReferenciaPrestamo()<br/>+ getDiaDevolucion()"]
Las tres decisiones de este diseño:
Una raíz abstracta común, BiblioTechException. Permite catch (BiblioTechException e) para atrapar cualquier fallo del dominio de una sola vez —lo que hará la frontera de errores en 06-07— sin arrastrar los NullPointerException que sí son bugs. Es abstracta porque nadie debería lanzarla directamente: sería tan poco informativa como el false mudo que estamos eliminando.
Dos niveles intermedios, CatalogoException y PrestamoException, para las capturas de granularidad media: "cualquier fallo del catálogo" sin tener que enumerar sus tres hijas.
Hojas concretas con datos, que son las que se lanzan y las que llevan los campos útiles.
La granularidad de captura que esto permite, de más ancha a más fina:
// Nivel 1: cualquier fallo del dominio BiblioTech (frontera de errores)
catch (BiblioTechException e) { ... }
// Nivel 2: cualquier fallo relacionado con prestamos
catch (PrestamoException e) { ... }
// Nivel 3: este fallo concreto, con sus datos
catch (LimitePrestamosExcedidoException e) { usar(e.getLimite()); }Y recuerda la regla de orden de 06-02: si combinas varios niveles en el mismo try, del más específico al más general, o no compila.
try {
gestor.prestar(referencia, idEmpleado, dia);
} catch (LimitePrestamosExcedidoException e) { // hoja
ofrecerDevolucionAnticipada(e.getIdEmpleado());
} catch (PrestamoException e) { // rama
System.out.println("No se pudo completar el prestamo: " + e.getMessage());
} catch (BiblioTechException e) { // raiz
System.out.println("Fallo de BiblioTech: " + e.getMessage());
}Un aviso sobre el tamaño de la jerarquía: no la hagas más profunda de lo que vas a usar. Tres niveles es un máximo razonable para una aplicación de este tamaño. Una jerarquía de seis niveles con cuarenta clases, la mitad de las cuales nadie captura nunca por separado, es puro ceremonial. La prueba práctica: si nunca vas a escribir un catch de un nivel intermedio, ese nivel sobra.
- La raíz común y lo que permite
Vale la pena detenerse en por qué la raíz común es la pieza más valiosa de la jerarquía. Estos son los tres usos que la justifican:
1. La frontera de errores del main (que montarás en 06-07):
try {
aplicacion.arrancar();
} catch (BiblioTechException e) {
// Fallo ESPERADO del dominio: mensaje limpio para el usuario
System.out.println("Operacion no completada: " + e.getMessage());
} catch (RuntimeException e) {
// Fallo INESPERADO: es un bug. Mensaje generico + registro completo
System.out.println("Error interno. Consulte el registro (incidencia " + id + ").");
logger.log(Level.SEVERE, "Fallo no controlado", e);
}Sin la raíz común no podrías distinguir "el usuario pidió algo imposible" de "tenemos un bug". Con ella, el primer caso produce un mensaje amable y el segundo una incidencia registrada.
2. Comportamiento compartido. La raíz puede aportar métodos a toda la familia:
public abstract class BiblioTechException extends RuntimeException {
/** Codigo estable para el log y para la documentacion de soporte. */
public String getCodigo() {
return getClass().getSimpleName().replace("Exception", "").toUpperCase();
}
/**
* ¿Es un fallo que el usuario puede corregir por si mismo?
* Las subclases lo redefinen cuando no sea asi.
*/
public boolean esCorregiblePorElUsuario() {
return true;
}
/** Mensaje apto para mostrar en pantalla, sin detalles internos. */
public String getMensajeParaUsuario() {
return getMessage();
}
}3. Un punto único de evolución. Si mañana quieres que todas las excepciones de BiblioTech lleven un identificador de operación para correlacionar logs, lo añades en la raíz y lo heredan las siete.
- Convenciones de nomenclatura
Cuatro reglas que sigue todo el ecosistema Java:
| Regla | Bien | Mal |
|---|---|---|
Sufijo Exception |
MaterialNoEncontradoException |
MaterialNoEncontrado, ErrorMaterial |
| Describe el problema, no la solución | LimitePrestamosExcedidoException |
DebeDevolverAlgoException |
| Específica, no genérica | ReferenciaDuplicadaException |
DatosInvalidosException |
| Sin el prefijo del proyecto en las hojas | MaterialNoEncontradoException |
BiblioTechMaterialNoEncontradoException |
Sobre la última: el prefijo está bien en la raíz (BiblioTechException) porque ahí identifica la familia. Repetirlo en cada hoja es ruido, ya que el paquete cumple esa función.
Y una nota sobre Error como sufijo: no lo uses. En Java, Error significa "fallo irrecuperable de la JVM". Llamar MaterialError a algo que extiende RuntimeException es engañoso para cualquiera que conozca la jerarquía.
- Convenciones de mensaje
Un buen mensaje de excepción responde tres preguntas:
- Qué pasó, en lenguaje del dominio.
- Con qué dato concreto.
- Qué se esperaba, cuando no sea obvio.
| Mensaje | Valoración |
|---|---|
"Error" |
Inútil |
"Referencia invalida" |
Falta el dato |
"Referencia invalida: LIB-99" |
Falta lo esperado |
"La referencia debe tener el formato LIB-NNNN, y era: 'LIB-99'" |
Completo |
Aplicado a la jerarquía:
// Qué + dato + esperado
"El empleado EMP-001 (Marta Ruiz) tiene 3 prestamos activos y el limite es 3"
// Qué + dato + contexto útil para actuar
"El material LIB-0001 ('Java Efectivo') esta prestado a EMP-002 desde el dia 12"
// Qué + los dos datos que colisionan
"Ya existe un material con la referencia LIB-0001: 'Java Efectivo'"Tres cosas que no deben ir en un mensaje de excepción:
- Datos sensibles: contraseñas, tokens, números de tarjeta, datos personales que no sean estrictamente necesarios. El mensaje acabará en un log, y posiblemente en un correo automático o en la pantalla de alguien. En 06-07 hay una advertencia formal sobre esto.
- La solución sugerida, cuando depende del contexto.
"El material no esta disponible. Devuelvalo primero."presupone que quien lee es quien lo tiene prestado, y no tiene por qué serlo. La sugerencia es cosa de la capa de presentación, que sí conoce al usuario. - Puntuación final ni saltos de línea. El mensaje se concatena en logs y en stack traces; un salto de línea rompe el formato de una línea por evento del que dependen los sistemas de análisis.
- Serialización y
serialVersionUID
serialVersionUIDThrowable implementa java.io.Serializable, lo que significa que todas las excepciones son serializables: se pueden convertir en bytes y enviarse por red o guardarse en disco. Es lo que permite que una excepción lanzada en un servidor llegue a un cliente remoto.
De ahí salen dos consecuencias prácticas.
1. El aviso del compilador sobre serialVersionUID. Si compilas con -Xlint:serial, verás:
warning: [serial] serializable class MaterialNoEncontradoException has no definition of serialVersionUID
Es una advertencia menor pero se soluciona con una línea:
public class MaterialNoEncontradoException extends BiblioTechException {
private static final long serialVersionUID = 1L;
// ...
}Ese número identifica la versión de la clase a efectos de serialización. Si no lo declaras, Java lo calcula automáticamente a partir de la estructura de la clase, y cambia cada vez que modificas un campo o un método, lo que rompe la compatibilidad con bytes serializados anteriormente. Declarándolo explícitamente controlas tú cuándo cambia. La mecánica completa de la serialización es materia de 07-05.
2. Los campos propios deben ser serializables. Si tu excepción guarda un objeto de una clase que no implementa Serializable, la serialización fallará con NotSerializableException:
// PROBLEMATICO si la excepcion viaja por red y Empleado no es Serializable
private final Empleado empleado;
// SEGURO: String e int siempre son serializables
private final String idEmpleado;
private final int limite;Es un argumento más a favor de guardar identificadores en lugar de objetos completos, además del de la memoria que ya viste. En una aplicación de consola como BiblioTech esto no llega a manifestarse, pero es el tipo de decisión que evita un problema el día que la aplicación crezca.
- Cuándo NO crear una excepción propia
Crear excepciones tiene coste: más clases, más que documentar, más que aprender. La regla:
Si ya existe una excepción estándar cuya semántica es exactamente la tuya y no necesitas transportar datos, úsala.
Las estándar que cubren la mayoría de casos:
| Excepción estándar | Semántica | Ejemplo en BiblioTech |
|---|---|---|
IllegalArgumentException |
Argumento inválido, sin significado de negocio | dia < 1; formato de referencia incorrecto |
IllegalStateException |
El objeto no está en condiciones, sin significado de negocio | Iterador ya cerrado |
NullPointerException |
Argumento obligatorio nulo | Objects.requireNonNull en cualquier constructor |
UnsupportedOperationException |
Operación no soportada por esta implementación | add sobre un catálogo de solo lectura |
NoSuchElementException |
No hay más elementos, o no se encontró | Iteración sobre la cola de reservas vacía |
IndexOutOfBoundsException |
Índice fuera de rango | Posición inválida en un listado |
La prueba de decisión, en tres preguntas:
- ¿El fallo tiene un nombre en el lenguaje del negocio? Si Marta Ruiz diría "ese libro ya está prestado", merece una excepción propia. Si diría "el programa se ha equivocado", no.
- ¿Necesita transportar datos estructurados? Si quien captura va a querer el objeto o el número, propia. Si solo va a mostrar el mensaje, estándar.
- ¿Alguien va a capturarla de forma específica? Si en todo el sistema siempre acabará en un
catchgenérico, propia no aporta nada.
Aplicado a BiblioTech, esta es la mezcla resultante, que es lo normal en un proyecto sano:
// ESTANDAR: validacion tecnica sin significado de negocio
Objects.requireNonNull(referencia, "La referencia no puede ser nula");
if (dia < 1) {
throw new IllegalArgumentException("El dia debe ser 1 o posterior, y era: " + dia);
}
if (!referencia.matches("LIB-\\d{4}")) {
throw new IllegalArgumentException("Formato de referencia invalido: " + referencia);
}
// PROPIA: regla de negocio con nombre y con datos
if (!material.estaDisponible()) {
throw new MaterialNoDisponibleException(referencia, material.getTitulo(),
material.getDiaPrevistoDevolucion());
}Un error de principiante frecuente en el otro extremo: crear NombreNuloException, AnioInvalidoException, TituloVacioException... una excepción por campo. Eso es sustituir un vocabulario estándar y conocido por uno propio que nadie conoce, sin ganar nada. Las validaciones técnicas de argumentos usan excepciones estándar; las reglas de negocio usan las tuyas.
- Nota avanzada: excepciones sin stack trace
En 06-02 mediste que lo caro de una excepción no es lanzarla ni capturarla, sino construirla, porque el constructor de Throwable llama a fillInStackTrace() para capturar la pila completa.
Para el 99,9% de los casos eso es irrelevante: si algo falla una vez cada mil operaciones, el coste no se nota. Pero existe un caso legítimo en el que sí importa: una excepción usada como señal de control muy frecuente en un bucle caliente, donde el stack trace nunca se lee.
Throwable ofrece un constructor protected con dos parámetros extra:
protected Throwable(String message, Throwable cause,
boolean enableSuppression, // ¿admite excepciones suprimidas? (06-06)
boolean writableStackTrace) // ¿captura la pila?Con writableStackTrace = false, no se captura la pila y la excepción se construye casi gratis:
package com.nexussoftware.bibliotech.servicio;
/**
* Excepcion de senal, sin stack trace.
*
* ADVERTENCIA: usar SOLO cuando se cumplan las tres condiciones:
* 1. Se lanza en un bucle muy caliente (miles de veces por segundo).
* 2. Se captura inmediatamente en el mismo metodo o muy cerca.
* 3. El stack trace no aporta NADA, porque el punto de lanzamiento es unico.
*
* Si no se cumplen las tres, esta optimizacion solo consigue que un fallo
* real sea imposible de diagnosticar. Es la definicion de optimizacion prematura.
*/
public class BusquedaInterrumpidaException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final int posicion;
public BusquedaInterrumpidaException(int posicion) {
// mensaje, causa, supresion, stackTrace
super("Busqueda interrumpida en la posicion " + posicion, null, false, false);
this.posicion = posicion;
}
public int getPosicion() { return posicion; }
}Y una variante todavía más extrema, la excepción preconstruida como constante:
/** Una unica instancia reutilizada: cero asignaciones de memoria. */
private static final BusquedaInterrumpidaException SENAL =
new BusquedaInterrumpidaException(-1);
// ...
throw SENAL; // no construye nadaLos números de 06-02 respaldan la técnica: una excepción sin stack trace costaba 1,5 veces un if, frente a las 70 veces de una completa.
Y ahora la advertencia, que es más importante que la técnica:
Esto es una optimización de último recurso. Una excepción sin stack trace que llegue a producción por un camino imprevisto es imposible de diagnosticar: verás la clase y el mensaje, y ni una sola línea de dónde ocurrió. Si además la reutilizas como constante, el mensaje será siempre el mismo aunque el contexto sea distinto.
El propio JDK usa esta técnica en sitios muy concretos y controlados, y las librerías de programación reactiva la emplean para señales internas. En código de aplicación como BiblioTech, no la necesitas. Está aquí para que la reconozcas cuando la veas y para que entiendas de dónde sale el coste de una excepción.
- BiblioTech: la refactorización completa
La jerarquía entera, lista para usar. Primero la raíz:
package com.nexussoftware.bibliotech.dominio;
/**
* Raiz de todas las excepciones del dominio de BiblioTech.
*
* Es abstracta porque nadie debe lanzarla directamente: seria tan poco
* informativa como el 'false' mudo que este modulo esta eliminando.
*
* Es NO COMPROBADA (extiende RuntimeException) porque representa reglas de
* negocio que el llamador normalmente no puede resolver en el sitio, y porque
* obligar a declararlas contaminaria toda la aplicacion, incluidas las lambdas.
*/
public abstract class BiblioTechException extends RuntimeException {
private static final long serialVersionUID = 1L;
protected BiblioTechException(String mensaje) {
super(mensaje);
}
protected BiblioTechException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
/**
* Codigo estable del fallo, derivado del nombre de la clase.
* Sirve para el log y para la documentacion de soporte:
* MATERIALNOENCONTRADO, LIMITEPRESTAMOSEXCEDIDO, etc.
*/
public String getCodigo() {
return getClass().getSimpleName()
.replace("Exception", "")
.toUpperCase();
}
/**
* ¿Puede el usuario corregir esto por si mismo?
* Por defecto si; las subclases que representen fallos del sistema lo redefinen.
*/
public boolean esCorregiblePorElUsuario() {
return true;
}
}Las dos ramas intermedias:
package com.nexussoftware.bibliotech.dominio;
/** Fallos relacionados con el catalogo de materiales. */
public abstract class CatalogoException extends BiblioTechException {
private static final long serialVersionUID = 1L;
protected CatalogoException(String mensaje) { super(mensaje); }
protected CatalogoException(String mensaje, Throwable causa) { super(mensaje, causa); }
}package com.nexussoftware.bibliotech.dominio;
/** Fallos relacionados con prestamos y devoluciones. */
public abstract class PrestamoException extends BiblioTechException {
private static final long serialVersionUID = 1L;
protected PrestamoException(String mensaje) { super(mensaje); }
protected PrestamoException(String mensaje, Throwable causa) { super(mensaje, causa); }
}Y las cinco hojas concretas, cada una con sus datos:
package com.nexussoftware.bibliotech.dominio;
/** No existe ningun material con la referencia buscada. */
public class MaterialNoEncontradoException extends CatalogoException {
private static final long serialVersionUID = 1L;
private final String referenciaBuscada;
private final int tamanoCatalogo;
public MaterialNoEncontradoException(String referenciaBuscada, int tamanoCatalogo) {
super("No existe ningun material con la referencia '" + referenciaBuscada
+ "' (el catalogo tiene " + tamanoCatalogo + " materiales)");
this.referenciaBuscada = referenciaBuscada;
this.tamanoCatalogo = tamanoCatalogo;
}
public String getReferenciaBuscada() { return referenciaBuscada; }
public int getTamanoCatalogo() { return tamanoCatalogo; }
public boolean catalogoVacio() { return tamanoCatalogo == 0; }
}package com.nexussoftware.bibliotech.dominio;
/** Se intenta registrar un material con una referencia o un ISBN que ya existen. */
public class ReferenciaDuplicadaException extends CatalogoException {
private static final long serialVersionUID = 1L;
/** Tipo de identificador que colisiona. Un enum (04-07) evita cadenas magicas. */
public enum Tipo { REFERENCIA, ISBN }
private final Tipo tipo;
private final String valorDuplicado;
private final String tituloExistente;
public ReferenciaDuplicadaException(Tipo tipo, String valorDuplicado, String tituloExistente) {
super("Ya existe un material con " + (tipo == Tipo.ISBN ? "el ISBN " : "la referencia ")
+ valorDuplicado + ": '" + tituloExistente + "'");
this.tipo = tipo;
this.valorDuplicado = valorDuplicado;
this.tituloExistente = tituloExistente;
}
public Tipo getTipo() { return tipo; }
public String getValorDuplicado() { return valorDuplicado; }
public String getTituloExistente() { return tituloExistente; }
}package com.nexussoftware.bibliotech.dominio;
/** El material existe pero esta prestado a otro empleado. */
public class MaterialNoDisponibleException extends PrestamoException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final String titulo;
private final String idTitularActual;
private final int diaPrevistoDevolucion;
public MaterialNoDisponibleException(String referencia, String titulo,
String idTitularActual, int diaPrevistoDevolucion) {
super("El material " + referencia + " ('" + titulo + "') esta prestado a "
+ idTitularActual + " y se espera su devolucion el dia " + diaPrevistoDevolucion);
this.referencia = referencia;
this.titulo = titulo;
this.idTitularActual = idTitularActual;
this.diaPrevistoDevolucion = diaPrevistoDevolucion;
}
public String getReferencia() { return referencia; }
public String getTitulo() { return titulo; }
public String getIdTitularActual() { return idTitularActual; }
public int getDiaPrevistoDevolucion() { return diaPrevistoDevolucion; }
/** Dias que faltan para que el material vuelva a estar libre. */
public int diasDeEspera(int diaActual) {
return Math.max(0, diaPrevistoDevolucion - diaActual);
}
}package com.nexussoftware.bibliotech.dominio;
/** El empleado ha alcanzado el maximo de prestamos simultaneos. */
public class LimitePrestamosExcedidoException extends PrestamoException {
private static final long serialVersionUID = 1L;
private final String idEmpleado;
private final String nombreEmpleado;
private final int prestamosActuales;
private final int limite;
public LimitePrestamosExcedidoException(String idEmpleado, String nombreEmpleado,
int prestamosActuales, int limite) {
super("El empleado " + idEmpleado + " (" + nombreEmpleado + ") tiene "
+ prestamosActuales + " prestamos activos y el limite es " + limite);
this.idEmpleado = idEmpleado;
this.nombreEmpleado = nombreEmpleado;
this.prestamosActuales = prestamosActuales;
this.limite = limite;
}
public String getIdEmpleado() { return idEmpleado; }
public String getNombreEmpleado() { return nombreEmpleado; }
public int getPrestamosActuales() { return prestamosActuales; }
public int getLimite() { return limite; }
/** Cuantos materiales debe devolver para poder tomar otro prestado. */
public int devolucionesNecesarias() {
return Math.max(1, prestamosActuales - limite + 1);
}
}package com.nexussoftware.bibliotech.dominio;
/** Se intenta devolver un prestamo que ya se habia devuelto. */
public class PrestamoYaDevueltoException extends PrestamoException {
private static final long serialVersionUID = 1L;
private final String referenciaPrestamo;
private final int diaDevolucionOriginal;
public PrestamoYaDevueltoException(String referenciaPrestamo, int diaDevolucionOriginal) {
super("El prestamo " + referenciaPrestamo + " ya se devolvio el dia "
+ diaDevolucionOriginal);
this.referenciaPrestamo = referenciaPrestamo;
this.diaDevolucionOriginal = diaDevolucionOriginal;
}
public String getReferenciaPrestamo() { return referenciaPrestamo; }
public int getDiaDevolucionOriginal() { return diaDevolucionOriginal; }
/**
* Este fallo casi siempre indica un problema de secuencia en el codigo
* o un doble clic del usuario, no algo que el usuario pueda "corregir".
*/
@Override
public boolean esCorregiblePorElUsuario() {
return false;
}
}Y la única comprobada de la familia, que no cuelga de BiblioTechException precisamente porque su naturaleza es distinta:
package com.nexussoftware.bibliotech.servicio;
/**
* El catalogo persistido no se puede leer o escribir.
*
* Es COMPROBADA porque representa un fallo del entorno (disco, permisos,
* fichero ausente) del que la aplicacion SI tiene una alternativa razonable:
* arrancar con el catalogo vacio. Obligar a decidirlo explicitamente es
* justamente lo que se quiere.
*
* No extiende BiblioTechException porque esa raiz es no comprobada y
* representa reglas de negocio; esto es un fallo de infraestructura.
* El modulo 7 desarrollara la parte de E/S.
*/
public class CatalogoNoAccesibleException extends Exception {
private static final long serialVersionUID = 1L;
private final String ruta;
public CatalogoNoAccesibleException(String ruta, Throwable causa) {
super("No se pudo acceder al catalogo en '" + ruta + "'", causa);
this.ruta = ruta;
}
public String getRuta() { return ruta; }
}Ahora el Catalogo refactorizado:
package com.nexussoftware.bibliotech.servicio;
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.dominio.Libro;
import com.nexussoftware.bibliotech.dominio.Material;
import com.nexussoftware.bibliotech.dominio.MaterialNoEncontradoException;
import com.nexussoftware.bibliotech.dominio.ReferenciaDuplicadaException;
/** Catalogo de BiblioTech con excepciones del dominio. */
public class Catalogo {
private final List<Material> materiales = new ArrayList<>();
private final Map<String, Material> indicePorReferencia = new HashMap<>();
private final Map<String, String> titulosPorIsbn = new HashMap<>();
/**
* Registra un material.
*
* @throws NullPointerException si el material es nulo (fallo tecnico)
* @throws ReferenciaDuplicadaException si la referencia o el ISBN ya existen
*/
public void registrar(Material material) {
// Validacion tecnica: excepcion ESTANDAR
Objects.requireNonNull(material, "El material a registrar no puede ser nulo");
String referencia = material.getReferencia();
// Regla de negocio: excepcion PROPIA, con datos
Material existente = indicePorReferencia.get(referencia);
if (existente != null) {
throw new ReferenciaDuplicadaException(
ReferenciaDuplicadaException.Tipo.REFERENCIA,
referencia, existente.getTitulo());
}
if (material instanceof Libro libro) {
String tituloConEseIsbn = titulosPorIsbn.get(libro.getIsbn());
if (tituloConEseIsbn != null) {
throw new ReferenciaDuplicadaException(
ReferenciaDuplicadaException.Tipo.ISBN,
libro.getIsbn(), tituloConEseIsbn);
}
titulosPorIsbn.put(libro.getIsbn(), libro.getTitulo());
}
materiales.add(material);
indicePorReferencia.put(referencia, material);
}
/**
* Obtiene un material EXIGIENDO que exista. Nunca devuelve null.
*
* @throws MaterialNoEncontradoException si no existe
*/
public Material obtenerPorReferencia(String referencia) {
Objects.requireNonNull(referencia, "La referencia no puede ser nula");
Material encontrado = indicePorReferencia.get(referencia);
if (encontrado == null) {
throw new MaterialNoEncontradoException(referencia, materiales.size());
}
return encontrado;
}
/** Busca ADMITIENDO que no exista. Nunca devuelve null: devuelve Optional. */
public Optional<Material> buscarPorReferencia(String referencia) {
if (referencia == null) { return Optional.empty(); }
return Optional.ofNullable(indicePorReferencia.get(referencia));
}
public List<Material> buscarPorPrefijo(String prefijo) {
List<Material> resultado = new ArrayList<>();
for (Material m : materiales) {
if (m.getReferencia().startsWith(prefijo)) {
resultado.add(m);
}
}
return resultado;
}
public boolean existe(String referencia) {
return referencia != null && indicePorReferencia.containsKey(referencia);
}
public int tamano() { return materiales.size(); }
public List<Material> listar() { return List.copyOf(materiales); }
}Y la demostración que muestra lo que hemos ganado:
package com.nexussoftware.bibliotech.presentacion;
import com.nexussoftware.bibliotech.dominio.*;
import com.nexussoftware.bibliotech.servicio.Catalogo;
import com.nexussoftware.bibliotech.servicio.GestorPrestamos;
/**
* Demuestra la diferencia entre capturar por TIPO con datos estructurados
* y capturar tipos estandar leyendo el mensaje.
*/
public class DemoExcepcionesDominio {
public static void main(String[] args) {
Catalogo catalogo = new Catalogo();
catalogo.registrar(new Libro("LIB-0001", "Java Efectivo", "Bloch", 2018, "978-0000000001"));
catalogo.registrar(new Libro("LIB-0002", "Patrones de Diseno", "GoF", 1994, "978-0000000002"));
catalogo.registrar(new Libro("LIB-0003", "Refactorizacion", "Fowler", 1999, "978-0000000003"));
GestorPrestamos gestor = new GestorPrestamos(catalogo);
gestor.darDeAlta(new Empleado("Marta Ruiz", "EMP-001"));
gestor.darDeAlta(new Empleado("Diego Alonso", "EMP-002"));
// Marta agota su cupo
gestor.prestar("LIB-0001", "EMP-001", 10);
gestor.prestar("LIB-0002", "EMP-001", 10);
gestor.prestar("LIB-0003", "EMP-001", 10);
System.out.println("=== Fallos, cada uno con SU manejador ===\n");
operar(gestor, catalogo, "LIB-9999", "EMP-001", 12); // no encontrado
operar(gestor, catalogo, "LIB-0001", "EMP-002", 12); // no disponible
operar(gestor, catalogo, "LIB-0001", "EMP-001", 12); // no disponible (y limite)
System.out.println("\n=== Duplicados ===\n");
registrar(catalogo, new Libro("LIB-0001", "Otro titulo", "X", 2020, "978-0000000009"));
registrar(catalogo, new Libro("LIB-0004", "Copia", "Bloch", 2018, "978-0000000001"));
System.out.println("\n=== Captura por la RAIZ: todo el dominio de una vez ===\n");
try {
catalogo.obtenerPorReferencia("LIB-7777");
} catch (BiblioTechException e) {
System.out.println("Codigo: " + e.getCodigo());
System.out.println("Mensaje: " + e.getMessage());
System.out.println("¿Corregible por el usuario? " + e.esCorregiblePorElUsuario());
}
}
/** Cada tipo de fallo se maneja de forma DISTINTA usando sus datos. */
private static void operar(GestorPrestamos gestor, Catalogo catalogo,
String referencia, String idEmpleado, int dia) {
System.out.println("--- prestar(" + referencia + ", " + idEmpleado + ") ---");
try {
gestor.prestar(referencia, idEmpleado, dia);
System.out.println(" OK");
} catch (MaterialNoEncontradoException e) {
// Usa los DATOS de la excepcion para ayudar al usuario
System.out.println(" No se encontro '" + e.getReferenciaBuscada() + "'.");
if (e.catalogoVacio()) {
System.out.println(" El catalogo esta vacio: cargue primero los materiales.");
} else {
var parecidos = catalogo.buscarPorPrefijo(
e.getReferenciaBuscada().substring(0, 4));
System.out.println(" Materiales con prefijo similar: " + parecidos.size());
}
} catch (MaterialNoDisponibleException e) {
System.out.println(" '" + e.getTitulo() + "' lo tiene " + e.getIdTitularActual() + ".");
System.out.println(" Vuelve en " + e.diasDeEspera(dia) + " dias.");
System.out.println(" -> Se anade a la cola de reservas de " + e.getReferencia());
} catch (LimitePrestamosExcedidoException e) {
System.out.println(" " + e.getNombreEmpleado() + " tiene "
+ e.getPrestamosActuales() + "/" + e.getLimite() + " prestamos.");
System.out.println(" Debe devolver " + e.devolucionesNecesarias()
+ " material(es) antes de tomar otro.");
}
}
private static void registrar(Catalogo catalogo, Material material) {
try {
catalogo.registrar(material);
System.out.println("Registrado: " + material.getReferencia());
} catch (ReferenciaDuplicadaException e) {
// El TIPO del duplicado permite un mensaje preciso, sin parsear texto
String queColisiona = (e.getTipo() == ReferenciaDuplicadaException.Tipo.ISBN)
? "El ISBN" : "La referencia";
System.out.println(queColisiona + " " + e.getValorDuplicado()
+ " ya la usa '" + e.getTituloExistente() + "'. Alta rechazada.");
}
}
}Salida:
=== Fallos, cada uno con SU manejador === --- prestar(LIB-9999, EMP-001) --- No se encontro 'LIB-9999'. Materiales con prefijo similar: 0 --- prestar(LIB-0001, EMP-002) --- 'Java Efectivo' lo tiene EMP-001. Vuelve en 13 dias. -> Se anade a la cola de reservas de LIB-0001 --- prestar(LIB-0001, EMP-001) --- 'Java Efectivo' lo tiene EMP-001. Vuelve en 13 dias. -> Se anade a la cola de reservas de LIB-0001 === Duplicados === La referencia LIB-0001 ya la usa 'Java Efectivo'. Alta rechazada. El ISBN 978-0000000001 ya la usa 'Java Efectivo'. Alta rechazada. === Captura por la RAIZ: todo el dominio de una vez === Codigo: MATERIALNOENCONTRADO Mensaje: No existe ningun material con la referencia 'LIB-7777' (el catalogo tiene 3 materiales) ¿Corregible por el usuario? true
Compara esta salida con la de 06-03. Allí cada fallo producía una línea de texto que solo servía para leerla. Aquí, cada fallo desencadena una reacción distinta y específica: buscar materiales parecidos, ofrecer una reserva con los días de espera calculados, o decirle al empleado cuántos materiales debe devolver. Toda esa lógica es posible porque los datos vienen en campos, no en una cadena.
Errores Comunes y Consejos
Crear una excepción por cada campo validado. NombreNuloException, AnioInvalidoException, TituloVacioException... Sustituyes un vocabulario estándar y conocido por uno propio que nadie conoce. Las validaciones técnicas usan IllegalArgumentException y NullPointerException; las reglas de negocio, las tuyas.
Hacer comprobadas las excepciones de negocio. Contaminan las firmas de toda la aplicación, impiden lanzarlas desde lambdas y empujan al catch vacío. Reserva las comprobadas para los fallos del entorno donde el llamador vaya a hacer algo distinto de propagar.
Olvidar el constructor (String, Throwable). Sin él, quien necesite envolver un fallo tendrá que descartar la causa o usar otro tipo. Es la vía directa a los stack traces mutilados de 06-03.
Campos no final. Una excepción describe un hecho ocurrido: nadie debería poder modificarla después de lanzarla.
Guardar objetos grandes en la excepción. Mantiene vivas referencias que el recolector no puede liberar, y rompe la serialización si esos objetos no son Serializable. Guarda identificadores.
Parsear el mensaje para tomar decisiones. if (e.getMessage().contains("limite")) se rompe en silencio en cuanto alguien mejore el texto o lo traduzca. Si necesitas ese dato, ponlo en un campo.
Una jerarquía más profunda de lo que vas a usar. Si nunca vas a escribir un catch de un nivel intermedio, ese nivel sobra. Tres niveles es un máximo razonable.
Raíz no abstracta. Si BiblioTechException es instanciable, alguien acabará lanzándola directamente, y volverás al punto de partida: un fallo sin nombre concreto.
Nombres sin el sufijo Exception. Rompe la convención universal de Java y confunde a cualquiera que lea el código. Y Error como sufijo es peor: sugiere un fallo irrecuperable de la JVM.
Consejo: construye el mensaje en el super(...) a partir de los campos. Así el texto y los datos no pueden contradecirse, y todo el formato del mensaje vive en un solo sitio.
Consejo: añade métodos de conveniencia útiles. diasDeEspera(diaActual), devolucionesNecesarias(), catalogoVacio(). La excepción sabe cosas sobre el fallo; deja que las calcule ella en vez de repetir la aritmética en cada catch.
Consejo: declara serialVersionUID = 1L. Una línea que silencia el aviso del compilador y te da control sobre la compatibilidad de serialización. La mecánica completa, en 07-05.
Consejo: documenta cada excepción con Javadoc y @throws en los métodos que la lanzan. Es la única forma de que quien use tu API sepa qué esperar sin leer la implementación.
Ejercicios
Ejercicio 1: excepción con datos y lógica
Crea MultaExcesivaException, que se lanzará cuando un empleado acumule una multa por encima de un umbral y no pueda tomar más préstamos hasta saldarla.
Requisitos:
- Extiende
PrestamoException. - Campos
final:idEmpleado,nombreEmpleado,importeAcumulado(double),umbralPermitido(double),materialesConMulta(unaList<String>de referencias). - Constructores: el completo y uno que además acepte
Throwable causa. - El mensaje se construye a partir de los campos e incluye el importe con dos decimales.
- Accesores para todos los campos. La lista se devuelve como copia inmutable.
- Métodos de conveniencia:
double getExceso(): cuánto sobrepasa el umbral.boolean superaLaMultaMaxima(): siimporteAcumulado >= MULTA_MAXIMA(20.0).String getResumen(): una línea por material con multa.
- Redefine
esCorregiblePorElUsuario()devolviendotrue(el empleado puede pagar).
Escribe un main que la lance con tres materiales en multa y la capture, demostrando el uso de todos sus métodos.
Ejercicio 2: rediseño de una jerarquía mal hecha
Un compañero ha escrito esta jerarquía para el módulo de reservas de BiblioTech. Tiene siete problemas de diseño. Identifícalos, explica el daño de cada uno y reescribe la jerarquía correctamente.
public class ErrorReserva extends Throwable {
public ErrorReserva() { }
}
public class ReservaNoValida extends ErrorReserva {
public String mensaje;
public ReservaNoValida(String m) { this.mensaje = m; }
}
public class ErrorFechaReserva extends Exception {
public ErrorFechaReserva(String m) { super(m); }
}
public class ReservaDuplicadaError extends ReservaNoValida {
public ReservaDuplicadaError() { super("Error"); }
}
// Uso tipico en el codigo:
// try {
// colaReservas.reservar(referencia, idEmpleado, dia);
// } catch (Throwable t) {
// if (t.getMessage() != null && t.getMessage().startsWith("Error")) {
// System.out.println("Fallo de reserva");
// }
// }Ejercicio 3: ColaReservas con su propia familia de excepciones
Amplía el servicio ColaReservas de BiblioTech con una jerarquía propia y datos estructurados.
Requisitos:
- Crea
ReservaException extends BiblioTechException(abstracta) y tres hojas:ReservaDuplicadaException: el mismo empleado ya tiene una reserva de ese material. Campos:referencia,idEmpleado,posicionActual.ColaReservasLlenaException: la cola de ese material ha alcanzado el máximo (MAX_RESERVAS_POR_MATERIAL = 5). Campos:referencia,maximo,diaEstimadoDisponibilidad.ReservaNoEncontradaException: se intenta cancelar una reserva inexistente. Campos:referencia,idEmpleado.
ColaReservasmantiene unMap<String, Deque<Reserva>>(una cola FIFO por material, como en 05-07) y ofrece:int reservar(String referencia, String idEmpleado, int dia): devuelve la posición en la cola (1 = el siguiente).void cancelar(String referencia, String idEmpleado).Optional<Reserva> atenderSiguiente(String referencia): saca al primero de la cola.int posicionDe(String referencia, String idEmpleado):-1si no está.
- Valida los argumentos con excepciones estándar y las reglas de negocio con las propias.
- El
maindebe demostrar los tres fallos, una cola completa con los tres empleados del proyecto y la atención de la cola en orden FIFO. Añade un manejador que capture porReservaException(la rama) y muestregetCodigo().
Soluciones
Solución 1
package com.nexussoftware.bibliotech.dominio;
import java.util.List;
/**
* El empleado ha acumulado una multa que le impide tomar mas prestamos
* hasta saldarla.
*
* Transporta el desglose completo para que la capa de presentacion pueda
* mostrar un recibo, y para que la de servicio pueda decidir si aplica
* una excepcion a la regla.
*/
public class MultaExcesivaException extends PrestamoException {
private static final long serialVersionUID = 1L;
public static final double MULTA_MAXIMA = 20.0;
private final String idEmpleado;
private final String nombreEmpleado;
private final double importeAcumulado;
private final double umbralPermitido;
private final List<String> materialesConMulta;
public MultaExcesivaException(String idEmpleado, String nombreEmpleado,
double importeAcumulado, double umbralPermitido,
List<String> materialesConMulta) {
super(construirMensaje(idEmpleado, nombreEmpleado, importeAcumulado,
umbralPermitido, materialesConMulta));
this.idEmpleado = idEmpleado;
this.nombreEmpleado = nombreEmpleado;
this.importeAcumulado = importeAcumulado;
this.umbralPermitido = umbralPermitido;
// Copia defensiva: la excepcion debe ser inmutable aunque la lista
// original cambie despues (03-07)
this.materialesConMulta = List.copyOf(materialesConMulta);
}
public MultaExcesivaException(String idEmpleado, String nombreEmpleado,
double importeAcumulado, double umbralPermitido,
List<String> materialesConMulta, Throwable causa) {
super(construirMensaje(idEmpleado, nombreEmpleado, importeAcumulado,
umbralPermitido, materialesConMulta), causa);
this.idEmpleado = idEmpleado;
this.nombreEmpleado = nombreEmpleado;
this.importeAcumulado = importeAcumulado;
this.umbralPermitido = umbralPermitido;
this.materialesConMulta = List.copyOf(materialesConMulta);
}
/**
* El mensaje se construye en un metodo estatico porque hay que llamarlo
* en el super(...), antes de que los campos existan.
*/
private static String construirMensaje(String idEmpleado, String nombreEmpleado,
double importe, double umbral,
List<String> materiales) {
return String.format(
"El empleado %s (%s) acumula %.2f EUR de multa, por encima del umbral de %.2f EUR, "
+ "en %d material(es)",
idEmpleado, nombreEmpleado, importe, umbral, materiales.size());
}
public String getIdEmpleado() { return idEmpleado; }
public String getNombreEmpleado() { return nombreEmpleado; }
public double getImporteAcumulado(){ return importeAcumulado; }
public double getUmbralPermitido() { return umbralPermitido; }
/** Ya es inmutable por List.copyOf, pero se devuelve explicitamente asi. */
public List<String> getMaterialesConMulta() {
return materialesConMulta;
}
// ---------- Metodos de conveniencia ----------
/** Cuanto hay que pagar como minimo para volver a estar por debajo del umbral. */
public double getExceso() {
return Math.max(0.0, importeAcumulado - umbralPermitido);
}
/** ¿Se ha alcanzado el tope absoluto de multa del sistema? */
public boolean superaLaMultaMaxima() {
return importeAcumulado >= MULTA_MAXIMA;
}
/** Desglose legible, una linea por material. */
public String getResumen() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("Multa de %s: %.2f EUR (umbral %.2f, exceso %.2f)%n",
nombreEmpleado, importeAcumulado, umbralPermitido, getExceso()));
for (String referencia : materialesConMulta) {
sb.append(" - ").append(referencia).append('\n');
}
if (superaLaMultaMaxima()) {
sb.append(String.format(" ATENCION: se ha alcanzado la multa maxima (%.2f EUR)%n",
MULTA_MAXIMA));
}
return sb.toString();
}
/** El empleado puede resolverlo pagando: es corregible por el. */
@Override
public boolean esCorregiblePorElUsuario() {
return true;
}
// ---------- Demostracion ----------
public static void main(String[] args) {
try {
throw new MultaExcesivaException(
"EMP-001", "Marta Ruiz",
23.75, 10.0,
List.of("LIB-0001", "LIB-0002", "REV-0007"));
} catch (MultaExcesivaException e) {
System.out.println("=== Captura especifica ===");
System.out.println("Codigo : " + e.getCodigo());
System.out.println("Mensaje : " + e.getMessage());
System.out.println("Empleado : " + e.getIdEmpleado()
+ " (" + e.getNombreEmpleado() + ")");
System.out.printf ("Importe acumulado : %.2f EUR%n", e.getImporteAcumulado());
System.out.printf ("Umbral permitido : %.2f EUR%n", e.getUmbralPermitido());
System.out.printf ("Exceso a pagar : %.2f EUR%n", e.getExceso());
System.out.println("Supera el maximo : " + e.superaLaMultaMaxima());
System.out.println("Materiales : " + e.getMaterialesConMulta());
System.out.println("Corregible por el usuario: " + e.esCorregiblePorElUsuario());
System.out.println("\n=== Resumen para el recibo ===");
System.out.print(e.getResumen());
// Demostracion de que la lista es INMUTABLE
System.out.println("=== Inmutabilidad ===");
try {
e.getMaterialesConMulta().add("LIB-9999");
} catch (UnsupportedOperationException uoe) {
System.out.println("La lista de materiales no se puede modificar. Correcto.");
}
}
// Captura por la rama y por la raiz
System.out.println("\n=== Captura por la rama PrestamoException ===");
try {
throw new MultaExcesivaException("EMP-002", "Diego Alonso",
12.5, 10.0, List.of("LIB-0003"));
} catch (PrestamoException e) {
System.out.println("Fallo de prestamo [" + e.getCodigo() + "]: " + e.getMessage());
}
}
}Salida:
=== Captura especifica === Codigo : MULTAEXCESIVA Mensaje : El empleado EMP-001 (Marta Ruiz) acumula 23,75 EUR de multa, por encima del umbral de 10,00 EUR, en 3 material(es) Empleado : EMP-001 (Marta Ruiz) Importe acumulado : 23,75 EUR Umbral permitido : 10,00 EUR Exceso a pagar : 13,75 EUR Supera el maximo : true Materiales : [LIB-0001, LIB-0002, REV-0007] Corregible por el usuario: true === Resumen para el recibo === Multa de Marta Ruiz: 23,75 EUR (umbral 10,00, exceso 13,75) - LIB-0001 - LIB-0002 - REV-0007 ATENCION: se ha alcanzado la multa maxima (20,00 EUR) === Inmutabilidad === La lista de materiales no se puede modificar. Correcto. === Captura por la rama PrestamoException === Fallo de prestamo [MULTAEXCESIVA]: El empleado EMP-002 (Diego Alonso) acumula 12,50 EUR de multa, por encima del umbral de 10,00 EUR, en 1 material(es)
Solución 2
Los siete problemas:
| # | Problema | Daño |
|---|---|---|
| 1 | ErrorReserva extends Throwable |
No es ni Error ni Exception: se escapa de todo catch (Exception e) y de todo catch (RuntimeException e) sin ser un fallo de la JVM. Además, al ser comprobada, obliga a throws Throwable por toda la aplicación |
| 2 | Nombres sin sufijo Exception |
ErrorReserva, ReservaNoValida, ReservaDuplicadaError rompen la convención. Y Error sugiere un fallo irrecuperable de la JVM |
| 3 | public String mensaje en lugar de usar super(m) |
Campo público, mutable y duplicado: getMessage() devolverá null mientras el texto vive en otro sitio. Los stack traces saldrán sin mensaje |
| 4 | ErrorFechaReserva extends Exception, fuera de la jerarquía |
Rompe la familia: no se puede capturar con la raíz común. Y es comprobada sin motivo |
| 5 | ReservaDuplicadaError con mensaje fijo "Error" |
No dice qué material, ni qué empleado, ni qué posición. Cero valor diagnóstico |
| 6 | Ninguna clase transporta datos | Todo el estado del fallo se pierde. Quien captura solo puede leer texto |
| 7 | El uso: catch (Throwable t) + getMessage().startsWith("Error") |
Captura OutOfMemoryError, y la lógica depende de un prefijo de texto que se rompe en cuanto alguien mejore el mensaje. Además, getMessage() puede ser null |
Versión corregida:
package com.nexussoftware.bibliotech.dominio;
/** Raiz de los fallos del subsistema de reservas. */
public abstract class ReservaException extends BiblioTechException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final String idEmpleado;
protected ReservaException(String mensaje, String referencia, String idEmpleado) {
super(mensaje);
this.referencia = referencia;
this.idEmpleado = idEmpleado;
}
protected ReservaException(String mensaje, String referencia, String idEmpleado,
Throwable causa) {
super(mensaje, causa);
this.referencia = referencia;
this.idEmpleado = idEmpleado;
}
/** Datos comunes a toda la familia: se declaran una sola vez, en la raiz. */
public String getReferencia() { return referencia; }
public String getIdEmpleado() { return idEmpleado; }
}package com.nexussoftware.bibliotech.dominio;
/** El empleado ya tiene una reserva activa de ese material. */
public class ReservaDuplicadaException extends ReservaException {
private static final long serialVersionUID = 1L;
private final int posicionActual;
public ReservaDuplicadaException(String referencia, String idEmpleado, int posicionActual) {
super("El empleado " + idEmpleado + " ya tiene una reserva de " + referencia
+ " en la posicion " + posicionActual + " de la cola",
referencia, idEmpleado);
this.posicionActual = posicionActual;
}
public int getPosicionActual() { return posicionActual; }
}package com.nexussoftware.bibliotech.dominio;
/** El dia de la reserva no es valido para ese material. */
public class DiaReservaInvalidoException extends ReservaException {
private static final long serialVersionUID = 1L;
private final int diaSolicitado;
private final int diaMinimo;
public DiaReservaInvalidoException(String referencia, String idEmpleado,
int diaSolicitado, int diaMinimo) {
super("La reserva de " + referencia + " se solicito para el dia " + diaSolicitado
+ ", pero el material no estara disponible hasta el dia " + diaMinimo,
referencia, idEmpleado);
this.diaSolicitado = diaSolicitado;
this.diaMinimo = diaMinimo;
}
public int getDiaSolicitado() { return diaSolicitado; }
public int getDiaMinimo() { return diaMinimo; }
public int getDiasDeEspera() { return Math.max(0, diaMinimo - diaSolicitado); }
}Y el uso corregido:
try {
int posicion = colaReservas.reservar(referencia, idEmpleado, dia);
System.out.println("Reservado. Posicion en la cola: " + posicion);
} catch (ReservaDuplicadaException e) {
// Se usan los DATOS, no el texto
System.out.println("Ya tenia una reserva de " + e.getReferencia()
+ ", en la posicion " + e.getPosicionActual() + ". No se ha duplicado.");
} catch (DiaReservaInvalidoException e) {
System.out.println("Ese material no estara libre hasta dentro de "
+ e.getDiasDeEspera() + " dias.");
System.out.println("¿Desea reservarlo para el dia " + e.getDiaMinimo() + "? (s/n)");
} catch (ReservaException e) {
// Red de seguridad de la RAMA, no de Throwable
System.out.println("No se pudo completar la reserva [" + e.getCodigo() + "]: "
+ e.getMessage());
}Los cambios, en resumen: raíz abstracta dentro de BiblioTechException, sufijo Exception en todos los nombres, mensajes construidos con super(...) y con los datos concretos, campos comunes declarados en la raíz, campos específicos en cada hoja, todo no comprobado por ser reglas de negocio, y capturas por tipo en lugar de por prefijo del mensaje.
Solución 3
package com.nexussoftware.bibliotech.dominio;
/** Raiz de los fallos de la cola de reservas. */
public abstract class ReservaException extends BiblioTechException {
private static final long serialVersionUID = 1L;
protected ReservaException(String mensaje) { super(mensaje); }
}package com.nexussoftware.bibliotech.dominio;
public class ReservaDuplicadaException extends ReservaException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final String idEmpleado;
private final int posicionActual;
public ReservaDuplicadaException(String referencia, String idEmpleado, int posicionActual) {
super("El empleado " + idEmpleado + " ya tiene reservado " + referencia
+ " (posicion " + posicionActual + " de la cola)");
this.referencia = referencia;
this.idEmpleado = idEmpleado;
this.posicionActual = posicionActual;
}
public String getReferencia() { return referencia; }
public String getIdEmpleado() { return idEmpleado; }
public int getPosicionActual() { return posicionActual; }
}package com.nexussoftware.bibliotech.dominio;
public class ColaReservasLlenaException extends ReservaException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final int maximo;
private final int diaEstimadoDisponibilidad;
public ColaReservasLlenaException(String referencia, int maximo,
int diaEstimadoDisponibilidad) {
super("La cola de reservas de " + referencia + " esta llena (" + maximo
+ " reservas). Disponibilidad estimada: dia " + diaEstimadoDisponibilidad);
this.referencia = referencia;
this.maximo = maximo;
this.diaEstimadoDisponibilidad = diaEstimadoDisponibilidad;
}
public String getReferencia() { return referencia; }
public int getMaximo() { return maximo; }
public int getDiaEstimadoDisponibilidad() { return diaEstimadoDisponibilidad; }
@Override
public boolean esCorregiblePorElUsuario() {
return false; // el usuario no puede hacer nada: la cola esta llena
}
}package com.nexussoftware.bibliotech.dominio;
public class ReservaNoEncontradaException extends ReservaException {
private static final long serialVersionUID = 1L;
private final String referencia;
private final String idEmpleado;
public ReservaNoEncontradaException(String referencia, String idEmpleado) {
super("El empleado " + idEmpleado + " no tiene ninguna reserva de " + referencia);
this.referencia = referencia;
this.idEmpleado = idEmpleado;
}
public String getReferencia() { return referencia; }
public String getIdEmpleado() { return idEmpleado; }
}package com.nexussoftware.bibliotech.servicio;
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.dominio.ColaReservasLlenaException;
import com.nexussoftware.bibliotech.dominio.ReservaDuplicadaException;
import com.nexussoftware.bibliotech.dominio.ReservaException;
import com.nexussoftware.bibliotech.dominio.ReservaNoEncontradaException;
/**
* Cola FIFO de reservas por material (retoma el ArrayDeque de 05-07).
*
* Politica de errores:
* - Argumentos invalidos -> excepciones ESTANDAR (NPE, IllegalArgument).
* - Reglas de negocio -> excepciones PROPIAS con datos.
*/
public class ColaReservas {
public static final int MAX_RESERVAS_POR_MATERIAL = 5;
private static final int DIAS_ESTIMADOS_POR_TURNO = 15;
/** Reserva de un empleado sobre un material (record, 04-07). */
public record Reserva(String referencia, String idEmpleado, int dia) { }
private final Map<String, Deque<Reserva>> colasPorMaterial = new HashMap<>();
/**
* Anade una reserva al final de la cola del material.
*
* @return posicion en la cola, siendo 1 el siguiente en ser atendido
* @throws NullPointerException si algun argumento es nulo
* @throws IllegalArgumentException si el dia es menor que 1
* @throws ReservaDuplicadaException si ese empleado ya reservo ese material
* @throws ColaReservasLlenaException si la cola alcanzo el maximo
*/
public int reservar(String referencia, String idEmpleado, int dia) {
// Validacion tecnica: excepciones estandar
Objects.requireNonNull(referencia, "La referencia no puede ser nula");
Objects.requireNonNull(idEmpleado, "El identificador del empleado no puede ser nulo");
if (dia < 1) {
throw new IllegalArgumentException("El dia debe ser 1 o posterior, y era: " + dia);
}
Deque<Reserva> cola = colasPorMaterial.computeIfAbsent(referencia, r -> new ArrayDeque<>());
// Regla de negocio 1: no duplicar
int yaEsta = posicionEn(cola, idEmpleado);
if (yaEsta > 0) {
throw new ReservaDuplicadaException(referencia, idEmpleado, yaEsta);
}
// Regla de negocio 2: cola llena
if (cola.size() >= MAX_RESERVAS_POR_MATERIAL) {
int diaEstimado = dia + cola.size() * DIAS_ESTIMADOS_POR_TURNO;
throw new ColaReservasLlenaException(referencia, MAX_RESERVAS_POR_MATERIAL, diaEstimado);
}
cola.addLast(new Reserva(referencia, idEmpleado, dia));
return cola.size();
}
/**
* Cancela la reserva de un empleado sobre un material.
*
* @throws ReservaNoEncontradaException si no habia tal reserva
*/
public void cancelar(String referencia, String idEmpleado) {
Objects.requireNonNull(referencia, "La referencia no puede ser nula");
Objects.requireNonNull(idEmpleado, "El identificador del empleado no puede ser nulo");
Deque<Reserva> cola = colasPorMaterial.get(referencia);
if (cola == null) {
throw new ReservaNoEncontradaException(referencia, idEmpleado);
}
// removeIf sobre un Deque: forma segura de eliminar mientras se itera (05-04)
boolean eliminada = cola.removeIf(r -> r.idEmpleado().equals(idEmpleado));
if (!eliminada) {
throw new ReservaNoEncontradaException(referencia, idEmpleado);
}
if (cola.isEmpty()) {
colasPorMaterial.remove(referencia); // no dejar colas vacias en el mapa
}
}
/**
* Saca al primero de la cola. Devuelve Optional vacio si no hay nadie
* esperando: aqui la ausencia es un caso NORMAL, no un fallo.
*/
public Optional<Reserva> atenderSiguiente(String referencia) {
Deque<Reserva> cola = colasPorMaterial.get(referencia);
if (cola == null || cola.isEmpty()) {
return Optional.empty();
}
Reserva primera = cola.pollFirst();
if (cola.isEmpty()) {
colasPorMaterial.remove(referencia);
}
return Optional.of(primera);
}
/** Posicion del empleado en la cola de ese material, o -1 si no esta. */
public int posicionDe(String referencia, String idEmpleado) {
Deque<Reserva> cola = colasPorMaterial.get(referencia);
if (cola == null) { return -1; }
int p = posicionEn(cola, idEmpleado);
return (p > 0) ? p : -1;
}
/** Devuelve la posicion 1..n, o 0 si no esta. */
private int posicionEn(Deque<Reserva> cola, String idEmpleado) {
int posicion = 1;
for (Iterator<Reserva> it = cola.iterator(); it.hasNext(); posicion++) {
if (it.next().idEmpleado().equals(idEmpleado)) {
return posicion;
}
}
return 0;
}
public int tamanoCola(String referencia) {
Deque<Reserva> cola = colasPorMaterial.get(referencia);
return (cola == null) ? 0 : cola.size();
}
// ------------------------------------------------------------------
public static void main(String[] args) {
ColaReservas colas = new ColaReservas();
String LIBRO = "LIB-0001";
System.out.println("=== Reservas correctas ===");
System.out.println("Marta -> posicion " + colas.reservar(LIBRO, "EMP-001", 10));
System.out.println("Diego -> posicion " + colas.reservar(LIBRO, "EMP-002", 11));
System.out.println("Nuria -> posicion " + colas.reservar(LIBRO, "EMP-003", 12));
System.out.println("\n=== Fallos ===");
intentar("Reserva duplicada", () -> colas.reservar(LIBRO, "EMP-001", 13));
intentar("Cancelar inexistente", () -> colas.cancelar(LIBRO, "EMP-009"));
intentar("Dia invalido", () -> colas.reservar(LIBRO, "EMP-004", 0));
intentar("Referencia nula", () -> colas.reservar(null, "EMP-004", 5));
// Llenar la cola hasta el maximo
colas.reservar(LIBRO, "EMP-004", 13);
colas.reservar(LIBRO, "EMP-005", 14);
System.out.println("\nCola de " + LIBRO + ": " + colas.tamanoCola(LIBRO)
+ "/" + MAX_RESERVAS_POR_MATERIAL);
intentar("Cola llena", () -> colas.reservar(LIBRO, "EMP-006", 15));
System.out.println("\n=== Atencion FIFO ===");
Optional<Reserva> siguiente;
while ((siguiente = colas.atenderSiguiente(LIBRO)).isPresent()) {
Reserva r = siguiente.get();
System.out.println(" Atendido " + r.idEmpleado() + " (reservo el dia " + r.dia() + ")");
}
System.out.println("Cola vacia: " + (colas.tamanoCola(LIBRO) == 0));
}
/** Manejador que captura por la RAMA y usa getCodigo() de la raiz. */
private static void intentar(String descripcion, Runnable accion) {
try {
accion.run();
System.out.println("[" + descripcion + "] -> no fallo (inesperado)");
} catch (ReservaException e) {
System.out.println("[" + descripcion + "] [" + e.getCodigo() + "] " + e.getMessage()
+ " | corregible: " + e.esCorregiblePorElUsuario());
} catch (NullPointerException | IllegalArgumentException e) {
System.out.println("[" + descripcion + "] [TECNICO] "
+ e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}Salida:
=== Reservas correctas === Marta -> posicion 1 Diego -> posicion 2 Nuria -> posicion 3 === Fallos === [Reserva duplicada] [RESERVADUPLICADA] El empleado EMP-001 ya tiene reservado LIB-0001 (posicion 1 de la cola) | corregible: true [Cancelar inexistente] [RESERVANOENCONTRADA] El empleado EMP-009 no tiene ninguna reserva de LIB-0001 | corregible: true [Dia invalido] [TECNICO] IllegalArgumentException: El dia debe ser 1 o posterior, y era: 0 [Referencia nula] [TECNICO] NullPointerException: La referencia no puede ser nula Cola de LIB-0001: 5/5 [Cola llena] [COLARESERVASLLENA] La cola de reservas de LIB-0001 esta llena (5 reservas). Disponibilidad estimada: dia 90 | corregible: false === Atencion FIFO === Atendido EMP-001 (reservo el dia 10) Atendido EMP-002 (reservo el dia 11) Atendido EMP-003 (reservo el dia 12) Atendido EMP-004 (reservo el dia 13) Atendido EMP-005 (reservo el dia 14) Cola vacia: true
Fíjate en la separación limpia del manejador: un catch para las reglas de negocio —capturado por la rama ReservaException, que además puede usar getCodigo() y esCorregiblePorElUsuario() de la raíz— y otro para los fallos técnicos con los tipos estándar. Los primeros son mensajes para el usuario; los segundos son bugs que en 06-07 acabarán en el registro con un identificador de incidencia.
Conclusión
Ya sabes crear excepciones que hablen el lenguaje de tu dominio. Conoces las dos razones que lo justifican: nombrar el fallo —para que el tipo sea la decisión y desaparezcan los frágiles e.getMessage().contains("limite")— y, sobre todo, transportar datos estructurados, porque el mensaje es para las personas y los campos son para el programa.
Sabes cómo se crea una: extendiendo Exception o RuntimeException, nunca Throwable ni Error directamente. Y tienes el criterio de elección entre comprobada y no comprobada, con la pregunta que lo decide —¿va el llamador a hacer algo distinto de propagar en la mayoría de los casos?— y su aplicación a BiblioTech: todas las reglas de negocio, no comprobadas; la única comprobada, CatalogoNoAccesibleException, porque representa un fallo del entorno frente al cual la aplicación sí tiene una alternativa. Sabes además que una comprobada en una interfaz ata a todas sus implementaciones presentes y futuras, por la regla de sobrescritura de 06-03.
Dominas los cuatro constructores canónicos y por qué el tercero, (String, Throwable), es imprescindible: sin él, alguien acabará perdiendo una causa. Sabes añadir campos propios —siempre final, con el mensaje construido a partir de ellos en el super(...), y guardando identificadores en lugar de grafos de objetos— y métodos de conveniencia que calculen lo que quien captura va a necesitar: diasDeEspera, devolucionesNecesarias, catalogoVacio.
Has diseñado una jerarquía de dominio de tres niveles con una raíz abstracta común, y entiendes qué habilita esa raíz: capturar todo el dominio de una vez en la frontera de errores —distinguiendo "el usuario pidió algo imposible" de "tenemos un bug"—, compartir comportamiento como getCodigo() y esCorregiblePorElUsuario(), y disponer de un punto único de evolución. Con la advertencia de no hacerla más profunda de lo que vas a usar: si nunca vas a escribir un catch de un nivel intermedio, ese nivel sobra.
Conoces las convenciones: sufijo Exception, nombres que describen el problema y no la solución, específicos y sin el prefijo del proyecto en las hojas; y mensajes que responden qué pasó, con qué dato y qué se esperaba, sin datos sensibles, sin soluciones sugeridas que dependan del contexto y sin saltos de línea. Sabes declarar serialVersionUID y por qué —todas las excepciones son Serializable, y 07-05 desarrolla la mecánica—, y sabes cuándo NO crear una excepción propia: si ya existe una estándar con tu misma semántica y no necesitas transportar datos, úsala. Las validaciones técnicas usan IllegalArgumentException, NullPointerException y IllegalStateException; solo las reglas de negocio merecen clases propias. Y tienes la nota avanzada sobre writableStackTrace = false para las excepciones de control muy frecuentes, con la advertencia de que una excepción sin stack trace en producción es imposible de diagnosticar.
BiblioTech tiene ahora su vocabulario completo: BiblioTechException abstracta como raíz, CatalogoException y PrestamoException como ramas, y cinco hojas con datos —MaterialNoEncontradoException con la referencia buscada y el tamaño del catálogo, ReferenciaDuplicadaException con el tipo de colisión y el título existente, MaterialNoDisponibleException con el titular actual y el día previsto de devolución, LimitePrestamosExcedidoException con el límite y los préstamos actuales, y PrestamoYaDevueltoException con el día de la devolución original—, más la comprobada CatalogoNoAccesibleException. Y la demostración lo prueba: cada fallo desencadena ahora una reacción distinta y específica —buscar materiales parecidos, ofrecer una reserva con los días de espera calculados, decirle al empleado cuántos materiales debe devolver— en lugar de imprimir una línea de texto.
Quedan dos fragilidades de la lista del módulo 5. La cuarta: las operaciones pueden fallar a medias dejando el estado incoherente. Si GestorPrestamos.prestar marca el material como prestado y luego falla al anotarlo en el registro, el material queda bloqueado sin ningún préstamo que lo justifique. Ninguna excepción, por bien diseñada que esté, arregla eso por sí sola: hace falta un mecanismo que garantice que cierto código se ejecute pase lo que pase.
Eso es la lección siguiente, Bloque Finally: qué garantiza exactamente finally —que se ejecuta con y sin excepción, y también cuando el try o el catch hacen return, break o continue—, con la traza completa del orden de ejecución; las combinaciones try-catch-finally y try-finally sin catch, y para qué sirve esta última; el propósito clásico de liberar recursos; la trampa del return dentro de finally, que descarta el valor y hasta la excepción pendiente; el finally que lanza su propia excepción y hace desaparecer la original —el problema que motiva directamente el try-with-resources de 06-06—; los dos casos en que finally no se ejecuta; y el verboso patrón manual de cierre anterior a Java 7. Con la aplicación que resuelve la cuarta fragilidad: GestorPrestamos garantizando que Catalogo y RegistroPrestamos quedan coherentes aunque la operación falle a mitad.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
