Java
Tutorial Java: Excepciones
Java excepciones: manejo y prevención. Aprende a manejar y prevenir excepciones en Java con ejemplos prácticos y detallados.
Aprende Java y certifícate¿Para qué sirven las excepciones?
Las excepciones permiten gestionar situaciones anómalas o errores que pueden ocurrir durante la ejecución de un programa. En lugar de permitir que un error detenga abruptamente la aplicación, las excepciones proporcionan una forma estructurada de detectar, comunicar y manejar estos problemas.
Cuando se desarrolla software, es inevitable enfrentarse a situaciones imprevistas: archivos que no existen, conexiones de red interrumpidas, divisiones por cero o intentos de acceder a posiciones inexistentes en un array.
Propósitos principales de las excepciones
- Separación de la lógica de error: Las excepciones permiten mantener el código principal limpio y enfocado en el flujo normal, mientras que la gestión de errores se maneja por separado.
// Sin excepciones (código mezclado con verificaciones de error)
public int dividir(int a, int b) {
if (b == 0) {
System.err.println("Error: División por cero");
return 0; // ¿Qué valor devolver en caso de error?
}
return a / b;
}
// Con excepciones (código más limpio)
public int dividir(int a, int b) throws ArithmeticException {
return a / b; // Java lanzará automáticamente ArithmeticException si b es 0
}
- Propagación de errores: Las excepciones pueden propagarse a través de múltiples niveles de llamadas a métodos, permitiendo que el error se maneje en el nivel más adecuado.
public void procesarDatos() throws IOException {
leerArchivo("datos.txt");
// Si leerArchivo falla, la excepción se propaga automáticamente
}
public void leerArchivo(String ruta) throws IOException {
// Código para leer archivo que puede lanzar IOException
Files.readAllLines(Path.of(ruta));
}
- Agrupación jerárquica de errores: Java organiza las excepciones en una jerarquía de clases, lo que permite capturar categorías enteras de errores o manejar tipos específicos según sea necesario.
try {
// Código que podría lanzar diferentes tipos de excepciones
procesarArchivo();
} catch (FileNotFoundException e) {
// Manejo específico para archivo no encontrado
System.err.println("El archivo no existe: " + e.getMessage());
} catch (IOException e) {
// Manejo para otros errores de E/S
System.err.println("Error de E/S: " + e.getMessage());
}
- Información detallada sobre errores: Las excepciones contienen datos sobre el error, como mensajes descriptivos, la pila de llamadas y, potencialmente, la causa raíz.
try {
int[] numeros = new int[5];
numeros[10] = 50; // Esto causará ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("Mensaje: " + e.getMessage());
System.err.println("Tipo de excepción: " + e.getClass().getName());
e.printStackTrace(); // Muestra la pila de llamadas completa
}
Ventajas del uso de excepciones
- Robustez: Las aplicaciones que manejan adecuadamente las excepciones son más resistentes a fallos y pueden continuar funcionando incluso cuando ocurren errores.
public void procesarDocumentos() {
List<String> documentos = obtenerListaDocumentos();
for (String documento : documentos) {
try {
procesarDocumento(documento);
} catch (Exception e) {
// Registrar el error pero continuar con el siguiente documento
System.err.println("Error al procesar " + documento + ": " + e.getMessage());
}
}
System.out.println("Procesamiento completado");
}
- Legibilidad: El código principal se mantiene enfocado en la lógica de negocio, sin mezclarse con verificaciones de errores constantes.
- Mantenibilidad: La centralización del manejo de errores facilita los cambios en la estrategia de gestión de errores sin modificar la lógica principal.
- Recuperación: Permite implementar estrategias de recuperación ante fallos, como reintentos o soluciones alternativas.
public void conectarServidor() {
int intentos = 0;
boolean conectado = false;
while (!conectado && intentos < 3) {
try {
// Intentar conexión
abrirConexion();
conectado = true;
System.out.println("Conexión establecida");
} catch (ConexionException e) {
intentos++;
System.out.println("Intento " + intentos + " fallido: " + e.getMessage());
if (intentos < 3) {
// Esperar antes de reintentar
try {
Thread.sleep(2000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
if (!conectado) {
System.err.println("No se pudo establecer conexión después de 3 intentos");
}
}
Cuándo utilizar excepciones
Las excepciones se utilizan principalmente en situaciones excepcionales (como su nombre indica), no para controlar el flujo normal del programa. Se recomienda su uso en los siguientes casos:
- Errores de validación que no pueden ser evitados mediante verificaciones previas.
- Problemas con recursos externos como archivos, bases de datos o servicios web.
- Violaciones de precondiciones importantes para el funcionamiento correcto de un método.
- Errores de programación como índices fuera de rango o referencias nulas.
public Usuario buscarPorId(int id) throws UsuarioNoEncontradoException {
// Buscar usuario en la base de datos
Usuario usuario = repositorio.findById(id);
if (usuario == null) {
// Situación excepcional: el ID debería existir
throw new UsuarioNoEncontradoException("No se encontró usuario con ID: " + id);
}
return usuario;
}
Las excepciones tienen un coste de rendimiento, por lo que no deben usarse para situaciones que forman parte del flujo normal de la aplicación. Por ejemplo, no se debe usar una excepción para detectar el final de un archivo si se está leyendo línea por línea, ya que llegar al final es un comportamiento esperado.
Tipos de excepciones: excepciones comprobadas y no comprobadas
En Java, las excepciones se dividen en dos categorías principales: excepciones comprobadas (checked exceptions) y excepciones no comprobadas (unchecked exceptions). Esta clasificación determina cómo el compilador trata cada tipo de excepción y cómo los desarrolladores deben manejarlas en su código.
La distinción entre estos dos tipos se basa en la jerarquía de clases de excepciones en Java. Todas las excepciones derivan de la clase Throwable
, que tiene dos subclases directas: Error
y Exception
.
Jerarquía de excepciones
Throwable
├── Error // Errores graves del sistema (unchecked)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception // Base para todas las excepciones
├── IOException // Checked exceptions
├── SQLException
├── ClassNotFoundException
├── ...
└── RuntimeException // Base para unchecked exceptions
├── NullPointerException
├── ArithmeticException
├── IndexOutOfBoundsException
└── ...
Excepciones comprobadas (Checked Exceptions)
Las excepciones comprobadas son aquellas que heredan de Exception
pero no de RuntimeException
. Se caracterizan por:
- El compilador obliga a manejarlas explícitamente mediante bloques
try-catch
o a declararlas en la firma del método conthrows
. - Representan condiciones recuperables que una aplicación debería anticipar y de las que podría recuperarse.
- Suelen estar relacionadas con factores externos al programa, como problemas de E/S, red o bases de datos.
public void leerArchivo(String ruta) throws IOException {
// Si no declaramos "throws IOException", el código no compilará
Files.readAllLines(Path.of(ruta));
}
// Alternativa: manejar la excepción con try-catch
public String leerContenidoArchivo(String ruta) {
try {
return Files.readString(Path.of(ruta));
} catch (IOException e) {
System.err.println("Error al leer el archivo: " + e.getMessage());
return ""; // Valor por defecto en caso de error
}
}
Ejemplos comunes de excepciones comprobadas:
IOException
: Problemas de entrada/salidaSQLException
: Errores en operaciones de bases de datosClassNotFoundException
: Clase no encontrada al cargar dinámicamenteParseException
: Error al analizar una cadena con formato específico
Excepciones no comprobadas (Unchecked Exceptions)
Las excepciones no comprobadas incluyen todas las que heredan de RuntimeException
y también las de tipo Error
. Sus características son:
- El compilador no obliga a capturarlas o declararlas.
- Generalmente indican errores de programación que no deberían ocurrir si el código estuviera correctamente escrito.
- Suelen ser difíciles o imposibles de recuperar en tiempo de ejecución.
public int dividir(int a, int b) {
// No es necesario declarar "throws ArithmeticException"
return a / b; // Lanzará ArithmeticException si b es 0
}
// Aunque no es obligatorio, podemos manejarla si queremos
public int dividirSeguro(int a, int b) {
try {
return a / b;
} catch (ArithmeticException e) {
System.err.println("Error: División por cero");
return 0; // Valor por defecto
}
}
Ejemplos comunes de excepciones no comprobadas:
NullPointerException
: Intento de acceder a un objeto nuloArithmeticException
: Operaciones aritméticas inválidas como división por ceroIndexOutOfBoundsException
: Acceso a índices fuera de los límites de un arrayIllegalArgumentException
: Argumentos inválidos en llamadas a métodosConcurrentModificationException
: Modificación de una colección mientras se itera sobre ella
Errores (Error)
Los errores son un tipo especial de excepciones no comprobadas que indican problemas graves a nivel del sistema:
- Representan situaciones catastróficas de las que normalmente no se puede recuperar la aplicación.
- No se espera que el código los capture o maneje.
- Suelen requerir la terminación del programa.
// Ejemplo que provoca StackOverflowError
public void causarStackOverflow() {
causarStackOverflow(); // Llamada recursiva sin condición de salida
}
// Ejemplo que puede provocar OutOfMemoryError
public void consumirMemoria() {
List<byte[]> memoria = new ArrayList<>();
while (true) {
memoria.add(new byte[1024 * 1024]); // Añadir 1MB continuamente
}
}
Ejemplos comunes de errores:
OutOfMemoryError
: La JVM se queda sin memoriaStackOverflowError
: Desbordamiento de la pila de llamadasNoClassDefFoundError
: No se encuentra la definición de una claseExceptionInInitializerError
: Excepción durante la inicialización estática
¿Cuándo usar cada tipo?
La elección entre crear excepciones comprobadas o no comprobadas depende del contexto:
- Usa excepciones comprobadas cuando:
- La condición de error es recuperable
- Quieres forzar al llamador a manejar la situación
- El error proviene de factores externos que el código no puede controlar
- Existe una acción alternativa razonable que el llamador podría tomar
public class ArchivoConfiguracion {
public static Properties cargar(String ruta) throws ConfiguracionException {
try {
Properties props = new Properties();
try (FileInputStream fis = new FileInputStream(ruta)) {
props.load(fis);
}
return props;
} catch (IOException e) {
throw new ConfiguracionException("No se pudo cargar la configuración", e);
}
}
}
// ConfiguracionException es una excepción comprobada personalizada
public class ConfiguracionException extends Exception {
public ConfiguracionException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}
- Usa excepciones no comprobadas cuando:
- El error indica un bug en el programa
- La recuperación es improbable o imposible
- Forzar el manejo sería una carga innecesaria para todos los llamadores
- El error representa una precondición violada (como argumentos inválidos)
public class CalculadoraImpuestos {
public double calcularImpuesto(double monto, double tasa) {
if (monto < 0 || tasa < 0 || tasa > 1) {
throw new IllegalArgumentException(
"Monto debe ser positivo y tasa debe estar entre 0 y 1");
}
return monto * tasa;
}
}
Buenas prácticas
- No atrapar excepciones genéricas: Evita usar
catch (Exception e)
ocatch (Throwable e)
sin una buena razón, ya que puede ocultar problemas graves.
// Mal ejemplo - atrapa todo tipo de excepciones
try {
procesarDatos();
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
// Mejor enfoque - manejo específico
try {
procesarDatos();
} catch (IOException e) {
System.err.println("Error de E/S: " + e.getMessage());
// Acción específica para errores de E/S
} catch (SQLException e) {
System.err.println("Error de base de datos: " + e.getMessage());
// Acción específica para errores de BD
}
- No ignorar excepciones: Siempre haz algo significativo cuando capturas una excepción, incluso si es solo registrarla.
// Mal ejemplo - excepción ignorada
try {
archivo.close();
} catch (IOException e) {
// No hacer nada es peligroso
}
// Mejor enfoque
try {
archivo.close();
} catch (IOException e) {
logger.warn("No se pudo cerrar el archivo correctamente", e);
// Podría no ser crítico, pero al menos lo registramos
}
- Preservar la causa original: Al lanzar una nueva excepción, incluye la original como causa.
try {
repositorio.guardarDatos(datos);
} catch (SQLException e) {
// Convertimos la excepción técnica en una de dominio, pero preservamos la causa
throw new ErrorPersistenciaException("No se pudieron guardar los datos", e);
}
- Documentar las excepciones: Usa Javadoc para documentar qué excepciones puede lanzar un método y bajo qué circunstancias.
/**
* Transfiere fondos entre dos cuentas.
*
* @param origen ID de la cuenta origen
* @param destino ID de la cuenta destino
* @param monto Cantidad a transferir
* @throws CuentaInexistenteException si alguna de las cuentas no existe
* @throws SaldoInsuficienteException si la cuenta origen no tiene fondos suficientes
* @throws MontoInvalidoException si el monto es negativo o cero
*/
public void transferir(String origen, String destino, double monto)
throws CuentaInexistenteException, SaldoInsuficienteException, MontoInvalidoException {
// Implementación
}
Manejo de excepciones
Cuando ocurre una situación excepcional durante la ejecución de un programa, se necesita un mecanismo para detectar y responder adecuadamente a estos eventos. Java proporciona una estructura clara para gestionar estas situaciones mediante bloques try-catch-finally
.
Estructura básica try-catch
La estructura más básica para manejar excepciones en Java se compone de un bloque try
seguido de uno o más bloques catch
:
try {
// Código que podría lanzar excepciones
int resultado = 10 / 0; // Esto lanzará ArithmeticException
} catch (ArithmeticException e) {
// Código que se ejecuta si ocurre una ArithmeticException
System.err.println("Error aritmético: " + e.getMessage());
}
El flujo de ejecución es el siguiente:
1. Se ejecuta el código dentro del bloque try
2. Si ocurre una excepción, la ejecución del bloque try
se interrumpe inmediatamente
3. Se busca un bloque catch
compatible con el tipo de excepción lanzada
4. Si se encuentra, se ejecuta ese bloque catch
5. La ejecución continúa después del último bloque catch
Captura de múltiples excepciones
Es posible manejar diferentes tipos de excepciones con múltiples bloques catch
:
try {
int[] numeros = new int[5];
numeros[10] = 10 / 0; // Podría lanzar dos tipos de excepciones
} catch (ArithmeticException e) {
System.err.println("Error en operación matemática: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("Índice fuera de rango: " + e.getMessage());
}
Los bloques catch
se evalúan en orden, por lo que es importante colocar las excepciones más específicas antes que las más generales:
try {
// Código que podría lanzar excepciones
procesarArchivo("datos.txt");
} catch (FileNotFoundException e) {
// Manejo específico para archivo no encontrado
System.err.println("No se encontró el archivo: " + e.getMessage());
} catch (IOException e) {
// Manejo para otros errores de E/S
System.err.println("Error de E/S: " + e.getMessage());
} catch (Exception e) {
// Captura cualquier otra excepción no manejada anteriormente
System.err.println("Error general: " + e.getMessage());
}
Multi-catch (desde Java 7)
Para reducir la duplicación de código, Java permite capturar múltiples tipos de excepciones en un solo bloque catch
:
try {
// Código que podría lanzar diferentes excepciones
Object obj = obtenerObjeto();
String texto = (String) obj;
int valor = Integer.parseInt(texto);
} catch (ClassCastException | NumberFormatException e) {
// Este bloque maneja ambos tipos de excepciones
System.err.println("Error de conversión: " + e.getMessage());
}
Las excepciones unidas con el operador |
deben ser disjuntas (ninguna puede ser subclase de otra).
El bloque finally
El bloque finally
contiene código que se ejecuta siempre, independientemente de si ocurre una excepción o no:
FileInputStream archivo = null;
try {
archivo = new FileInputStream("config.txt");
// Procesar el archivo
} catch (IOException e) {
System.err.println("Error al procesar el archivo: " + e.getMessage());
} finally {
// Este código se ejecuta siempre
if (archivo != null) {
try {
archivo.close();
} catch (IOException e) {
System.err.println("Error al cerrar el archivo");
}
}
}
El bloque finally
es ideal para:
- Liberar recursos (cerrar archivos, conexiones de red, etc.)
- Realizar operaciones de limpieza
- Asegurar que ciertas operaciones se completen independientemente de las excepciones
Try-with-resources (desde Java 7)
Para simplificar el manejo de recursos que deben cerrarse, Java introdujo la estructura try-with-resources
:
try (FileInputStream archivo = new FileInputStream("datos.txt");
BufferedReader lector = new BufferedReader(new InputStreamReader(archivo))) {
// Usar los recursos
String linea = lector.readLine();
System.out.println(linea);
} catch (IOException e) {
System.err.println("Error de E/S: " + e.getMessage());
}
// Los recursos se cierran automáticamente al finalizar el bloque try
Características importantes:
- Los recursos se cierran automáticamente al finalizar el bloque
try
- Los recursos se cierran en orden inverso a su declaración
- Se pueden declarar múltiples recursos separados por punto y coma
- Los recursos deben implementar la interfaz
AutoCloseable
Obtención de información de la excepción
Las excepciones en Java proporcionan métodos para obtener información detallada:
try {
int resultado = 10 / 0;
} catch (ArithmeticException e) {
// Mensaje de error
System.err.println("Mensaje: " + e.getMessage());
// Nombre completo de la clase de excepción
System.err.println("Tipo: " + e.getClass().getName());
// Traza completa de la pila de llamadas
e.printStackTrace();
// Causa original (si esta excepción fue causada por otra)
Throwable causa = e.getCause();
if (causa != null) {
System.err.println("Causada por: " + causa.getMessage());
}
}
Excepciones encadenadas
Es común encadenar excepciones para preservar la información de la causa original mientras se proporciona un contexto más específico:
public void guardarDatos(Usuario usuario) throws PersistenciaException {
try {
// Intentar guardar en la base de datos
conexion.ejecutar("INSERT INTO usuarios VALUES (...)");
} catch (SQLException e) {
// Convertir la excepción técnica en una de dominio
throw new PersistenciaException("Error al guardar el usuario: " + usuario.getId(), e);
}
}
El constructor de la excepción que recibe otra excepción como segundo parámetro establece la causa original, que se puede recuperar posteriormente con getCause()
.
Patrones comunes de manejo de excepciones
Patrón de reintento
Útil para operaciones que pueden fallar temporalmente, como conexiones de red:
public void conectarServidor() {
int intentos = 0;
boolean conectado = false;
while (!conectado && intentos < 3) {
try {
// Intentar conexión
socket = new Socket("servidor.ejemplo.com", 8080);
conectado = true;
System.out.println("Conexión establecida");
} catch (IOException e) {
intentos++;
System.out.println("Intento " + intentos + " fallido: " + e.getMessage());
if (intentos < 3) {
try {
// Esperar antes de reintentar
Thread.sleep(2000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
if (!conectado) {
System.err.println("No se pudo establecer conexión después de 3 intentos");
}
}
Patrón de conversión de excepciones
Convierte excepciones de bajo nivel en excepciones más significativas para la capa de aplicación:
public List<Producto> buscarProductos(String categoria) throws ServicioException {
try {
return repositorio.findByCategoria(categoria);
} catch (SQLException e) {
throw new ServicioException("Error al buscar productos de categoría: " + categoria, e);
} catch (TimeoutException e) {
throw new ServicioException("Tiempo de espera agotado en la búsqueda", e);
}
}
Patrón de registro y propagación
Registra información sobre la excepción pero permite que se propague hacia arriba:
public void procesarPedido(Pedido pedido) throws ProcesoPedidoException {
try {
validarPedido(pedido);
calcularTotal(pedido);
aplicarDescuentos(pedido);
guardarPedido(pedido);
} catch (ValidacionException e) {
logger.error("Error de validación en pedido " + pedido.getId(), e);
throw new ProcesoPedidoException("Pedido inválido", e);
} catch (CalculoException e) {
logger.error("Error en cálculos del pedido " + pedido.getId(), e);
throw new ProcesoPedidoException("Error en cálculos", e);
}
}
Buenas prácticas en el manejo de excepciones
- Ser específico con las excepciones: Captura tipos específicos de excepciones en lugar de usar
Exception
genérico.
// Enfoque incorrecto
try {
// Código
} catch (Exception e) { // Demasiado genérico
System.err.println("Error");
}
// Enfoque correcto
try {
// Código
} catch (IOException e) {
System.err.println("Error de E/S: " + e.getMessage());
} catch (SQLException e) {
System.err.println("Error de base de datos: " + e.getMessage());
}
- No ignorar excepciones: Siempre haz algo significativo cuando capturas una excepción.
// Incorrecto
try {
conexion.close();
} catch (SQLException e) {
// Vacío - ¡Peligroso!
}
// Correcto
try {
conexion.close();
} catch (SQLException e) {
logger.warn("No se pudo cerrar la conexión correctamente", e);
}
- Usar try-with-resources para recursos que deben cerrarse.
// Antiguo enfoque (propenso a errores)
InputStream entrada = null;
try {
entrada = new FileInputStream("archivo.txt");
// Usar entrada
} catch (IOException e) {
// Manejar excepción
} finally {
if (entrada != null) {
try {
entrada.close();
} catch (IOException e) {
// ¿Qué hacer aquí?
}
}
}
// Enfoque moderno con try-with-resources
try (InputStream entrada = new FileInputStream("archivo.txt")) {
// Usar entrada
} catch (IOException e) {
// Manejar excepción
}
- Mantener el ámbito del try lo más pequeño posible: Incluye solo el código que realmente puede lanzar la excepción.
// Incorrecto - ámbito demasiado amplio
try {
String datos = obtenerDatos(); // Podría lanzar IOException
procesarDatos(datos); // No lanza excepciones
guardarResultados(datos); // Podría lanzar SQLException
} catch (Exception e) {
// Difícil saber qué falló exactamente
}
// Correcto - ámbitos específicos
String datos;
try {
datos = obtenerDatos();
} catch (IOException e) {
System.err.println("Error al obtener datos: " + e.getMessage());
return;
}
procesarDatos(datos);
try {
guardarResultados(datos);
} catch (SQLException e) {
System.err.println("Error al guardar resultados: " + e.getMessage());
}
- Evitar anti-patrones comunes:
- No usar excepciones para controlar el flujo normal del programa
- No capturar
Throwable
(excepto en casos muy específicos) - No crear excepciones sin mensaje informativo
// Anti-patrón: usar excepciones para control de flujo
public boolean existeArchivo(String ruta) {
try {
new FileInputStream(ruta).close();
return true;
} catch (IOException e) {
return false;
}
}
// Mejor enfoque
public boolean existeArchivo(String ruta) {
return Files.exists(Path.of(ruta));
}
Lanzamiento de excepciones
El lanzamiento de excepciones permite señalar condiciones de error o situaciones excepcionales durante la ejecución de un programa. Cuando se detecta una situación anómala que no puede ser manejada en el contexto actual, se puede lanzar una excepción para transferir el control a un código capaz de manejarla adecuadamente.
En Java, se utiliza la palabra clave throw
para lanzar explícitamente una excepción. Este mecanismo permite interrumpir el flujo normal de ejecución y propagar información sobre el error ocurrido.
Sintaxis básica para lanzar excepciones
La sintaxis para lanzar una excepción es sencilla:
throw new TipoExcepcion("Mensaje descriptivo del error");
Por ejemplo, para lanzar una excepción cuando un argumento no cumple con ciertos requisitos:
public void transferir(double monto, Cuenta destino) {
if (monto <= 0) {
throw new IllegalArgumentException("El monto debe ser positivo");
}
if (destino == null) {
throw new NullPointerException("La cuenta destino no puede ser nula");
}
// Continuar con la lógica de transferencia
}
Declaración de excepciones con throws
Cuando un método puede lanzar excepciones comprobadas (checked), es obligatorio declararlas en la firma del método usando la cláusula throws
:
public void leerArchivo(String ruta) throws IOException {
if (!Files.exists(Path.of(ruta))) {
throw new FileNotFoundException("El archivo no existe: " + ruta);
}
// Código para leer el archivo
}
Para excepciones no comprobadas (unchecked), la declaración con throws
es opcional, pero puede ser útil como documentación:
public int dividir(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("División por cero no permitida");
}
return a / b;
}
Cuándo lanzar excepciones
Las excepciones deben lanzarse en situaciones excepcionales, no como parte del flujo normal del programa. Algunos casos apropiados para lanzar excepciones son:
- Validación de precondiciones: Cuando los parámetros de entrada no cumplen con los requisitos necesarios.
public void registrarUsuario(String nombre, String email, int edad) {
if (nombre == null || nombre.isBlank()) {
throw new IllegalArgumentException("El nombre no puede estar vacío");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Email inválido: " + email);
}
if (edad < 18) {
throw new IllegalArgumentException("El usuario debe ser mayor de edad");
}
// Continuar con el registro
}
- Estados inconsistentes: Cuando el objeto se encuentra en un estado que no permite realizar la operación solicitada.
public class CuentaBancaria {
private double saldo;
private boolean bloqueada;
public void retirar(double monto) {
if (bloqueada) {
throw new IllegalStateException("No se puede operar con una cuenta bloqueada");
}
if (monto > saldo) {
throw new SaldoInsuficienteException("Saldo insuficiente para retirar " + monto);
}
saldo -= monto;
}
}
- Errores de operación: Cuando una operación no puede completarse debido a factores externos.
public void conectarServidor(String host, int puerto) throws ConexionException {
try {
Socket socket = new Socket(host, puerto);
// Configurar la conexión
} catch (IOException e) {
throw new ConexionException("No se pudo establecer conexión con " + host + ":" + puerto, e);
}
}
Relanzamiento de excepciones
En ocasiones, es necesario capturar una excepción para realizar alguna acción (como registro o limpieza) y luego relanzarla para que sea manejada en un nivel superior:
public void procesarDatos() throws IOException {
try {
// Código que puede lanzar IOException
leerArchivo("datos.txt");
} catch (IOException e) {
// Registrar el error
logger.error("Error al procesar datos", e);
// Relanzar la misma excepción
throw e;
}
}
También se puede transformar la excepción en otra más específica o más adecuada para el contexto:
public void cargarConfiguracion() throws ConfiguracionException {
try {
Properties props = new Properties();
props.load(new FileInputStream("config.properties"));
// Procesar propiedades
} catch (IOException e) {
// Convertir la excepción técnica en una de dominio
throw new ConfiguracionException("Error al cargar archivo de configuración", e);
}
}
Enriquecimiento de excepciones
Al lanzar excepciones, es importante proporcionar información contextual que ayude a diagnosticar y resolver el problema:
public Usuario buscarPorId(int id) throws UsuarioException {
try {
return repositorio.findById(id);
} catch (SQLException e) {
throw new UsuarioException(
String.format("Error al buscar usuario con ID %d en la base de datos %s",
id, repositorio.getNombreBaseDatos()),
e
);
}
}
Un buen mensaje de excepción debe incluir:
- Qué ocurrió
- Dónde ocurrió
- Por qué ocurrió (si es posible)
- Valores relevantes que ayuden a diagnosticar el problema
Excepciones personalizadas con información adicional
Se pueden crear excepciones personalizadas que contengan información adicional relevante para el problema:
public class ValidacionException extends Exception {
private final String campo;
private final Object valorRechazado;
public ValidacionException(String mensaje, String campo, Object valorRechazado) {
super(mensaje);
this.campo = campo;
this.valorRechazado = valorRechazado;
}
public String getCampo() {
return campo;
}
public Object getValorRechazado() {
return valorRechazado;
}
}
// Uso
public void validarProducto(Producto producto) throws ValidacionException {
if (producto.getPrecio() < 0) {
throw new ValidacionException(
"El precio no puede ser negativo",
"precio",
producto.getPrecio()
);
}
}
Patrones comunes para lanzar excepciones
- Validación temprana: Validar todos los parámetros al inicio del método antes de realizar cualquier operación.
public void procesarPedido(Pedido pedido, Cliente cliente) {
// Validación temprana
if (pedido == null) {
throw new IllegalArgumentException("El pedido no puede ser nulo");
}
if (cliente == null) {
throw new IllegalArgumentException("El cliente no puede ser nulo");
}
if (pedido.getItems().isEmpty()) {
throw new PedidoVacioException("El pedido no contiene ítems");
}
// Continuar con el procesamiento
}
- Excepciones específicas para el dominio: Crear jerarquías de excepciones que reflejen el modelo de dominio de la aplicación.
// Jerarquía de excepciones
public class NegocioException extends Exception { /* ... */ }
public class ClienteException extends NegocioException { /* ... */ }
public class ProductoException extends NegocioException { /* ... */ }
public class InventarioException extends ProductoException { /* ... */ }
// Uso
public void venderProducto(String codigoProducto, int cantidad)
throws ProductoException, InventarioException, ClienteException {
if (!existeProducto(codigoProducto)) {
throw new ProductoException("Producto no encontrado: " + codigoProducto);
}
if (getStock(codigoProducto) < cantidad) {
throw new InventarioException("Stock insuficiente para " + codigoProducto);
}
// Continuar con la venta
}
- Excepciones con códigos de error: Incluir códigos de error estandarizados para facilitar el manejo automatizado.
public class APIException extends Exception {
private final String codigo;
public APIException(String codigo, String mensaje) {
super(mensaje);
this.codigo = codigo;
}
public String getCodigo() {
return codigo;
}
}
// Uso
if (respuesta.getEstado() != 200) {
throw new APIException(
"API-" + respuesta.getEstado(),
"Error en llamada a API: " + respuesta.getMensajeError()
);
}
Creación y lanzamiento y captura de excepciones propias
Cuando las excepciones estándar no describen adecuadamente los problemas específicos de nuestro dominio, crear nuestras propias clases de excepción permite comunicar errores de manera más precisa y significativa.
Fundamentos de excepciones personalizadas
Las excepciones personalizadas son clases que extienden de alguna clase base de excepción. Dependiendo de nuestras necesidades, podemos crear:
- Excepciones comprobadas: Extendiendo de
Exception
o alguna de sus subclases - Excepciones no comprobadas: Extendiendo de
RuntimeException
o alguna de sus subclases
// Excepción comprobada personalizada
public class ConfiguracionException extends Exception {
public ConfiguracionException(String mensaje) {
super(mensaje);
}
}
// Excepción no comprobada personalizada
public class UsuarioNoAutorizadoException extends RuntimeException {
public UsuarioNoAutorizadoException(String mensaje) {
super(mensaje);
}
}
Estructura de una excepción personalizada
Una excepción personalizada bien diseñada suele incluir:
- Constructores adecuados para diferentes escenarios
- Campos adicionales que proporcionen contexto específico
- Métodos de acceso para obtener información detallada del error
public class ValidacionException extends Exception {
private final String campo;
private final String codigoError;
// Constructor básico
public ValidacionException(String mensaje) {
super(mensaje);
this.campo = null;
this.codigoError = "VALIDACION_ERROR";
}
// Constructor con campo específico
public ValidacionException(String mensaje, String campo) {
super(mensaje);
this.campo = campo;
this.codigoError = "VALIDACION_ERROR";
}
// Constructor con causa original
public ValidacionException(String mensaje, String campo, Throwable causa) {
super(mensaje, causa);
this.campo = campo;
this.codigoError = "VALIDACION_ERROR";
}
// Constructor completo
public ValidacionException(String mensaje, String campo, String codigoError, Throwable causa) {
super(mensaje, causa);
this.campo = campo;
this.codigoError = codigoError;
}
// Métodos de acceso
public String getCampo() {
return campo;
}
public String getCodigoError() {
return codigoError;
}
// Método para generar mensaje detallado
@Override
public String toString() {
return "ValidacionException [código=" + codigoError +
", campo=" + campo + ", mensaje=" + getMessage() + "]";
}
}
Jerarquías de excepciones personalizadas
Para aplicaciones complejas, es recomendable crear una jerarquía de excepciones que refleje la estructura del dominio:
// Excepción base para toda la aplicación
public abstract class AplicacionException extends Exception {
public AplicacionException(String mensaje) {
super(mensaje);
}
public AplicacionException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}
// Excepciones específicas por módulo
public class SeguridadException extends AplicacionException {
public SeguridadException(String mensaje) {
super(mensaje);
}
}
public class PersistenciaException extends AplicacionException {
public PersistenciaException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}
// Excepciones aún más específicas
public class AutenticacionException extends SeguridadException {
private final String usuario;
public AutenticacionException(String mensaje, String usuario) {
super(mensaje);
this.usuario = usuario;
}
public String getUsuario() {
return usuario;
}
}
Esta estructura jerárquica permite capturar excepciones a diferentes niveles de granularidad según sea necesario.
Lanzamiento de excepciones personalizadas
El lanzamiento de excepciones personalizadas sigue el mismo patrón que las excepciones estándar:
public void validarEdad(int edad) throws ValidacionException {
if (edad < 0) {
throw new ValidacionException("La edad no puede ser negativa", "edad");
}
if (edad > 120) {
throw new ValidacionException("La edad parece no ser válida", "edad", "EDAD_INVALIDA", null);
}
}
Para excepciones que encapsulan otras, es importante preservar la causa original:
public void guardarUsuario(Usuario usuario) throws PersistenciaException {
try {
// Intentar guardar en base de datos
repositorio.save(usuario);
} catch (SQLException e) {
// Convertir la excepción técnica en una de dominio
throw new PersistenciaException(
"Error al guardar el usuario: " + usuario.getNombre(), e);
}
}
Captura y manejo de excepciones personalizadas
La captura de excepciones personalizadas se realiza igual que con las excepciones estándar:
try {
servicio.procesarSolicitud(solicitud);
} catch (ValidacionException e) {
System.err.println("Error de validación en el campo: " + e.getCampo());
System.err.println("Código de error: " + e.getCodigoError());
System.err.println("Mensaje: " + e.getMessage());
} catch (AutenticacionException e) {
System.err.println("Error de autenticación para el usuario: " + e.getUsuario());
System.err.println("Mensaje: " + e.getMessage());
} catch (AplicacionException e) {
// Captura cualquier otra excepción de la aplicación
System.err.println("Error general de la aplicación: " + e.getMessage());
}
La ventaja de las excepciones personalizadas es que podemos extraer información específica del contexto para manejar el error de manera más precisa.
Ejemplo práctico: Sistema de gestión bancaria
Veamos un ejemplo completo de creación, lanzamiento y captura de excepciones personalizadas en un sistema bancario:
// Jerarquía de excepciones
public class BancoException extends Exception {
public BancoException(String mensaje) {
super(mensaje);
}
public BancoException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}
public class CuentaException extends BancoException {
private final String numeroCuenta;
public CuentaException(String mensaje, String numeroCuenta) {
super(mensaje);
this.numeroCuenta = numeroCuenta;
}
public String getNumeroCuenta() {
return numeroCuenta;
}
}
public class SaldoInsuficienteException extends CuentaException {
private final double saldoActual;
private final double montoSolicitado;
public SaldoInsuficienteException(String numeroCuenta, double saldoActual, double montoSolicitado) {
super("Saldo insuficiente en la cuenta", numeroCuenta);
this.saldoActual = saldoActual;
this.montoSolicitado = montoSolicitado;
}
public double getSaldoActual() {
return saldoActual;
}
public double getMontoSolicitado() {
return montoSolicitado;
}
public double getSaldoFaltante() {
return montoSolicitado - saldoActual;
}
}
public class CuentaBloqueadaException extends CuentaException {
private final String motivo;
public CuentaBloqueadaException(String numeroCuenta, String motivo) {
super("La cuenta está bloqueada", numeroCuenta);
this.motivo = motivo;
}
public String getMotivo() {
return motivo;
}
}
Ahora, implementemos la clase CuentaBancaria
que utiliza estas excepciones:
public class CuentaBancaria {
private String numeroCuenta;
private double saldo;
private boolean bloqueada;
private String motivoBloqueo;
// Constructor y otros métodos...
public void retirar(double monto) throws CuentaException {
// Validar que la cuenta no esté bloqueada
if (bloqueada) {
throw new CuentaBloqueadaException(numeroCuenta, motivoBloqueo);
}
// Validar que el monto sea positivo
if (monto <= 0) {
throw new CuentaException("El monto a retirar debe ser positivo", numeroCuenta);
}
// Validar que haya saldo suficiente
if (saldo < monto) {
throw new SaldoInsuficienteException(numeroCuenta, saldo, monto);
}
// Realizar el retiro
saldo -= monto;
System.out.println("Retiro exitoso. Nuevo saldo: " + saldo);
}
public void transferir(CuentaBancaria destino, double monto) throws CuentaException {
try {
// Intentar retirar de esta cuenta
this.retirar(monto);
// Depositar en la cuenta destino
try {
destino.depositar(monto);
} catch (Exception e) {
// Si falla el depósito, revertir el retiro
this.depositar(monto);
throw new CuentaException(
"Error al depositar en cuenta destino. Transferencia cancelada.",
numeroCuenta);
}
System.out.println("Transferencia exitosa de " + monto +
" desde " + this.numeroCuenta +
" hacia " + destino.numeroCuenta);
} catch (SaldoInsuficienteException e) {
throw new CuentaException(
"Fondos insuficientes para realizar la transferencia. " +
"Saldo actual: " + e.getSaldoActual() +
", Monto solicitado: " + e.getMontoSolicitado(),
numeroCuenta);
}
}
public void depositar(double monto) throws CuentaException {
if (bloqueada) {
throw new CuentaBloqueadaException(numeroCuenta, motivoBloqueo);
}
if (monto <= 0) {
throw new CuentaException("El monto a depositar debe ser positivo", numeroCuenta);
}
saldo += monto;
System.out.println("Depósito exitoso. Nuevo saldo: " + saldo);
}
}
Finalmente, veamos cómo se utilizarían estas excepciones en un cliente:
public class SistemaBancario {
public static void main(String[] args) {
CuentaBancaria cuenta1 = new CuentaBancaria("123456", 1000.0, false, null);
CuentaBancaria cuenta2 = new CuentaBancaria("789012", 500.0, false, null);
CuentaBancaria cuenta3 = new CuentaBancaria("345678", 200.0, true, "Verificación pendiente");
// Caso 1: Retiro exitoso
try {
cuenta1.retirar(500.0);
} catch (CuentaException e) {
System.err.println("Error: " + e.getMessage());
}
// Caso 2: Saldo insuficiente
try {
cuenta2.retirar(1000.0);
} catch (SaldoInsuficienteException e) {
System.err.println("Error en cuenta " + e.getNumeroCuenta() + ": " + e.getMessage());
System.err.println("Saldo actual: " + e.getSaldoActual());
System.err.println("Monto solicitado: " + e.getMontoSolicitado());
System.err.println("Saldo faltante: " + e.getSaldoFaltante());
} catch (CuentaException e) {
System.err.println("Error general: " + e.getMessage());
}
// Caso 3: Cuenta bloqueada
try {
cuenta3.depositar(100.0);
} catch (CuentaBloqueadaException e) {
System.err.println("Error en cuenta " + e.getNumeroCuenta() + ": " + e.getMessage());
System.err.println("Motivo de bloqueo: " + e.getMotivo());
} catch (CuentaException e) {
System.err.println("Error general: " + e.getMessage());
}
// Caso 4: Transferencia
try {
cuenta1.transferir(cuenta2, 200.0);
} catch (CuentaException e) {
System.err.println("Error en transferencia: " + e.getMessage());
}
}
}
Patrones avanzados con excepciones personalizadas
Excepciones con información de recuperación
Podemos diseñar excepciones que no solo informen del error, sino que también sugieran cómo recuperarse:
public class LimiteExcedidoException extends BancoException {
private final double limiteActual;
private final double limiteRecomendado;
public LimiteExcedidoException(double actual, double recomendado) {
super("Límite de operaciones excedido");
this.limiteActual = actual;
this.limiteRecomendado = recomendado;
}
public double getLimiteActual() {
return limiteActual;
}
public double getLimiteRecomendado() {
return limiteRecomendado;
}
public boolean puedeAumentarLimite() {
return limiteRecomendado > limiteActual;
}
}
// Uso
try {
realizarOperacion(monto);
} catch (LimiteExcedidoException e) {
if (e.puedeAumentarLimite()) {
System.out.println("¿Desea aumentar su límite a " + e.getLimiteRecomendado() + "?");
// Lógica para aumentar el límite
} else {
System.out.println("Ha alcanzado el límite máximo permitido.");
}
}
Excepciones con múltiples errores
Para validaciones que pueden generar múltiples errores a la vez:
public class ValidacionMultipleException extends BancoException {
private final List<String> errores;
public ValidacionMultipleException(List<String> errores) {
super("Se encontraron múltiples errores de validación");
this.errores = new ArrayList<>(errores);
}
public List<String> getErrores() {
return Collections.unmodifiableList(errores);
}
public int getCantidadErrores() {
return errores.size();
}
}
// Uso
public void validarFormulario(Formulario form) throws ValidacionMultipleException {
List<String> errores = new ArrayList<>();
if (form.getNombre() == null || form.getNombre().isBlank()) {
errores.add("El nombre es obligatorio");
}
if (form.getEmail() == null || !form.getEmail().contains("@")) {
errores.add("El email no es válido");
}
if (form.getEdad() < 18) {
errores.add("Debe ser mayor de edad");
}
if (!errores.isEmpty()) {
throw new ValidacionMultipleException(errores);
}
}
// Captura
try {
validarFormulario(formulario);
} catch (ValidacionMultipleException e) {
System.err.println("Se encontraron " + e.getCantidadErrores() + " errores:");
for (String error : e.getErrores()) {
System.err.println("- " + error);
}
}
Buenas prácticas para excepciones personalizadas
- Nombres descriptivos: El nombre de la excepción debe describir claramente el problema (
SaldoInsuficienteException
en lugar deCuentaException
). - Mensajes informativos: Incluye detalles específicos en los mensajes de error.
// Poco informativo
throw new PagoException("Error en el pago");
// Informativo
throw new PagoException("Error en el pago con tarjeta terminada en 4321: Fondos insuficientes");
- Serialización: Las excepciones personalizadas deben ser serializables para funcionar correctamente en entornos distribuidos.
public class MiExcepcion extends Exception implements Serializable {
private static final long serialVersionUID = 1L;
// Resto de la implementación...
}
- Documentación clara: Documenta tus excepciones personalizadas con Javadoc para facilitar su uso.
/**
* Excepción lanzada cuando una operación excede el límite diario establecido.
*
* <p>Esta excepción contiene información sobre el límite actual y el monto
* que se intentó operar, permitiendo a los manejadores mostrar información
* precisa al usuario.</p>
*/
public class LimiteDiarioExcedidoException extends OperacionException {
// Implementación...
}
- Inmutabilidad: Diseña tus excepciones como objetos inmutables para evitar comportamientos inesperados.
public final class DatoInvalidoException extends Exception {
private final String campo;
private final String valor;
public DatoInvalidoException(String campo, String valor, String mensaje) {
super(mensaje);
this.campo = campo;
this.valor = valor;
}
// Solo getters, sin setters
public String getCampo() {
return campo;
}
public String getValor() {
return valor;
}
}
Consideraciones de rendimiento
Las excepciones personalizadas, como cualquier excepción, tienen un coste de rendimiento asociado. Algunas consideraciones importantes:
- Creación de la pila de llamadas: La construcción de la pila de llamadas (stack trace) es costosa.
- Uso adecuado: Utiliza excepciones para situaciones realmente excepcionales, no para control de flujo normal.
// Ineficiente: usar excepciones para control de flujo
public boolean existeUsuario(String id) {
try {
buscarUsuarioPorId(id);
return true;
} catch (UsuarioNoEncontradoException e) {
return false;
}
}
// Eficiente: método específico para verificación
public boolean existeUsuario(String id) {
return repositorioUsuarios.existsById(id);
}
- Optimización de stack trace: Para excepciones frecuentes en código crítico, se puede considerar omitir la pila de llamadas.
public class ExcepcionOptimizada extends RuntimeException {
public ExcepcionOptimizada(String mensaje) {
super(mensaje, null, true, false); // Suprime la pila de llamadas
}
}
Ejercicios de esta lección Excepciones
Evalúa tus conocimientos de esta lección Excepciones con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases abstractas
Streams: reduce()
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
CRUD en Java de modelo Customer sobre un ArrayList
Tipos de variables
Streams: collect()
Operadores aritméticos
Interfaz funcional Consumer
API java.nio 2
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Creación de Streams
Streams: min max
Métodos avanzados de la clase String
Polimorfismo de tiempo de compilación
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
CRUD en Java de modelo Customer sobre un HashMap
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Instalación
Funciones
Estructuras de control
Herencia de clases
Streams: map()
Funciones y encapsulamiento
Streams: match
Gestión de errores y excepciones
Datos primitivos
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Ecosistema Jakarta Ee De Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Api Java.nio 2
Entrada Y Salida (Io)
Api Java.time
Api Java.time
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Entender qué son las excepciones y por qué se utilizan en Java
- Aprender sobre los diferentes tipos de excepciones en Java: checked exceptions, unchecked exceptions y errors
- Familiarizarse con el manejo de excepciones mediante el uso de bloques
try/catch/finally
- Aprender cómo lanzar excepciones de forma explícita utilizando la palabra clave
throw
- Conocer cómo se pueden crear excepciones personalizadas al extender la clase
Exception
- Entender la importancia del manejo de excepciones en la creación de software robusto y de alta calidad