Módulo 15: Excepciones
Objetivos del módulo
- Distinguir los errores de compilación, los errores de ejecución y los errores lógicos
- Entender qué es una excepción y cómo se propaga por la pila de llamadas
- Conocer la jerarquía
Throwable,Error,ExceptionyRuntimeException - Diferenciar las excepciones comprobadas de las no comprobadas
- Capturar excepciones con
try/catch, varioscatch, multi-catch yfinally - Propagar excepciones con
throwsy lanzarlas conthrow - Crear excepciones propias con mensaje y causa
- Gestionar recursos con try-with-resources y
AutoCloseable - Leer e interpretar una traza (stack trace) real
- Reconocer las excepciones más habituales y validar la entrada del usuario de forma robusta
- Aplicar las buenas prácticas del manejo de errores
1. Tipos de errores
Cuando un programa no hace lo que esperas, el problema puede aparecer en tres momentos distintos:
| Tipo de error | Cuándo se detecta | Quién lo detecta | Ejemplo |
|---|---|---|---|
| De compilación | Al compilar | El compilador (javac) | Falta un ;, tipo incorrecto, variable no declarada |
| De ejecución | Mientras el programa se ejecuta | La JVM, que lanza una excepción | Dividir entre 0, acceder a la posición 10 de un array de 5 |
| Lógico | Cuando ves un resultado incorrecto | Tú (o tus pruebas) | Usar > en lugar de >=; calcular la media dividiendo entre 2 en vez de entre 3 |
1.1. Errores de compilación
El compilador revisa que el código cumple las reglas del lenguaje. Si no las cumple, no genera el programa:
public class ErrorCompilacion {
public static void main(String[] args) {
int edad = "veinte"; // un texto no cabe en un int
System.out.println(edad);
}
}
ErrorCompilacion.java:3: error: incompatible types: String cannot be converted to int
int edad = "veinte"; // un texto no cabe en un int
^
1 error
error: compilation failed
El mensaje indica el fichero, la línea (:3), la descripción y, con ^, la posición exacta. Si además olvidas un punto y coma, el mensaje es error: ';' expected. Los errores de compilación son los “mejores” errores: el compilador te los señala antes de que el programa llegue a ningún usuario.
1.2. Errores de ejecución
Un programa puede compilar perfectamente y fallar al ejecutarse, porque el problema depende de los datos:
public class ErrorEjecucion {
public static void main(String[] args) {
int total = 100;
int personas = 0;
System.out.println("Calculando el reparto...");
int porPersona = total / personas; // compila, pero falla al ejecutar
System.out.println("A cada uno: " + porPersona);
System.out.println("Fin del programa");
}
}
Salida:
Calculando el reparto...
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ErrorEjecucion.main(ErrorEjecucion.java:6)
El programa empieza bien, pero al llegar a la división la JVM detecta una operación imposible, lanza una excepción y, como nadie la trata, el programa termina sin ejecutar las líneas siguientes (“Fin del programa” no aparece).
📘 Concepto: Una excepción es un objeto que representa una situación anómala ocurrida durante la ejecución. Cuando se produce, se dice que el código lanza (throws) la excepción. El flujo normal se interrumpe y Java busca un fragmento de código que la capture (catch) y la trate. Si no lo encuentra, el programa termina mostrando una traza con lo que ha pasado.
Piensa en un restaurante: si un camarero se encuentra con un problema que no sabe resolver (un cliente alérgico pregunta por los ingredientes), no se queda bloqueado ni lo ignora: avisa a su responsable, y si este tampoco sabe, al jefe de sala. El aviso sube por la cadena hasta que alguien lo resuelve. Si nadie puede, el problema “llega a la calle”. Las excepciones funcionan igual.
2. La pila de llamadas y la propagación
En el módulo 07 viste que cada vez que un método llama a otro se apila un marco (frame) en la pila de llamadas (call stack), y que al terminar el método su marco se desapila. Las excepciones viajan por esa pila:
public class PilaLlamadas {
static int dividir(int a, int b) {
return a / b; // aquí se produce la excepción
}
static int calcularMedia(int[] datos) {
int suma = 0;
for (int d : datos) {
suma += d;
}
return dividir(suma, datos.length);
}
static void informe(int[] datos) {
System.out.println("Media: " + calcularMedia(datos));
}
public static void main(String[] args) {
informe(new int[] {4, 6, 8});
informe(new int[] {}); // array vacío: longitud 0
System.out.println("Esta línea no se ejecuta");
}
}
Salida:
Media: 6
Exception in thread "main" java.lang.ArithmeticException: / by zero
at PilaLlamadas.dividir(PilaLlamadas.java:4)
at PilaLlamadas.calcularMedia(PilaLlamadas.java:12)
at PilaLlamadas.informe(PilaLlamadas.java:16)
at PilaLlamadas.main(PilaLlamadas.java:21)
main llama a informe, que llama a calcularMedia, que llama a dividir. Con el array vacío, dividir intenta 0 / 0 y lanza ArithmeticException. Como dividir no la trata, termina inmediatamente y la excepción pasa a calcularMedia; tampoco la trata, así que pasa a informe, luego a main y, finalmente, a la JVM, que muestra la traza y detiene el programa.
Figura 1. Propagación de una excepción por la pila de llamadas y su reflejo en la traza.
2.1. Cómo leer una traza (stack trace)
Una traza se lee de arriba abajo:
| Línea | Qué te dice |
|---|---|
Exception in thread "main" | En qué hilo ocurrió (de momento, siempre main) |
java.lang.ArithmeticException | El tipo de excepción: la pista principal |
: / by zero | El mensaje: el detalle de lo que pasó |
at PilaLlamadas.dividir(PilaLlamadas.java:4) | Dónde se produjo: clase, método, fichero y línea |
at PilaLlamadas.calcularMedia(PilaLlamadas.java:12) | Quién llamó a ese método, y desde qué línea |
| … | … y así hasta main |
💡 Consejo: Para depurar, busca la primera línea
atque pertenezca a tu código (no ajava.base/...). Esa es la línea donde tienes que empezar a mirar. En IntelliJ IDEA y VS Code los nombres de fichero de la traza son enlaces: haz clic y el editor te lleva a esa línea.
3. Jerarquía de excepciones
Las excepciones son objetos, y sus clases forman una jerarquía de herencia (módulo 12). Conocerla es imprescindible para saber qué capturar.
Figura 2. Jerarquía de las excepciones más habituales (algunas heredan de forma indirecta: por ejemplo,
NoSuchFileException hereda de FileSystemException, que hereda de IOException).
| Clase | Significado | ¿Se captura? |
|---|---|---|
Throwable | Raíz de todo lo que se puede lanzar con throw | No directamente |
Error | Fallos graves de la JVM: memoria agotada (OutOfMemoryError), recursión infinita (StackOverflowError) | No: el programa no puede arreglarlos |
Exception | Situaciones excepcionales que un programa razonable puede querer tratar | Sí |
RuntimeException | Subclase de Exception para errores que suelen deberse a un fallo de programación (un null, un índice mal calculado) | Opcional |
3.1. Excepciones comprobadas y no comprobadas
Java divide las excepciones en dos familias, y el compilador las trata de forma distinta:
| Comprobadas (checked) | No comprobadas (unchecked) | |
|---|---|---|
| Qué clases | Exception y sus hijas, excepto RuntimeException | RuntimeException y sus hijas (y los Error) |
| Ejemplos | IOException, FileNotFoundException, InterruptedException | NullPointerException, ArithmeticException, NumberFormatException, IllegalArgumentException |
| ¿Obliga el compilador a tratarlas? | Sí: o las capturas (catch) o las declaras (throws) | No |
| Qué suelen indicar | Problemas externos que no puedes evitar: un fichero que no existe, una red que se cae | Errores de programación que deberías evitar comprobando antes |
Si llamas a un método que lanza una excepción comprobada y no haces nada, el programa no compila. Thread.sleep(ms), que detiene el programa unos milisegundos, puede lanzar la excepción comprobada InterruptedException:
public class Comprobada {
static void esperar(int ms) {
Thread.sleep(ms); // sleep puede lanzar InterruptedException (comprobada)
}
public static void main(String[] args) {
esperar(100);
}
}
Comprobada.java:3: error: unreported exception InterruptedException; must be caught or declared to be thrown
Thread.sleep(ms); // sleep puede lanzar InterruptedException (comprobada)
^
1 error
error: compilation failed
El mensaje lo dice claramente: must be caught or declared to be thrown (“debe capturarse o declararse”). Verás las dos soluciones en las secciones 4 y 7.
4. Capturar excepciones: try y catch
4.1. Sintaxis básica
try {
// código que puede lanzar una excepción
} catch (TipoDeExcepcion e) {
// qué hacer si se lanza
}
- El bloque
trycontiene el código “arriesgado”. - Si dentro del
tryse lanza una excepción del tipo indicado (o de una subclase), el resto deltryse salta y se ejecuta el bloquecatch. ees una variable que referencia el objeto excepción: puedes consultar su mensaje, su tipo, etc.- Después del
catch, el programa continúa normalmente tras el bloquetry/catch.
public class TryCatchBasico {
public static void main(String[] args) {
String[] entradas = {"42", "3.5", "hola", "-7"};
for (String texto : entradas) {
try {
int numero = Integer.parseInt(texto); // puede lanzar la excepción
System.out.println(texto + " -> el doble es " + numero * 2);
} catch (NumberFormatException e) {
System.out.println(texto + " -> no es un entero (" + e.getMessage() + ")");
}
}
System.out.println("El programa continúa con normalidad");
}
}
Salida:
42 -> el doble es 84
3.5 -> no es un entero (For input string: "3.5")
hola -> no es un entero (For input string: "hola")
-7 -> el doble es -14
El programa continúa con normalidad
Gracias al try/catch, un dato incorrecto ya no detiene el programa: se informa del problema y se sigue con el siguiente.
4.2. Qué información tiene una excepción
| Método | Devuelve | Ejemplo con Integer.parseInt("hola") |
|---|---|---|
getMessage() | El mensaje | For input string: "hola" |
toString() | Tipo completo + mensaje (lo que sale con println(e)) | java.lang.NumberFormatException: For input string: "hola" |
getClass().getSimpleName() | Solo el nombre de la clase | NumberFormatException |
getCause() | La excepción que la provocó, si existe (sección 9) | null |
printStackTrace() | Nada: imprime la traza completa en la salida de error | (la traza) |
⚠️ Importante:
printStackTrace()es útil mientras aprendes y depuras, pero no es una forma de tratar un error. Un usuario no debe ver nunca una traza: muéstrale un mensaje comprensible y, en aplicaciones reales, registra la traza en un fichero de log (módulo 17).
5. Varios catch y multi-catch
Un mismo try puede tener varios catch, uno por cada tipo de problema que quieras tratar de forma distinta. Desde Java 7 también puedes capturar varios tipos en un mismo catch separándolos con | (multi-catch):
public class VariosCatch {
static void probar(String[] datos, int indice, int divisor) {
try {
int valor = Integer.parseInt(datos[indice].strip());
System.out.println("Resultado: " + valor / divisor);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Índice no válido: " + e.getMessage());
} catch (NumberFormatException | NullPointerException e) { // multi-catch
System.out.println("Dato incorrecto: " + e.getClass().getSimpleName());
} catch (RuntimeException e) { // la más general, al final
System.out.println("Otro problema: " + e);
}
}
public static void main(String[] args) {
String[] datos = {"10", "abc", null, "8"};
probar(datos, 0, 2); // correcto
probar(datos, 7, 2); // índice fuera de rango
probar(datos, 1, 2); // "abc" no es un número
probar(datos, 2, 2); // null
probar(datos, 3, 0); // división entre cero
}
}
Salida:
Resultado: 5
Índice no válido: Index 7 out of bounds for length 4
Dato incorrecto: NumberFormatException
Dato incorrecto: NullPointerException
Otro problema: java.lang.ArithmeticException: / by zero
Reglas:
- Java prueba los
catchde arriba abajo y ejecuta solo el primero cuyo tipo coincida. - Por eso, los tipos más específicos (subclases) van antes que los generales. Si pones
RuntimeExceptionantes queArithmeticException, el segundocatchsería inalcanzable y el programa no compila:
try {
System.out.println(10 / 0);
} catch (RuntimeException e) {
System.out.println("Error general");
} catch (ArithmeticException e) { // nunca se alcanzaría
System.out.println("División entre cero");
}
CatchOrdenError.java:7: error: exception ArithmeticException has already been caught
} catch (ArithmeticException e) { // nunca se alcanzaría
^
1 error
error: compilation failed
- En un multi-catch, los tipos no pueden estar relacionados por herencia (no tiene sentido
NumberFormatException | IllegalArgumentException, porque la primera es hija de la segunda). Además, la variableees implícitamentefinal.
6. El bloque finally
El bloque finally se coloca tras los catch (o directamente tras el try) y contiene código que se ejecuta siempre: haya excepción o no, se capture o no, e incluso si el try o el catch hacen return.
public class Finally {
static int procesar(String texto) {
System.out.println("-- procesar(\"" + texto + "\")");
try {
int n = Integer.parseInt(texto);
System.out.println(" try: convertido " + n);
return n; // ¡finally se ejecuta igualmente!
} catch (NumberFormatException e) {
System.out.println(" catch: no es un número");
return -1;
} finally {
System.out.println(" finally: siempre se ejecuta");
}
}
public static void main(String[] args) {
System.out.println("Devuelve " + procesar("25"));
System.out.println("Devuelve " + procesar("x"));
}
}
Salida:
-- procesar("25")
try: convertido 25
finally: siempre se ejecuta
Devuelve 25
-- procesar("x")
catch: no es un número
finally: siempre se ejecuta
Devuelve -1
Fíjate en el primer caso: el try ejecuta return n, pero antes de salir del método se ejecuta el finally.
Figura 3. Flujo de ejecución de un bloque try/catch/finally.
¿Para qué sirve? Para liberar recursos pase lo que pase: cerrar un fichero, una conexión de red o un Scanner, devolver un objeto a su sitio, apagar un indicador de “procesando”… Para cerrar recursos existe una forma todavía mejor, el try-with-resources (sección 10).
⚠️ Importante: No pongas
returndentro de unfinally: anularía el valor devuelto por eltryy “se tragaría” cualquier excepción pendiente. Elfinallyes para limpiar, no para decidir resultados.
7. Propagar excepciones: throws
A veces el método donde se produce el problema no sabe qué hacer con él. Lo correcto entonces es no capturarlo y dejar que lo decida quien lo llamó. Para una excepción comprobada, eso se declara en la cabecera del método con throws:
public class Propagacion {
// Opción 1: declarar con throws (la excepción "sube" a quien llame)
static void esperar(int ms) throws InterruptedException {
System.out.println("Esperando " + ms + " ms...");
Thread.sleep(ms);
}
// Opción 2: capturarla aquí mismo
static void esperarSinAvisar(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
System.out.println("Me han interrumpido");
}
}
// main también puede declarar throws: si llega hasta aquí, el programa termina
public static void main(String[] args) throws InterruptedException {
esperar(100);
esperarSinAvisar(100);
System.out.println("Hecho");
}
}
Salida:
Esperando 100 ms...
Hecho
throws InterruptedException es una advertencia en la firma del método: “cuidado, quien me llame tendrá que tratar esta excepción o volver a declararla”. Es parte del contrato del método, igual que los parámetros y el tipo de retorno.
| Opción | Cuándo usarla |
|---|---|
Capturar (try/catch) | El método sabe cómo recuperarse: reintentar, usar un valor por defecto, pedir el dato otra vez |
Declarar (throws) | El método no sabe qué hacer; la decisión corresponde a un nivel superior (normalmente, el que habla con el usuario) |
💡 Consejo: Que
maindeclarethrows Exceptiones cómodo en programas de prueba, pero significa “si algo falla, que el programa termine con una traza”. En una aplicación de verdad, el nivel más alto (el que interactúa con el usuario) debe capturar las excepciones y mostrar un mensaje claro.
Las excepciones no comprobadas también se propagan igual, pero no hace falta declararlas (aunque puedes hacerlo, y documentarlas en el Javadoc, módulo 20).
8. Lanzar excepciones: throw
Hasta ahora las excepciones las lanzaba Java. Tú también puedes lanzarlas con throw cuando detectas una situación incorrecta. El caso típico es validar argumentos en constructores y setters (módulo 11), en lugar de imprimir un mensaje y seguir como si nada:
class Persona {
private final String nombre;
private int edad;
Persona(String nombre, int edad) {
if (nombre == null || nombre.isBlank()) {
throw new IllegalArgumentException("El nombre no puede estar vacío");
}
this.nombre = nombre;
setEdad(edad);
}
public void setEdad(int edad) {
if (edad < 0 || edad > 150) {
throw new IllegalArgumentException("Edad fuera de rango: " + edad);
}
this.edad = edad;
}
@Override
public String toString() {
return nombre + " (" + edad + ")";
}
}
public class Throw {
public static void main(String[] args) {
Persona p = new Persona("Lucía", 30);
System.out.println(p);
try {
p.setEdad(-5);
} catch (IllegalArgumentException e) {
System.out.println("No se pudo cambiar la edad: " + e.getMessage());
}
System.out.println(p); // el objeto sigue en un estado válido
Persona q = new Persona("", 20); // sin try: el programa termina
}
}
Salida:
Lucía (30)
No se pudo cambiar la edad: Edad fuera de rango: -5
Lucía (30)
Exception in thread "main" java.lang.IllegalArgumentException: El nombre no puede estar vacío
at Persona.<init>(Throw.java:7)
at Throw.main(Throw.java:36)
Observa:
throw new IllegalArgumentException("...")crea el objeto excepción con un mensaje y lo lanza. El método termina en ese punto (como conreturn).- Tras el error de
setEdad(-5), el objeto sigue con su edad anterior: validar antes de modificar mantiene el objeto en un estado válido. - En la traza,
Persona.<init>significa “el constructor dePersona”.
⚠️ Importante: No confundas
throw(sentencia que lanza una excepción, dentro del cuerpo) conthrows(cláusula que declara en la cabecera qué excepciones puede lanzar el método).
Excepciones estándar que conviene reutilizar antes de inventar las tuyas:
| Excepción | Úsala cuando… |
|---|---|
IllegalArgumentException | Un argumento tiene un valor no válido (edad negativa, precio 0) |
IllegalStateException | El objeto no está en un estado que permita la operación (retirar de una cuenta cerrada) |
NullPointerException | Un argumento es null y no debe serlo (puedes usar Objects.requireNonNull(x, "mensaje")) |
UnsupportedOperationException | La operación no está soportada (modificar una lista inmutable) |
IndexOutOfBoundsException | Un índice está fuera de rango |
9. Excepciones propias
9.1. Por qué y cómo
Cuando un error pertenece a tu dominio (saldo insuficiente, nota fuera de rango, stock agotado), una excepción propia hace el código mucho más expresivo: catch (SaldoInsuficienteException e) se entiende solo, y además puede llevar datos útiles (cuánto falta).
Crear una es tan sencillo como heredar:
- De
Exceptionsi quieres que sea comprobada (el que llama está obligado a pensar en ella). - De
RuntimeExceptionsi quieres que sea no comprobada (errores de programación o situaciones que no tiene sentido obligar a tratar).
Por convención, el nombre termina en Exception y se ofrecen al menos dos constructores: uno con el mensaje y otro con el mensaje y la causa:
class ConfiguracionException extends Exception {
public ConfiguracionException(String mensaje) {
super(mensaje);
}
public ConfiguracionException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}
9.2. Encadenar excepciones (la causa)
A menudo capturas una excepción “técnica” y lanzas otra que tenga sentido para tu programa. Al hacerlo, pasa la original como causa: así no se pierde la información de qué ocurrió realmente.
class ConfiguracionException extends Exception {
public ConfiguracionException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}
public class TraceCausa {
static int leerPuerto(String valor) throws ConfiguracionException {
try {
return Integer.parseInt(valor);
} catch (NumberFormatException e) {
// Envolvemos la excepción técnica en una de nuestro dominio, conservando la causa
throw new ConfiguracionException("Puerto mal configurado: '" + valor + "'", e);
}
}
static void arrancarServidor() throws ConfiguracionException {
int puerto = leerPuerto("80a");
System.out.println("Servidor en el puerto " + puerto);
}
public static void main(String[] args) throws ConfiguracionException {
arrancarServidor();
}
}
Salida (compilando con javac TraceCausa.java y ejecutando con java TraceCausa):
Exception in thread "main" ConfiguracionException: Puerto mal configurado: '80a'
at TraceCausa.leerPuerto(TraceCausa.java:14)
at TraceCausa.arrancarServidor(TraceCausa.java:19)
at TraceCausa.main(TraceCausa.java:24)
Caused by: java.lang.NumberFormatException: For input string: "80a"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:565)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at TraceCausa.leerPuerto(TraceCausa.java:11)
... 2 more
Cómo se lee esta traza:
- La excepción principal es
ConfiguracionExceptioncon un mensaje útil para una persona: qué valor estaba mal. Caused by:muestra la causa original:parseIntno pudo convertir"80a".- Las líneas
java.base/...son de la biblioteca de Java; la primera línea tuya de la causa esTraceCausa.leerPuerto(TraceCausa.java:11): ahí está la llamada aparseInt. ... 2 moresignifica que las dos líneas siguientes son las mismas que en la traza principal (arrancarServidorymain), y Java no las repite.
💡 Consejo: Si ejecutas el programa directamente con
java TraceCausa.java(sin compilar antes), la causa muestra además unas líneas internas del lanzador de código fuente (jdk.compiler/com.sun.tools.javac.launcher...). No te asustes: no son de tu programa.
Si no pasaras e como causa (throw new ConfiguracionException("Puerto mal configurado")), la parte Caused by: desaparecería y perderías la pista del error original. Nunca pierdas la causa.
10. Try-with-resources y AutoCloseable
Muchos objetos usan recursos que hay que liberar al terminar: ficheros, conexiones, Scanner… Con finally se puede, pero el código es largo y fácil de equivocar. Java 7 introdujo el try-with-resources: declaras el recurso entre paréntesis tras try y Java lo cierra automáticamente al salir del bloque, haya excepción o no.
Funciona con cualquier objeto que implemente la interfaz AutoCloseable (módulo 13), que tiene un único método: void close().
// Un "recurso" de prueba que avisa cuando se abre y se cierra
class Recurso implements AutoCloseable {
private final String nombre;
Recurso(String nombre) {
this.nombre = nombre;
System.out.println(" Abriendo " + nombre);
}
void usar(boolean fallar) {
System.out.println(" Usando " + nombre);
if (fallar) {
throw new IllegalStateException("fallo usando " + nombre);
}
}
@Override
public void close() {
System.out.println(" Cerrando " + nombre);
}
}
public class TryWithResources {
public static void main(String[] args) {
System.out.println("Caso 1: todo va bien");
try (Recurso a = new Recurso("A"); Recurso b = new Recurso("B")) {
a.usar(false);
b.usar(false);
}
System.out.println("Caso 2: hay una excepción");
try (Recurso c = new Recurso("C")) {
c.usar(true);
System.out.println(" (esta línea no se ejecuta)");
} catch (IllegalStateException e) {
System.out.println(" Capturada: " + e.getMessage());
} finally {
System.out.println(" finally");
}
}
}
Salida:
Caso 1: todo va bien
Abriendo A
Abriendo B
Usando A
Usando B
Cerrando B
Cerrando A
Caso 2: hay una excepción
Abriendo C
Usando C
Cerrando C
Capturada: fallo usando C
finally
Detalles importantes:
- Se pueden declarar varios recursos separados por
;. Se cierran en orden inverso a su apertura (B antes que A), igual que se cierran unas muñecas rusas. - El recurso se cierra antes de ejecutar el
catchy elfinally. - Si
close()también lanza una excepción mientras ya había otra, la declose()no se pierde: queda suprimida dentro de la principal (e.getSuppressed()). - Desde Java 9 puedes usar una variable ya existente si es final o efectivamente final:
try (recurso) { ... }.
En el módulo 17 lo usarás constantemente con ficheros:
try (BufferedReader lector = Files.newBufferedReader(Path.of("datos.txt"))) {
// leer líneas...
} // el fichero se cierra aquí, pase lo que pase
💡 Consejo: Cada vez que crees un objeto que implementa
AutoCloseable(o su hijaCloseable), pregúntate: “¿quién lo cierra?”. Si la respuesta es “yo, en este método”, usa try-with-resources. La excepción es elScanner(System.in): no lo cierres, porque cerrarías también la entrada estándar y no podrías volver a leer del teclado.
11. Excepciones habituales
Estas son las excepciones que más vas a ver mientras aprendes. El siguiente programa provoca cada una a propósito y muestra su tipo y su mensaje real:
import java.util.Scanner;
class Cliente {
String nombre; // sin inicializar: vale null
Cliente pareja; // sin inicializar: vale null
}
public class Habituales {
// Cada caso provoca a propósito una excepción típica
static void provocar(int caso) {
switch (caso) {
case 1 -> {
String texto = null;
System.out.println(texto.length());
}
case 2 -> {
Cliente c = new Cliente();
System.out.println(c.pareja.nombre);
}
case 3 -> {
int a = 7, b = 0;
System.out.println(a / b);
}
case 4 -> {
int[] notas = new int[3];
notas[3] = 10;
}
case 5 -> System.out.println(Integer.parseInt("12,5"));
case 6 -> System.out.println("Java".charAt(10));
case 7 -> {
Object o = "hola";
Integer n = (Integer) o;
}
case 8 -> {
Scanner sc = new Scanner("doce");
System.out.println(sc.nextInt());
}
}
}
public static void main(String[] args) {
for (int caso = 1; caso <= 8; caso++) {
try {
provocar(caso);
} catch (RuntimeException e) {
System.out.println(caso + ") " + e.getClass().getName());
System.out.println(" " + e.getMessage());
}
}
}
}
Salida (compilado con javac -g, que es lo que hacen por defecto los IDE y Maven):
1) java.lang.NullPointerException
Cannot invoke "String.length()" because "texto" is null
2) java.lang.NullPointerException
Cannot read field "nombre" because "c.pareja" is null
3) java.lang.ArithmeticException
/ by zero
4) java.lang.ArrayIndexOutOfBoundsException
Index 3 out of bounds for length 3
5) java.lang.NumberFormatException
For input string: "12,5"
6) java.lang.StringIndexOutOfBoundsException
Index 10 out of bounds for length 4
7) java.lang.ClassCastException
class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
8) java.util.InputMismatchException
null
| Excepción | Causa típica | Cómo evitarla |
|---|---|---|
NullPointerException | Usar un método o atributo de una referencia que vale null | Inicializa los atributos; comprueba if (x != null); valida en el constructor con Objects.requireNonNull |
ArithmeticException | División entera entre 0 (con double no hay excepción: da Infinity o NaN) | Comprueba el divisor antes de dividir |
ArrayIndexOutOfBoundsException | Índice < 0 o >= length (el clásico i <= array.length) | Recorre con i < array.length o con for-each |
StringIndexOutOfBoundsException | charAt, substring… con posiciones fuera del texto | Comprueba length() |
NumberFormatException | Integer.parseInt / Double.parseDouble con un texto que no es un número (¡"12,5" usa coma!) | Captúrala al convertir la entrada del usuario |
ClassCastException | Downcasting a un tipo que el objeto no tiene | Comprueba con instanceof (mejor, con patrón: módulo 14) |
InputMismatchException | Scanner.nextInt() (o nextDouble…) cuando lo siguiente no es un número | Captúrala y descarta la entrada con nextLine() (sección 12) |
11.1. Mensajes útiles de NullPointerException (Java 14+)
Antes de Java 14, una NullPointerException no tenía mensaje: solo sabías la línea, y si en esa línea había varias referencias (c.pareja.nombre) tenías que adivinar cuál era null. Desde Java 14 (y activado por defecto desde Java 15), la JVM explica exactamente qué era null: because “c.pareja” is null.
public class NpeDetallado {
public static void main(String[] args) {
String[] palabras = new String[2];
System.out.println(palabras[1].toUpperCase());
}
}
Si compilas con javac -g (o desde el IDE), el mensaje usa los nombres de tus variables:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "palabras[1]" is null
at NpeDetallado.main(NpeDetallado.java:4)
Si compilas sin información de depuración (javac a secas, o ejecutando java NpeDetallado.java), las variables locales aparecen numeradas:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "<local1>[1]" is null
at NpeDetallado.main(NpeDetallado.java:4)
new String[2] crea un array con dos posiciones que valen null (módulo 08): no hay ningún texto al que pasar a mayúsculas.
⚠️ Importante: Una
NullPointerException, unaArrayIndexOutOfBoundsExceptiono unaClassCastExceptioncasi siempre indican un error en tu código. No las “arregles” con untry/catchque las oculte: corrige la causa (inicializa, comprueba, valida).
12. Validar la entrada del usuario
El usuario puede escribir cualquier cosa. Un programa robusto no se rompe por una entrada incorrecta: avisa y vuelve a preguntar. La combinación clásica es un bucle que repite la lectura hasta que el dato es válido, y una excepción para detectar que no lo es.
import java.util.InputMismatchException;
import java.util.Scanner;
public class LecturaRobusta {
// Versión 1: nextInt() + InputMismatchException
static int leerEntero(Scanner sc, String mensaje) {
while (true) {
System.out.print(mensaje);
try {
int n = sc.nextInt();
sc.nextLine(); // consumir el resto de la línea
return n;
} catch (InputMismatchException e) {
String basura = sc.nextLine(); // ¡imprescindible! descartar la entrada errónea
System.out.println(" \"" + basura + "\" no es un número entero. Inténtalo de nuevo.");
}
}
}
// Versión 2: leer la línea completa + parseInt + validar el rango
static int leerEnteroEnRango(Scanner sc, String mensaje, int min, int max) {
while (true) {
System.out.print(mensaje);
String linea = sc.nextLine().strip();
try {
int n = Integer.parseInt(linea);
if (n >= min && n <= max) {
return n;
}
System.out.println(" Debe estar entre " + min + " y " + max + ".");
} catch (NumberFormatException e) {
System.out.println(" \"" + linea + "\" no es un número entero.");
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int edad = leerEntero(sc, "Edad: ");
int mes = leerEnteroEnRango(sc, "Mes de nacimiento (1-12): ", 1, 12);
System.out.println("Edad " + edad + ", nacido en el mes " + mes);
}
}
Ejemplo de ejecución (lo que escribe el usuario aparece tras cada pregunta):
Edad: veinte
"veinte" no es un número entero. Inténtalo de nuevo.
Edad: 25
Mes de nacimiento (1-12): 13
Debe estar entre 1 y 12.
Mes de nacimiento (1-12): cero
"cero" no es un número entero.
Mes de nacimiento (1-12): abril
"abril" no es un número entero.
Mes de nacimiento (1-12): 4
Edad 25, nacido en el mes 4
Las dos versiones son correctas, pero conviene entender sus diferencias:
nextInt() + InputMismatchException | nextLine() + parseInt + NumberFormatException | |
|---|---|---|
| Qué lee | Solo el siguiente “token” | La línea completa |
| Trampa | Si falla, el texto erróneo sigue en el buffer: sin sc.nextLine() en el catch, el bucle sería infinito | Ninguna: la línea ya se ha consumido |
Mezclar con nextLine() | Hay que consumir el salto de línea pendiente (módulo 04) | No hay problema |
| Recomendación | Válido para ejercicios sencillos | Preferible: más predecible, y valida rango y formato en un solo sitio |
💡 Consejo: Escribe tus métodos de lectura (
leerEntero,leerDecimal,leerTextoNoVacio…) una vez y reutilízalos en todos tus programas, por ejemplo en una clase de utilidadesTeclado. Es lo que harás en el proyecto final (módulo 21).
13. Ejemplo completo: cuenta bancaria con SaldoInsuficienteException
Este ejemplo reúne casi todo el módulo:
- Una excepción comprobada propia con datos (
getFaltante()), porque quedarse sin saldo es una situación esperable del negocio y quien retira dinero debe decidir qué hacer. - Una excepción no comprobada estándar (
IllegalArgumentException) para un error del programador (un importe negativo). - Propagación con
throwsdesderetirarhastatransferiry de ahí amain, que es quien informa al usuario.
// Excepción COMPROBADA propia: el que retira dinero debe pensar qué hacer si no hay saldo
class SaldoInsuficienteException extends Exception {
private final double saldo;
private final double importe;
public SaldoInsuficienteException(double saldo, double importe) {
super(String.format("Saldo insuficiente: saldo %.2f €, se intentan retirar %.2f €",
saldo, importe));
this.saldo = saldo;
this.importe = importe;
}
public double getFaltante() {
return importe - saldo;
}
}
class Cuenta {
private final String iban;
private double saldo;
public Cuenta(String iban, double saldoInicial) {
if (saldoInicial < 0) {
throw new IllegalArgumentException("El saldo inicial no puede ser negativo");
}
this.iban = iban;
this.saldo = saldoInicial;
}
public void ingresar(double importe) {
validarImporte(importe);
saldo += importe;
}
public void retirar(double importe) throws SaldoInsuficienteException {
validarImporte(importe);
if (importe > saldo) {
throw new SaldoInsuficienteException(saldo, importe);
}
saldo -= importe;
}
public void transferir(Cuenta destino, double importe) throws SaldoInsuficienteException {
retirar(importe); // si falla, no se llega a ingresar: la operación no se hace a medias
destino.ingresar(importe);
}
// Error del PROGRAMADOR: excepción no comprobada
private static void validarImporte(double importe) {
if (importe <= 0) {
throw new IllegalArgumentException("El importe debe ser positivo: " + importe);
}
}
@Override
public String toString() {
return String.format("%s: %.2f €", iban, saldo);
}
}
public class CuentaBancaria {
public static void main(String[] args) {
Cuenta ana = new Cuenta("ES01-0001", 500);
Cuenta luis = new Cuenta("ES02-0002", 50);
double[] importes = {200, 400, -10};
for (double importe : importes) {
try {
ana.transferir(luis, importe);
System.out.println("Transferidos " + importe + " €");
} catch (SaldoInsuficienteException e) {
System.out.println(e.getMessage());
System.out.printf(" Te faltan %.2f €%n", e.getFaltante());
} catch (IllegalArgumentException e) {
System.out.println("Operación no válida: " + e.getMessage());
}
System.out.println(" " + ana + " | " + luis);
}
}
}
Salida:
Transferidos 200.0 €
ES01-0001: 300.00 € | ES02-0002: 250.00 €
Saldo insuficiente: saldo 300.00 €, se intentan retirar 400.00 €
Te faltan 100.00 €
ES01-0001: 300.00 € | ES02-0002: 250.00 €
Operación no válida: El importe debe ser positivo: -10.0
ES01-0001: 300.00 € | ES02-0002: 250.00 €
Fíjate en transferir: primero se retira y luego se ingresa. Si la retirada falla, la excepción interrumpe el método antes de ingresar, así que el dinero no “aparece” en la cuenta destino. Ordenar las operaciones para que un fallo no deje los datos a medias es una idea que verás a lo grande con las transacciones de bases de datos en el Curso Avanzado.
14. Buenas prácticas
| ✘ Evita | ✔ Haz |
|---|---|
catch (Exception e) para todo, “por si acaso” | Captura los tipos concretos que sabes tratar; deja pasar el resto |
Un catch vacío { } que silencia el error | Trata el error, o registra el problema, o vuelve a lanzarlo; nunca lo ignores en silencio |
Mensajes como "Error" o "Algo ha ido mal" | Mensajes con datos: "Saldo insuficiente: saldo 300.00 €, se intentan retirar 400.00 €" |
| Usar excepciones para controlar el flujo normal (salir de un bucle con una excepción) | Usa condiciones normales; las excepciones son para lo excepcional |
try/catch para tapar una NullPointerException | Valida antes: comprueba null, índices, divisores, rangos |
Lanzar Exception o RuntimeException genéricas | Reutiliza las estándar (IllegalArgumentException…) o crea una propia con nombre claro |
Perder la causa al envolver: throw new MiException("x") dentro de un catch | Pásala: throw new MiException("x", e) |
Bloques try enormes que envuelven medio método | try pequeños, alrededor de lo que realmente puede fallar |
| Cerrar recursos a mano (o no cerrarlos) | Try-with-resources |
| Mostrar trazas al usuario final | Mensaje comprensible al usuario; traza al log |
💡 Consejo: Una regla útil para decidir entre comprobada y no comprobada: si quien llama puede hacer algo razonable para recuperarse (pedir otro fichero, avisar de que falta saldo), usa una comprobada. Si el error indica que el código que llama está mal (pasa un
null, un índice negativo), usa una no comprobada. En la práctica, muchas bibliotecas modernas prefieren excepciones no comprobadas para no llenar las firmas dethrows.
15. Ejercicios
Ejercicio 1: División a prueba de errores
Escribe un programa que pida dos enteros (dividendo y divisor) y muestre el cociente y el resto. Si el usuario escribe algo que no es un número, debe volver a pedir ese mismo dato. Si el divisor es 0, debe avisar y empezar de nuevo pidiendo los dos números. Usa un método leerEntero(Scanner, String) y captura ArithmeticException para la división.
Ver solución
import java.util.Scanner;
public class E1Division {
static int leerEntero(Scanner sc, String mensaje) {
while (true) {
System.out.print(mensaje);
String linea = sc.nextLine();
try {
return Integer.parseInt(linea.strip());
} catch (NumberFormatException e) {
System.out.println(" Escribe un número entero.");
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (true) {
int a = leerEntero(sc, "Dividendo: ");
int b = leerEntero(sc, "Divisor: ");
try {
System.out.println(a + " / " + b + " = " + (a / b) + " (resto " + (a % b) + ")");
break;
} catch (ArithmeticException e) {
System.out.println(" No se puede dividir entre 0. Empecemos otra vez.");
}
}
}
}
Ejemplo de ejecución:
Dividendo: diez
Escribe un número entero.
Dividendo: 10
Divisor: 0
No se puede dividir entre 0. Empecemos otra vez.
Dividendo: 10
Divisor: tres
Escribe un número entero.
Divisor: 3
10 / 3 = 3 (resto 1)
leerEntero encapsula el bucle de validación y se reutiliza para los dos datos. Para la división se podría comprobar if (b == 0) antes de dividir (y sería lo más recomendable según la sección 14); aquí se captura ArithmeticException para practicar. Observa que break solo se ejecuta si la división ha ido bien.
Ejercicio 2: Sumar los números válidos de un texto
Escribe static int sumarValidos(String linea) que reciba una lista de valores separados por comas, como "4, 8,x,15,,23, 4.2 ,-1", sume los que sean enteros válidos (ignorando los espacios alrededor) e informe de cada valor ignorado y del total de errores.
Ver solución
public class E2SumaTexto {
static int sumarValidos(String linea) {
int suma = 0, errores = 0;
for (String trozo : linea.split(",")) {
try {
suma += Integer.parseInt(trozo.strip());
} catch (NumberFormatException e) {
errores++;
System.out.println(" Ignorado: \"" + trozo + "\"");
}
}
System.out.println(" " + errores + " valores ignorados");
return suma;
}
public static void main(String[] args) {
String datos = "4, 8,x,15,,23, 4.2 ,-1";
System.out.println("Suma: " + sumarValidos(datos));
}
}
Salida:
Ignorado: "x"
Ignorado: ""
Ignorado: " 4.2 "
3 valores ignorados
Suma: 49
El try está dentro del bucle: así, un valor erróneo solo afecta a ese valor y no detiene el procesamiento de los demás. Si el try envolviera el bucle completo, el primer error terminaría la suma. Este patrón lo usarás para leer ficheros CSV en el módulo 17.
Ejercicio 3: Excepción comprobada para notas
Crea una excepción comprobada NotaInvalidaException que guarde la nota errónea. Escribe static double media(double[] notas) throws NotaInvalidaException que lance esa excepción si alguna nota no está entre 0 y 10, y IllegalArgumentException si el array está vacío. Pruébala con un grupo correcto, otro con una nota 11 y otro vacío.
Ver solución
class NotaInvalidaException extends Exception {
private final double nota;
public NotaInvalidaException(double nota) {
super("Nota fuera de rango (0-10): " + nota);
this.nota = nota;
}
public double getNota() {
return nota;
}
}
public class E3Notas {
static double media(double[] notas) throws NotaInvalidaException {
if (notas.length == 0) {
throw new IllegalArgumentException("No hay notas");
}
double suma = 0;
for (double n : notas) {
if (n < 0 || n > 10) {
throw new NotaInvalidaException(n);
}
suma += n;
}
return suma / notas.length;
}
public static void main(String[] args) {
double[][] grupos = { {7, 8.5, 6}, {9, 11, 5}, {} };
for (double[] g : grupos) {
try {
System.out.printf("Media: %.2f%n", media(g));
} catch (NotaInvalidaException e) {
System.out.println("Error: " + e.getMessage() + " (nota " + e.getNota() + ")");
} catch (IllegalArgumentException e) {
System.out.println("Error de uso: " + e.getMessage());
}
}
}
}
Salida:
Media: 7.17
Error: Nota fuera de rango (0-10): 11.0 (nota 11.0)
Error de uso: No hay notas
Las dos excepciones tienen intenciones distintas: una nota fuera de rango es un dato incorrecto que puede llegar en cualquier momento (comprobada: quien llama debe tratarla); un array vacío es un mal uso del método (no comprobada). Gracias al atributo nota, quien captura la excepción puede saber exactamente qué valor falló.
Ejercicio 4: Conexión simulada con try-with-resources
Crea una clase Conexion que implemente AutoCloseable, lleve la cuenta de las conexiones abiertas en un atributo static y avise al abrir y al cerrar. Su constructor lanza IllegalArgumentException si el servidor está en blanco, y su método consultar(String orden) lanza UnsupportedOperationException si la orden contiene DROP. Escribe un método ejecutar(servidor, orden) con try-with-resources y comprueba que, al final, no queda ninguna conexión abierta.
Ver solución
class Conexion implements AutoCloseable {
private static int abiertas = 0;
private final String servidor;
Conexion(String servidor) {
if (servidor.isBlank()) {
throw new IllegalArgumentException("Servidor vacío");
}
this.servidor = servidor;
abiertas++;
System.out.println("Conectado a " + servidor + " (abiertas: " + abiertas + ")");
}
String consultar(String orden) {
if (orden.contains("DROP")) {
throw new UnsupportedOperationException("Orden prohibida: " + orden);
}
return "OK[" + orden + "]";
}
@Override
public void close() {
abiertas--;
System.out.println("Desconectado de " + servidor + " (abiertas: " + abiertas + ")");
}
static int getAbiertas() {
return abiertas;
}
}
public class E4Conexion {
static void ejecutar(String servidor, String orden) {
try (Conexion c = new Conexion(servidor)) {
System.out.println(" " + c.consultar(orden));
} catch (RuntimeException e) {
System.out.println(" Error: " + e.getMessage());
}
}
public static void main(String[] args) {
ejecutar("db1", "SELECT 1");
ejecutar("db1", "DROP TABLE clientes");
ejecutar(" ", "SELECT 1");
System.out.println("Conexiones abiertas al final: " + Conexion.getAbiertas());
}
}
Salida:
Conectado a db1 (abiertas: 1)
OK[SELECT 1]
Desconectado de db1 (abiertas: 0)
Conectado a db1 (abiertas: 1)
Desconectado de db1 (abiertas: 0)
Error: Orden prohibida: DROP TABLE clientes
Error: Servidor vacío
Conexiones abiertas al final: 0
Fíjate en el segundo caso: “Desconectado” aparece antes que “Error”, porque el recurso se cierra antes de ejecutar el catch. En el tercer caso no aparece “Desconectado”: como el constructor falló, el recurso nunca llegó a existir y no hay nada que cerrar. En todos los casos el contador termina a 0, que es justo lo que garantiza el try-with-resources.
Ejercicio 5: Calculadora de consola robusta
Escribe una calculadora que lea líneas con el formato número operador número (por ejemplo 3 + 4.5) hasta que el usuario escriba fin. Debe soportar + - * / y no romperse nunca: número mal escrito, operador desconocido, división entre 0 o formato incorrecto deben mostrar un mensaje claro y seguir. El método operar debe lanzar las excepciones; el main las captura.
Ver solución
import java.util.Scanner;
public class E5Calculadora {
static double operar(double a, char op, double b) {
return switch (op) {
case '+' -> a + b;
case '-' -> a - b;
case '*' -> a * b;
case '/' -> {
if (b == 0) {
throw new ArithmeticException("división entre cero");
}
yield a / b;
}
default -> throw new IllegalArgumentException("operador desconocido '" + op + "'");
};
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("> ");
String linea = sc.nextLine().strip();
if (linea.equalsIgnoreCase("fin")) {
break;
}
String[] partes = linea.split(" ");
try {
if (partes.length != 3 || partes[1].length() != 1) {
throw new IllegalArgumentException("formato: número operador número");
}
double a = Double.parseDouble(partes[0]);
double b = Double.parseDouble(partes[2]);
System.out.println("= " + operar(a, partes[1].charAt(0), b));
} catch (NumberFormatException e) {
System.out.println("Error: número no válido");
} catch (ArithmeticException | IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
System.out.println("Adiós");
}
}
Ejemplo de ejecución:
> 3 + 4.5
= 7.5
> 10 / 0
Error: división entre cero
> 2 ^ 8
Error: operador desconocido '^'
> siete * 2
Error: número no válido
> 12
Error: formato: número operador número
> 9 / 2
= 4.5
> fin
Adiós
Con double, 10 / 0 no lanza excepción (da Infinity), por eso operar comprueba el divisor y lanza ArithmeticException a mano. El default del switch lanza una excepción: en una expresión switch, throw es una alternativa válida a devolver un valor. El orden de los catch importa: NumberFormatException es hija de IllegalArgumentException, así que debe ir antes; por eso no se puede incluir en el multi-catch de la segunda línea.
Ejercicio 6: Almacén con excepciones propias
Crea un Almacen con códigos y stock en dos arrays. El método servir(codigo, unidades) debe lanzar:
IllegalArgumentExceptionsi las unidades no son positivas.ProductoNoEncontradoException(no comprobada, propia) si el código no existe.StockInsuficienteException(comprobada, propia) si no hay unidades suficientes.
Procesa una lista de pedidos con el formato "A1:3" y muestra cuáles se sirven y cuáles no.
Ver solución
class StockInsuficienteException extends Exception {
public StockInsuficienteException(String producto, int pedidas, int disponibles) {
super("No hay stock de " + producto + ": pedidas " + pedidas
+ ", disponibles " + disponibles);
}
}
class ProductoNoEncontradoException extends RuntimeException {
public ProductoNoEncontradoException(String codigo) {
super("No existe el producto " + codigo);
}
}
class Almacen {
private final String[] codigos = {"A1", "B2", "C3"};
private final int[] stock = {5, 0, 12};
private int buscar(String codigo) {
for (int i = 0; i < codigos.length; i++) {
if (codigos[i].equals(codigo)) {
return i;
}
}
throw new ProductoNoEncontradoException(codigo);
}
public void servir(String codigo, int unidades) throws StockInsuficienteException {
if (unidades <= 0) {
throw new IllegalArgumentException("Unidades no válidas: " + unidades);
}
int i = buscar(codigo);
if (stock[i] < unidades) {
throw new StockInsuficienteException(codigo, unidades, stock[i]);
}
stock[i] -= unidades;
}
public int stockDe(String codigo) {
return stock[buscar(codigo)];
}
}
public class E6Inventario {
public static void main(String[] args) {
Almacen almacen = new Almacen();
String[] pedidos = {"A1:3", "B2:1", "Z9:2", "C3:-4", "A1:3", "C3:12"};
int servidos = 0;
for (String pedido : pedidos) {
String[] p = pedido.split(":");
try {
almacen.servir(p[0], Integer.parseInt(p[1]));
servidos++;
System.out.println("✓ " + pedido);
} catch (StockInsuficienteException | RuntimeException e) {
System.out.println("✗ " + pedido + " -> " + e.getMessage());
}
}
System.out.println("Pedidos servidos: " + servidos + " de " + pedidos.length);
System.out.println("Stock final de A1: " + almacen.stockDe("A1") + ", C3: " + almacen.stockDe("C3"));
}
}
Salida:
✓ A1:3
✗ B2:1 -> No hay stock de B2: pedidas 1, disponibles 0
✗ Z9:2 -> No existe el producto Z9
✗ C3:-4 -> Unidades no válidas: -4
✗ A1:3 -> No hay stock de A1: pedidas 3, disponibles 2
✓ C3:12
Pedidos servidos: 2 de 6
Stock final de A1: 2, C3: 0
catch (StockInsuficienteException | RuntimeException e) es un multi-catch válido porque los dos tipos no están relacionados por herencia (StockInsuficienteException hereda de Exception, no de RuntimeException). Recoge las tres situaciones con un solo bloque porque en este programa todas se tratan igual: informar y seguir con el siguiente pedido. Un pedido fallido no modifica el stock: la comprobación se hace antes de restar.
Ejercicios propuestos
✏️ Ejercicio 7: Edad con reintentos limitados. Escribe un método
leerEdad(Scanner sc)que permita como máximo 3 intentos. Si se agotan, debe lanzar una excepción propiaDemasiadosIntentosException. Elmainla captura y muestra “Inténtalo más tarde”.
✏️ Ejercicio 8: Fecha válida. Crea un
record Fecha(int dia, int mes, int anio)cuyo constructor compacto lanceIllegalArgumentExceptioncon un mensaje preciso (“El mes 13 no existe”, “Febrero de 2027 solo tiene 28 días”). Pide fechas al usuario hasta que introduzca una correcta.
✏️ Ejercicio 9: Traza a mano. Escribe un programa con cuatro métodos encadenados (
allama ab,bac,cad), dondedlance unaIllegalStateException. Antes de ejecutarlo, escribe en papel cómo crees que será la traza. Después, captura la excepción enby comprueba qué cambia. Por último, enc, envuélvela en una excepción propia con causa y observa elCaused by:.
✏️ Ejercicio 10: Matrícula. Modela una
Asignaturacon un número máximo de plazas y un métodomatricular(String alumno)que lancePlazasAgotadasException(comprobada) si no quedan plazas yAlumnoYaMatriculadoException(no comprobada) si el alumno ya está matriculado. Escribe unmainque matricule una lista de alumnos y muestre un resumen.
✏️ Ejercicio 11:
finallyfrente a try-with-resources. Escribe un recursoTemporizador implements AutoCloseableque muestre enclose()el tiempo transcurrido desde su creación (System.currentTimeMillis()). Úsalo para medir un bucle, primero contry/finallyy después con try-with-resources. ¿Cuál queda más claro?
✏️ Ejercicio 12: Pedido con excepciones. Retoma el ejemplo de los estados de un pedido del módulo 14 (sección 9.2) y sustituye el mensaje
✗por una excepción propiaTransicionNoPermitidaExceptionque incluya el estado de origen y el de destino. Elmaindebe capturarla y mostrar su mensaje.
Resumen
| Concepto | Sintaxis / ejemplo | Idea clave |
|---|---|---|
| Excepción | Objeto que representa un error en ejecución | Interrumpe el flujo y se propaga por la pila |
| Traza | Exception in thread "main" ... at Clase.metodo(Fichero.java:N) | Se lee de arriba abajo; busca la primera línea de tu código |
| Jerarquía | Throwable → Error / Exception → RuntimeException | Los Error no se capturan |
| Comprobadas | Hijas de Exception salvo RuntimeException | Obligatorio catch o throws |
| No comprobadas | Hijas de RuntimeException | Suelen ser errores de programación: evítalas validando |
| Capturar | try { ... } catch (Tipo e) { ... } | El programa sigue tras el catch |
Varios catch | De lo más específico a lo más general | Solo se ejecuta el primero que coincide |
| Multi-catch | catch (A \| B e) | Tipos sin relación de herencia |
finally | finally { ... } | Se ejecuta siempre |
| Declarar | void m() throws IOException | La excepción la tratará quien llame |
| Lanzar | throw new IllegalArgumentException("...") | Señalar una situación incorrecta |
| Excepción propia | class MiException extends Exception | Constructores (mensaje) y (mensaje, causa) |
| Causa | new MiException("...", e) y getCause() | Caused by: en la traza; nunca la pierdas |
| Try-with-resources | try (Recurso r = new Recurso()) { ... } | Cierre automático de AutoCloseable, en orden inverso |
| NPE útiles (Java 14+) | because "c.pareja" is null | Indican exactamente qué era null |
| Validar entrada | Bucle + nextLine() + parseInt + catch | El programa nunca se rompe por una entrada incorrecta |