Tabla de Contenidos

05 - Control de Errores y Configuración

1. Gestión de Excepciones y Control de Flujo

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.

1.1 Jerarquía de Excepciones

Todas las excepciones derivan de la clase base java.lang.Throwable, que se divide en dos ramas principales:

ObjectThrowableErrorExceptionRuntimeException

1.2 Tratamiento de Excepciones

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).

Tratamiento de Checked Exceptions: La regla "Catch or Declare"

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:

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");
    }
}

Tratamiento de Unchecked Exceptions: Programación Defensiva

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.");
    }
}

Resumen de Estrategias de Tratamiento

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.
En el desarrollo de backend moderno, la mayoría de los frameworks (como Spring Boot) disponen de manejadores globales de excepciones (por ejemplo, mediante la anotación @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.

1.3 Sintaxis y Estructura: try, catch, finally y try-with-resources

Para capturar y gestionar las excepciones en tiempo de ejecución, Java proporciona una estructura de bloques bien definida.

El bloque clásico: try - catch - finally

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).");
        }
    }
}

Simplificación moderna: try-with-resources (Java 7+)

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.

1.4 Lanzamiento de Excepciones y Excepciones Personalizadas

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.

Las palabras clave throw y throws

Excepciones Personalizadas

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);
    }
}

Al capturar un error técnico (como un fallo de base de datos o de archivo) y relanzarlo como una excepción propia de negocio, pásale siempre la excepción original como segundo argumento al constructor (super(mensaje, causa);). De este modo, no se perderá la traza original del problema cuando se registre en el archivo de log.

2. Formatos Estructurados y Archivos de Configuración

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.

2.1 Comparativa de Formatos

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

2.2 Proceso de Parseo y Deserialización

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:

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.");
    }
}

En los temas posteriores aprenderemos a mapear automáticamente archivos JSON y YAML a objetos Java complejos.

Ejercicios

Ejercicio 1

Crea una excepción no verificada llamada InvalidConfigurationException.

Modifica el método loadSettings de la clase ConfigurationLoader para que:

Ejercicio 2

Implementa un programa principal en la clase Main que instancie ConfigurationLoader y trate de cargar el archivo config.properties.