Excepciones en Java
En esta lección
- Qué es una excepción
- La jerarquía de excepciones
- Checked y unchecked
- try y catch: capturar una excepción
- finally: código que se ejecuta siempre
- try-with-resources: cerrar recursos automáticamente
- throw y throws: lanzar excepciones
- Propagar excepciones
- Crear tus propias excepciones
- Leer un stack trace
- Buenas prácticas y errores frecuentes
- Resumen
Un programa real se encuentra con situaciones que no controla: un fichero que no existe, un usuario que escribe “abc” donde se esperaba un número o una división entre cero. En Java, esos problemas se comunican con excepciones. En esta lección aprenderás qué son, cómo capturarlas para que el programa no se detenga, cómo lanzar las tuyas y cómo leer el famoso stack trace que aparece en la consola cuando algo falla.
Qué es una excepción
Una excepción es un objeto que Java crea cuando ocurre un error durante la ejecución. Ese objeto describe qué ha pasado (el tipo de error y un mensaje) y dónde (la cadena de métodos que se estaban ejecutando).
Cuando se produce una excepción, Java interrumpe el método actual y la “lanza” hacia arriba, al método que lo llamó. Si nadie la captura, llega hasta main y el programa termina con un mensaje de error:
int[] notas = {7, 9, 5};
System.out.println(notas[3]);
System.out.println("Fin");
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
// Index 3 out of bounds for length 3
La línea "Fin" nunca se imprime: el array solo tiene las posiciones 0, 1 y 2 (lo viste en arrays).
¿Por qué excepciones y no, por ejemplo, devolver -1 cuando algo falla? Porque un código de error se ignora sin darse cuenta. Una excepción no se puede ignorar: o la tratas o el programa se detiene y te avisa.
La jerarquía de excepciones
Las excepciones son clases, y como aprendiste en herencia, forman una jerarquía. Esta es la parte que más vas a usar:
Throwable
├── Error (fallos graves de la JVM: no se capturan)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception (problemas que el programa puede tratar)
├── IOException (checked)
│ └── FileNotFoundException
├── SQLException (checked)
└── RuntimeException (unchecked)
├── ArithmeticException
├── NullPointerException
├── IllegalArgumentException
│ └── NumberFormatException
├── IndexOutOfBoundsException
│ └── ArrayIndexOutOfBoundsException
└── ClassCastException
Throwablees la raíz: todo lo que se puede lanzar hereda de ella.Errorrepresenta problemas de la propia máquina virtual, como quedarse sin memoria o una recursividad infinita (StackOverflowError). No se capturan.Exceptionagrupa los problemas que tu código sí puede y debe tratar.RuntimeExceptiones una rama especial deException: son errores de programación, como acceder a un índice que no existe.
Gracias al polimorfismo, un catch (Exception e) captura también todas sus subclases.
Checked y unchecked
Java divide las excepciones en dos grupos, y la diferencia es muy importante:
| Checked (comprobadas) | Unchecked (no comprobadas) | |
|---|---|---|
| Clases | Exception y sus hijas, salvo RuntimeException | RuntimeException, Error y sus hijas |
| Ejemplos | IOException, SQLException, FileNotFoundException | NullPointerException, ArithmeticException, NumberFormatException |
| ¿Obliga el compilador a tratarlas? | Sí: capturar con catch o declarar con throws | No |
| Origen típico | Algo externo falla (disco, red, base de datos) | Un fallo en tu código (un null, un índice mal calculado) |
Si llamas a un método que lanza una excepción checked y no la tratas, no compila:
String texto = Files.readString(Path.of("datos.txt"));
// error: unreported exception IOException; must be caught or declared to be thrown
El compilador te obliga a pensar qué hacer si el fichero no existe. Con las unchecked no: no deberían ocurrir si el código es correcto.
try y catch: capturar una excepción
Para tratar una excepción, se mete el código que puede fallar en un bloque try y se indica en catch qué hacer si falla:
try {
int[] notas = {7, 9, 5};
System.out.println(notas[3]);
System.out.println("Esto no se imprime");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println("El programa sigue");
// Error: Index 3 out of bounds for length 3
// El programa sigue
Cómo funciona:
- Java ejecuta el
trylínea a línea. - En cuanto una línea lanza una excepción, salta directamente al
catchque coincide con su tipo. El resto deltryse descarta. - Después del
catch, el programa continúa normalmente. - Si no hay ninguna excepción, el
catchse ignora.
La variable e es el objeto excepción. Sus métodos más útiles:
try {
Integer.parseInt("12a");
} catch (NumberFormatException e) {
System.out.println(e.getMessage()); // For input string: "12a"
System.out.println(e); // java.lang.NumberFormatException: For input string: "12a"
}
e.printStackTrace() imprime además la traza completa, muy útil mientras depuras.
Varios catch
Un mismo try puede tener varios catch, uno por tipo de error. Java prueba de arriba abajo y entra en el primero que encaje:
String[] entradas = {"42", "abc", null};
for (String t : entradas) {
try {
int n = Integer.parseInt(t.trim());
System.out.println("Número: " + n);
} catch (NumberFormatException e) {
System.out.println("No es un número: " + t);
} catch (NullPointerException e) {
System.out.println("No hay texto");
}
}
// Número: 42
// No es un número: abc
// No hay texto
Cuidado: pon siempre las excepciones más concretas primero. Si pones
catch (Exception e)antes quecatch (NumberFormatException e), el segundo nunca se alcanzaría y el compilador da el error exception NumberFormatException has already been caught.
Multi-catch
Cuando dos errores se tratan igual, se pueden unir con | en un solo catch:
String[] cantidades = {"10", "0", "x"};
for (String c : cantidades) {
try {
int porPersona = 100 / Integer.parseInt(c);
System.out.println("Por persona: " + porPersona);
} catch (NumberFormatException | ArithmeticException e) {
System.out.println("Dato no válido: " + e.getClass().getSimpleName());
}
}
// Por persona: 10
// Dato no válido: ArithmeticException
// Dato no válido: NumberFormatException
finally: código que se ejecuta siempre
El bloque finally va tras los catch y se ejecuta pase lo que pase: haya excepción o no, se capture o no, e incluso si el try hace return. Se usa para liberar recursos o dejar las cosas en orden:
try {
System.out.println("Abriendo caja");
int r = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("No se puede dividir entre cero");
} finally {
System.out.println("Cerrando caja");
}
// Abriendo caja
// No se puede dividir entre cero
// Cerrando caja
try-with-resources: cerrar recursos automáticamente
Ficheros, conexiones a bases de datos o conexiones de red son recursos que hay que cerrar al terminar. Antes se cerraban en un finally, lo que daba un código largo y propenso a olvidos. Desde Java 7 existe el try-with-resources: declaras el recurso entre paréntesis tras el try y Java lo cierra solo al salir del bloque, falle o no:
static String leerPrimeraLinea(String ruta) throws IOException {
try (BufferedReader lector = new BufferedReader(new FileReader(ruta))) {
return lector.readLine();
} // aquí se llama a lector.close() automáticamente
}
Funciona con cualquier clase que implemente la interfaz AutoCloseable. Se pueden abrir varios recursos separados por ; y se cierran en orden inverso:
class Conexion implements AutoCloseable {
private final String nombre;
Conexion(String nombre) {
this.nombre = nombre;
System.out.println("Abriendo " + nombre);
}
@Override
public void close() {
System.out.println("Cerrando " + nombre);
}
}
try (Conexion a = new Conexion("A"); Conexion b = new Conexion("B")) {
System.out.println("Trabajando");
}
// Abriendo A
// Abriendo B
// Trabajando
// Cerrando B
// Cerrando A
Lo usarás constantemente en ficheros y en JDBC.
throw y throws: lanzar excepciones
Hasta ahora las excepciones las lanzaba Java. Tú también puedes lanzarlas con throw cuando detectas algo incorrecto. Es la forma correcta de validar los datos que recibe un método:
public void setEdad(int edad) {
if (edad < 0) {
throw new IllegalArgumentException("La edad no puede ser negativa: " + edad);
}
this.edad = edad;
}
throw crea y lanza el objeto; el método se interrumpe en ese punto, como con un return.
throws (con s) es otra cosa: va en la cabecera del método y avisa de que ese método puede lanzar una excepción checked que no trata él mismo:
public static String cargarConfiguracion() throws IOException {
return Files.readString(Path.of("config.txt"));
}
| Palabra | Dónde va | Qué hace |
|---|---|---|
throw | Dentro del método | Lanza una excepción concreta: throw new ... |
throws | En la cabecera | Declara qué excepciones puede lanzar el método |
Propagar excepciones
Cuando un método no sabe qué hacer con un error, lo mejor es dejarlo pasar hacia arriba (propagarlo) con throws, hasta llegar a quien sí sabe tratarlo, normalmente la parte que habla con el usuario:
static void cargarDatos() throws IOException {
leerPrimeraLinea("no-existe.txt"); // no la captura: la propaga
}
public static void main(String[] args) {
try {
cargarDatos();
} catch (IOException e) {
System.out.println("No se pudo leer: " + e.getMessage());
}
}
// No se pudo leer: no-existe.txt (No such file or directory)
A veces conviene envolver una excepción en otra más significativa para tu programa. Pasa la original como segundo argumento para no perder la información (se llama causa):
try {
Integer.parseInt(cantidad);
} catch (NumberFormatException e) {
throw new IllegalStateException("Pedido con cantidad incorrecta", e);
}
// Quien la capture puede ver e.getCause():
// java.lang.NumberFormatException: For input string: "abc"
Crear tus propias excepciones
Para errores propios de tu aplicación puedes crear una clase que herede de Exception (checked) o de RuntimeException (unchecked). Por convención, su nombre termina en Exception:
public class SaldoInsuficienteException extends Exception {
private final double falta;
public SaldoInsuficienteException(String mensaje, double falta) {
super(mensaje); // guarda el mensaje para getMessage()
this.falta = falta;
}
public double getFalta() {
return falta;
}
}
La usamos en una cuenta bancaria:
public class Cuenta {
private double saldo;
public Cuenta(double saldo) { this.saldo = saldo; }
public void retirar(double importe) throws SaldoInsuficienteException {
if (importe <= 0) {
throw new IllegalArgumentException("El importe debe ser positivo: " + importe);
}
if (importe > saldo) {
throw new SaldoInsuficienteException("Saldo insuficiente", importe - saldo);
}
saldo -= importe;
}
public double getSaldo() { return saldo; }
}
Cuenta cuenta = new Cuenta(100);
try {
cuenta.retirar(30);
cuenta.retirar(120);
} catch (SaldoInsuficienteException e) {
System.out.println(e.getMessage() + ". Faltan " + e.getFalta() + " €");
}
System.out.println("Saldo: " + cuenta.getSaldo());
// Saldo insuficiente. Faltan 50.0 €
// Saldo: 70.0
¿Checked o unchecked? Una regla práctica: si quien llama puede hacer algo razonable para recuperarse (pedir otro importe, avisar al usuario), usa checked. Si el error indica un fallo de programación (un importe negativo que nunca debería llegar), usa unchecked, como hemos hecho con IllegalArgumentException.
Leer un stack trace
Cuando una excepción no se captura, Java imprime la traza de la pila (stack trace). Aprender a leerla te ahorrará horas. Mira este programa:
public class Tienda {
public static void main(String[] args) {
int[] ventas = {};
mostrarInforme(ventas);
}
static void mostrarInforme(int[] ventas) {
System.out.println("Media: " + calcularMedia(ventas));
}
static int calcularMedia(int[] ventas) {
int suma = 0;
for (int v : ventas) suma += v;
return suma / ventas.length;
}
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Tienda.calcularMedia(Tienda.java:12)
at Tienda.mostrarInforme(Tienda.java:7)
at Tienda.main(Tienda.java:4)
Se lee así:
- Primera línea: el tipo de excepción (
ArithmeticException) y el mensaje (/ by zero). Ya sabes qué ha pasado. - Primera línea
at: dónde se produjo exactamente: métodocalcularMedia, archivoTienda.java, línea 12. Empieza a buscar ahí. - Resto de líneas: el camino que llevó hasta allí, de más reciente a más antiguo:
mainllamó amostrarInforme, que llamó acalcularMedia.
Si aparece Caused by: más abajo, es la excepción original que se envolvió; suele ser la más reveladora.
Consejo: en proyectos grandes la traza incluye líneas de bibliotecas (
java.base/...). Busca la primera línea que pertenezca a tu código: casi siempre el error está ahí.
Buenas prácticas y errores frecuentes
- No dejes un
catchvacío.catch (Exception e) { }se “traga” el error y el programa sigue con datos incorrectos sin que nadie se entere. Como mínimo, muestra un mensaje o regístralo. - No captures
Exceptionpor costumbre. Captura el tipo concreto que esperas. Uncatch (Exception e)genérico esconde también fallos de programación que deberías corregir. - No uses excepciones para el flujo normal. Si puedes comprobar algo con un
if(que el divisor no sea cero), hazlo. Y unNullPointerExceptionse arregla corrigiendo el código, no rodeándolo detry. - Cierra los recursos con try-with-resources, no a mano.
- Da mensajes claros al lanzar (incluye el dato que falló) y conserva la causa al envolver:
new MiException("...", e).
Resumen
| Concepto | Para qué sirve |
|---|---|
try { } | Código que puede fallar |
catch (Tipo e) { } | Qué hacer si falla con ese tipo de excepción |
catch (A | B e) | Tratar varios tipos igual (multi-catch) |
finally { } | Código que se ejecuta siempre |
try (Recurso r = ...) { } | Cierra el recurso automáticamente |
throw new X("...") | Lanzar una excepción |
throws X | Declarar que un método puede lanzarla |
| Checked | El compilador obliga a tratarla (IOException, SQLException) |
| Unchecked | Hereda de RuntimeException; errores de programación |
| Excepción propia | class MiException extends Exception |
e.getMessage(), e.getCause() | Mensaje y excepción original |
En la siguiente lección dejarás atrás los arrays de tamaño fijo y aprenderás a usar listas que crecen solas: ArrayList y listas.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.