Java es casi el único lenguaje que distingue entre excepciones que el compilador te obliga a tratar y las que no. Esta lección explica cuál usar, cómo cerrar recursos sin acordarte, y cómo dejar de devolver null.
Dos familias de excepciones
Esta división es propia de Java y no la tiene casi ningún otro lenguaje. Entenderla evita discusiones estériles:
| Comprobadas | No comprobadas | |
|---|---|---|
| Heredan de | Exception |
RuntimeException |
| El compilador | te obliga a tratarlas o declararlas | no dice nada |
| Representan | algo que puede salir mal y se puede prever | un error de programación |
| Ejemplos | IOException, SQLException |
NullPointerException, NumberFormatException |
class SaldoInsuficiente extends Exception { // comprobada: obliga a tratarla
private final int faltan;
SaldoInsuficiente(int faltan) { super("faltan " + faltan + " €"); this.faltan = faltan; }
int faltan() { return faltan; }
}
void retirar(int saldo, int cantidad) throws SaldoInsuficiente {
if (cantidad > saldo) throw new SaldoInsuficiente(cantidad - saldo);
}
salidacomprobada: faltan 30 € (faltan 30) no comprobada: For input string: "hola"
Fíjate en que la excepción propia lleva un dato, el faltan(). Eso es lo que distingue una excepción útil de un mensaje de texto: quien la recibe puede decidir con ella, no solo imprimirla.
catch vacíos, que es el peor resultado posible.try, catch, finally
try {
return "vale " + Integer.parseInt(s);
} catch (NumberFormatException e) {
return "no es un número";
} catch (RuntimeException e) {
return "otro problema: " + e.getClass().getSimpleName();
} finally {
// se ejecuta siempre, incluso tras el return
}
salidavale 7 no es un número no es un número
Dos reglas sobre los catch: van de lo más concreto a lo más general —si pones RuntimeException primero, el de abajo no compila por inalcanzable—, y varios tipos se agrupan con |: catch (IOException | SQLException e).
La tercera línea de la salida tiene miga: probar(null) no da NullPointerException, da «no es un número». Resulta que Integer.parseInt(null) lanza NumberFormatException, no NPE. No es lo que uno supondría, y es justo el tipo de cosa que solo se descubre ejecutándolo.
try-with-resources
Todo lo que haya que cerrar —archivos, conexiones, sockets— se abre así:
try (BufferedReader r = Files.newBufferedReader(p)) {
IO.println("primera línea: " + r.readLine());
} // se cierra solo, pase lo que pase
salidaprimera línea: una línea el archivo existe todavía: true
Lo que va entre paréntesis se cierra automáticamente al salir del bloque, haya excepción o no. Es la versión corta y correcta del viejo finally { if (r != null) r.close(); }, que además tenía la pega de que un fallo al cerrar podía tapar la excepción original. Vale para cualquier objeto que implemente AutoCloseable, incluidos los tuyos.
Envolver sin perder la pista
try { Integer.parseInt("x"); }
catch (NumberFormatException e) { throw new IllegalStateException("configuración inválida", e); }
salidaconfiguración inválida <- causado por: java.lang.NumberFormatException: For input string: "x"
Al relanzar una excepción con otra más adecuada al nivel en el que estás, pásale siempre la original como segundo argumento. Eso la guarda como «causa» y hace que aparezca en la traza con un Caused by:. Sin eso, el error real desaparece y quien lo depure tendrá un mensaje genérico y ninguna pista.
catch vacío, o uno que solo imprima. Tragarse una excepción no hace que el problema desaparezca: lo esconde hasta que reaparece más lejos y más raro. Si de verdad no hay nada que hacer, lo honesto es dejarla subir.Optional: dejar de devolver null
Un método que a veces no tiene nada que devolver tenía dos malas opciones: devolver null —y confiar en que quien llama lo compruebe— o lanzar una excepción por algo que no es excepcional. Optional es la tercera:
Optional<Usuario> buscar(String nombre) {
return nombre.equals("ana") ? Optional.of(new Usuario("ana","ana@ejemplo.com"))
: Optional.empty();
}
salidaOptional[Usuario[nombre=ana, email=ana@ejemplo.com]] Optional.empty
La firma del método ya avisa de que puede no haber nada, y el compilador no deja usar el valor sin desenvolverlo. Las formas de sacarlo:
buscar("nadie").map(Usuario::email).orElse("(sin correo)");
buscar("nadie").map(Usuario::email).orElseGet(() -> calcularCaro());
buscar("ana").ifPresentOrElse(u -> ..., () -> ...);
buscar("nadie").orElseThrow(() -> new NoSuchElementException("no hay usuario"));
salidaorElse : (sin correo) orElseGet : calculado ifPresentOrElse: encontrado ana filter : false orElseThrow : no hay usuario get() a secas : No value present
La diferencia entre orElse y orElseGet importa: el argumento de orElse se evalúa siempre, haya valor o no. Si es caro de calcular o tiene efectos, usa orElseGet, que solo lo ejecuta cuando hace falta.
Y get() a secas es la forma de volver al problema original: lanza si está vacío, igual que un NPE pero con otro nombre. Si lo estás escribiendo, casi siempre quieres orElseThrow con un mensaje que explique algo.
Lo que lo hace valer la pena
String dominio = buscar("ana")
.map(Usuario::email)
.filter(e -> e.contains("@"))
.map(e -> e.substring(e.indexOf('@') + 1))
.map(String::toUpperCase)
.orElse("desconocido");
salidaEJEMPLO.COM
Cuatro pasos encadenados y ni una sola comprobación de nulo. Si en cualquier punto no hay valor, el resto se salta solo y acaba en «desconocido». Con null, esas cinco líneas serían cuatro if anidados.
Optional está pensado para devolver. No para campos de una clase, ni para parámetros —ahí complica más de lo que arregla—, ni para colecciones: una lista vacía ya dice «no hay nada», y un Optional<List> solo añade un caso más que tratar.
Lo esencial
| Situación | Qué se hace |
|---|---|
| Algo que el que llama puede resolver | una excepción comprobada, con datos dentro |
| Un fallo de programación | IllegalArgumentException, IllegalStateException |
| Hay que cerrar un recurso | try (var r = ...) { } |
| Relanzar con otro tipo | pasar la original como causa |
| No hay nada que hacer con el error | dejarlo subir, nunca un catch vacío |
| El método a veces no devuelve nada | Optional<T> |
| Sacar el valor con alternativa | orElse; orElseGet si es cara |
Todo el código de esta lección se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicarla; las salidas y los errores del compilador están copiados de esa ejecución.