Las excepciones son eventos anómalos que ocurren durante la ejecución de un programa y rompen el flujo normal de las instrucciones. Java proporciona un mecanismo orientado a objetos para capturar y gestionar estos errores de forma controlada.
Todas las excepciones derivan de la clase base java.lang.Throwable, que se divide en dos ramas principales:
Error: Fallos graves e irrecuperables a nivel de entorno o Máquina Virtual (ej. OutOfMemoryError, StackOverflowError). La aplicación no debe intentar capturarlos.Exception: Errores que la aplicación puede (y debe) gestionar:Checked Exceptions): Se verifican en tiempo de compilación. El compilador obliga a gestionarlas explícitamente mediante try-catch o a declararlas en la firma del método con throws (ej. IOException, FileNotFoundException). Heredan de ExceptionUnchecked / RuntimeExceptions): Ocurren en tiempo de ejecución y heredan de RuntimeException. Suelen indicar errores de programación o lógica (ej. NullPointerException, IllegalArgumentException).
La estrategia para gestionar una excepción varía fundamentalmente según si se trata de una excepción verificada (Checked) o no verificada (Unchecked).
Dado que el compilador verifica la presencia de estas excepciones, la sintaxis de Java obliga a tomar una de las siguientes dos decisiones ante cualquier instrucción que pueda lanzarlas:
try-catch): El método asume la responsabilidad de gestionar el fallo localmente en el momento en que ocurre.throws): El método delega la responsabilidad al nivel superior en la cadena de llamadas, indicando en su firma que puede lanzar dicha excepción.
public class GestorConfiguracion {
// OPCIÓN A: Captura local mediante try-catch
public void cargarDesdeFicheroLocal() {
try {
// FileReader lanza FileNotFoundException (Checked Exception)
FileReader reader = new FileReader("config.json");
} catch (FileNotFoundException e) {
IO.println("Fichero no encontrado. Cargando valores por defecto...");
}
}
// OPCIÓN B: Propagación hacia el llamador mediante 'throws'
public void cargarDesdeFicheroDelegado() throws FileNotFoundException {
// Se traslada la obligación de tratamiento al método que invoque a 'cargarDesdeFicheroDelegado'
FileReader reader = new FileReader("config.json");
}
}
Al derivar de RuntimeException, el compilador no exige el uso de try-catch ni la declaración con throws. Aunque la sintaxis permite capturarlas, la buena práctica establece que las Unchecked Exceptions no deben enmascararse con try-catch, sino prevenirse mediante validaciones previas (programación defensiva).
// MALA PRÁCTICA: Usar try-catch para tapar un error de lógica del programador
public void mostrarLongitudIncorrecto(String texto) {
try {
IO.println("Longitud: " + texto.length()); // Si texto es null, lanza NullPointerException
} catch (NullPointerException e) {
IO.println("Error: La cadena recibida era nula.");
}
}
// BUENA PRÁCTICA: Programación defensiva (validación explícita de condiciones)
public void mostrarLongitudCorrecto(String texto) {
if (texto != null) {
IO.println("Longitud: " + texto.length());
} else {
IO.println("Aviso: No se puede procesar una cadena nula.");
}
}
| Tipo de Excepción | Control del Compilador | Estrategia Recomendada |
|---|---|---|
| Checked Exception | Obligatorio | Usar try-catch para recuperarse localmente o throws para delegar la gestión a capas superiores. |
| Unchecked Exception | Opcional | Evitar su aparición mediante validaciones preliminares (if). Evitar el abuso de try-catch. |
@ControllerAdvice).
Por este motivo, la arquitectura moderna favorece crear excepciones personalizadas que hereden de RuntimeException (Unchecked). De este modo, evitamos llenar las clases de negocio y servicios con bloques try-catch repetitivos o firmas de métodos sobrecargadas con throws. La excepción se interrumpe de forma limpia, vuela hasta el manejador global, y este se encarga automáticamente de capturarla, registrar el log y devolver la respuesta HTTP adecuada (por ejemplo, un código 400 Bad Request o 404 Not Found en formato JSON) al cliente web.
Para capturar y gestionar las excepciones en tiempo de ejecución, Java proporciona una estructura de bloques bien definida.
try: Delimita el bloque de código que contiene operaciones susceptibles de lanzar una excepción. Si ocurre un fallo, la ejecución del bloque se interrumpe inmediatamente en la línea del error.catch: Captura la excepción producida en el bloque try. Se pueden encadenar múltiples bloques catch para gestionar distintos tipos de excepciones de forma específica. Regla de orden: deben colocarse siempre desde la excepción más específica (hija) hasta la más genérica (padre).finally: Bloque de ejecución garantizada. El código dentro de finally se ejecutará siempre, independientemente de si se lanzó una excepción, si esta fue capturada o si el bloque try ejecutó un return. Históricamente se utilizaba para liberar recursos críticos (cerrar ficheros, desconectar bases de datos o liberar sockets).
public class GestionTradicional {
public void leerFicheroClasico() {
FileReader reader = null;
try {
reader = new FileReader("datos.txt");
int caracter = reader.read();
IO.println("Primer carácter: " + (char) caracter);
} catch (IOException e) {
// Captura de errores de Entrada/Salida
IO.println("Error al procesar el archivo: " + e.getMessage());
} finally {
// Limpieza manual obligatoria: Garantizamos el cierre del recurso
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
IO.println("Error al cerrar el lector.");
}
}
IO.println("Operación finalizada (bloque finally ejecutado).");
}
}
}
La gestión manual de cierre de recursos en el bloque finally genera un código redundante, propenso a errores y difícil de leer. Para solucionarlo, Java introduce el bloque try-with-resources.
Cualquier clase que implemente la interfaz java.lang.AutoCloseable (o java.io.Closeable) puede instanciarse dentro de los paréntesis del try. Java se encargará de invocar automáticamente su método close() al finalizar el bloque, se haya producido una excepción o no.
Además de capturar las excepciones lanzadas por el propio API de Java, podemos provocar nuestras propias excepciones o crear tipos de error específicos para nuestro dominio de negocio.
throw (Acción): Se utiliza dentro del cuerpo de un método para lanzar explícitamente una instancia de excepción (throw new MiExcepcion(“Detalle…”);). Detiene inmediatamente la ejecución normal del método.throws (Declaración): Se añade en la firma del método para avisar a los invocadores de que este método puede propagar una Checked Exception que no ha sido capturada internamente.
Crear clases de excepción propias permite dar nombres significativos a los fallos de nuestra aplicación (ej. UsuarioNoEncontradoException, SaldoInsuficienteException).
Para crearlas, basta con extender de Exception (si queremos que sea verificada) o de RuntimeException (si queremos que sea no verificada, opción recomendada en arquitectura moderna).
public class ConfigurationException extends RuntimeException {
// Constructor básico con mensaje
public ConfigurationException(String mensaje) {
super(mensaje);
}
// Constructor con mensaje y causa (Exception Chaining)
// Permite envolver una excepción de bajo nivel dentro de una de nuestro dominio
public ConfigurationException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}
public void validarClave(String clave) {
if (clave == null || clave.isEmpty()) {
// Lanzamiento explícito mediante 'throw'
throw new ConfigurationException("La clave de configuración no puede estar vacía.");
}
}
public void procesarFichero(String ruta) {
try {
FileReader fr = new FileReader(ruta);
} catch (IOException e) {
// Encadenamiento de excepciones (Exception Chaining):
// Capturamos una IOException de bajo nivel y lanzamos nuestra excepción de dominio preservando la causa 'e'
throw new ConfigurationException("Error al cargar la configuración desde " + ruta, e);
}
}
super(mensaje, causa);). De este modo, no se perderá la traza original del problema cuando se registre en el archivo de log.
En las aplicaciones web modernas y arquitecturas de microservicios, la lectura de ficheros se centra en la carga de archivos de configuración y el intercambio de datos. Para ello se utilizan formatos estructurados estandarizados: JSON, YAML y XML.
| Formato | Sintaxis Base | Casos de Uso Principales | Ventajas |
|---|---|---|---|
| JSON | Clave-Valor, Arrays {} [] | APIS REST, intercambio de datos web, configs simples | Ligero, nativo en la web, fácil parseo |
| YAML | Indentación por espacios | Configuración de aplicaciones (Spring Boot, Docker, K8s) | Altamente legible por humanos, admite comentarios |
| XML | Etiquetas anidadas <tag></tag> | Configuración empresarial (Log4j2, Maven pom.xml), SOAP | Validación estricta con esquemas (XSD), estándares robustos |
El proceso de convertir un fichero en cualquiera de estos formatos a objetos Java de nuestro dominio se denomina deserialización (y la operación inversa, serialización).
Durante este proceso, es donde ocurren la mayoría de las excepciones de entrada/salida y parseo que la aplicación debe controlar:
FileNotFoundException / IOException: El archivo no existe en la ruta indicada, el nombre está mal escrito o el proceso no tiene permisos de lectura.NumberFormatException): Ocurre cuando la aplicación espera leer un parámetro numérico (ej. un puerto o número de conexiones) pero en el fichero se ha escrito un texto no válido.
Para leer archivos de configuración sin necesidad de librerías externas ni mapeadores complejos, Java proporciona la clase nativa java.util.Properties. Podemos diseñar una clase de utilidad para realizar esta lectura de forma limpia:
public class ConfigurationLoader {
/**
* Carga un archivo de propiedades y valida sus valores clave.
* @param filePath Ruta relativa o absoluta del archivo .properties
* @return Objeto Properties cargado, o null si ocurrió un error crítico
*/
public Properties loadSettings(String filePath) {
Properties config = new Properties();
// Uso de try-with-resources para garantizar el cierre del fichero
try (FileInputStream file = new FileInputStream(filePath)) {
// Carga el contenido del fichero en el objeto Properties
config.load(file);
// Lectura y conversión explícita con control de errores de tipo
String url = config.getProperty("db.url");
int maxConnections = Integer.parseInt(config.getProperty("db.max_connections"));
IO.println("Configuration loaded successfully. Max connections: " + maxConnections);
return config;
} catch (FileNotFoundException e) {
IO.println("Critical error: Configuration file not found at " + filePath);
} catch (IOException e) {
IO.println("I/O error while reading configuration file: " + e.getMessage());
} catch (NumberFormatException e) {
IO.println("Format error: db.max_connections must be a valid integer.");
}
return null; // Si ocurre cualquier excepción, devolvemos null
}
}
Esta aproximación facilita la sustitución de componentes y la realización de pruebas unitarias en el futuro:
static void main() {
// Instanciación del servicio de configuración
ConfigurationLoader configLoader = new ConfigurationLoader();
// Carga de ajustes desde el fichero
Properties config = configLoader.loadSettings("config.properties");
if (config != null) {
// Uso de los valores leídos
String dbUrl = config.getProperty("db.url");
IO.println("Connecting to database at: " + dbUrl);
} else {
IO.println("Application failed to start due to invalid configuration.");
}
}
Crea una excepción no verificada llamada InvalidConfigurationException.
Modifica el método loadSettings de la clase ConfigurationLoader para que:
FileNotFoundException) o hay un error de lectura (IOException), relance una InvalidConfigurationException envolviendo la excepción original (Exception Chaining).db.max_connections no es un número entero válido o su valor es menor o igual a cero, lance explícitamente una InvalidConfigurationException con un mensaje descriptivo.
Implementa un programa principal en la clase Main que instancie ConfigurationLoader y trate de cargar el archivo config.properties.
InvalidConfigurationException mediante un bloque try-catch.catch, muestra por pantalla el mensaje de error de la excepción y, si existe una causa raíz (root cause), imprime también el mensaje de la excepción original.