En la lección anterior seguiste el viaje de una excepción desde el punto en que se lanza hasta que llega a la JVM sin que nadie la atienda: el programa imprime un stack trace y muere. Ahora vas a interceptar ese viaje. El bloque try-catch es el mecanismo con el que un método dice: «esto que voy a hacer puede fallar, y si falla de esta manera concreta, yo sé qué hacer».

Es la construcción más usada del módulo y también la más maltratada. Escribir try { ... } catch (Exception e) { e.printStackTrace(); } es tan fácil que se ha convertido en un reflejo automático, y es casi siempre la peor decisión posible: captura demasiado, no arregla nada y deja el programa corriendo con el estado roto. Esta lección te da la versión correcta: qué se protege exactamente dentro de un try, qué código se salta cuando salta la excepción, cómo interrogar al objeto capturado, por qué el orden de varios catch no es una cuestión de estilo sino un requisito del compilador, y —lo más importante— qué se puede hacer de verdad dentro de un catch más allá de imprimir el error.

Al final tendrás la primera victoria tangible del módulo: el menú interactivo que escribiste en 02-06 dejará de morirse cuando Marta Ruiz escriba "doce" donde el programa esperaba 12.

Contenido

  1. Sintaxis y semántica del try-catch
  2. Qué se protege y qué se salta
  3. El objeto excepción y sus métodos
  4. Múltiples bloques catch y la regla de orden
  5. Multi-catch con |
  6. Bloques try anidados
  7. Alcance de las variables declaradas dentro del try
  8. Qué se puede hacer dentro de un catch
  9. Estrategia 1: recuperarse con un valor por defecto
  10. Estrategia 2: reintentar
  11. Estrategia 3: traducir a otra excepción
  12. Estrategia 4: registrar y relanzar
  13. Anti-patrones, con demostración
  14. El coste real de una excepción frente a un if
  15. BiblioTech: el menú que ya no se rompe
  16. Errores Comunes y Consejos
  17. Ejercicios

  1. Sintaxis y semántica del try-catch

La forma mínima:

try {
    // Codigo PROTEGIDO: puede lanzar excepciones
} catch (TipoDeExcepcion e) {
    // MANEJADOR: se ejecuta solo si el try lanzo una excepcion
    // compatible con TipoDeExcepcion
}

Tres reglas de sintaxis que el compilador impone:

  1. Las llaves son obligatorias, incluso con una sola sentencia. A diferencia de if o for, aquí no hay versión sin llaves.
  2. Un try no puede ir solo. Debe llevar al menos un catch, o un finally (06-05), o ser un try-with-resources (06-06). try { ... } a secas no compila.
  3. El parámetro del catch se declara como el de un método: tipo y nombre. Por convención se llama e, ex o algo descriptivo como fallo o noEncontrado.

Un ejemplo completo con BiblioTech:

package com.nexussoftware.bibliotech.presentacion;

public class PrimerTryCatch {

    public static void main(String[] args) {
        String entrada = "doce";                       // lo que escribio el usuario

        System.out.println("1. Antes del try");

        try {
            System.out.println("2. Dentro del try, antes de convertir");
            int dias = Integer.parseInt(entrada);      // (*) lanza NumberFormatException
            System.out.println("3. Dias convertidos: " + dias);   // NO se ejecuta
        } catch (NumberFormatException e) {
            System.out.println("4. Capturada: " + e.getMessage());
        }

        System.out.println("5. Despues del try-catch: el programa SIGUE VIVO");
    }
}

Salida:

1. Antes del try
2. Dentro del try, antes de convertir
4. Capturada: For input string: "doce"
5. Despues del try-catch: el programa SIGUE VIVO

Compara esto con lo que pasaba en 06-01 sin try: el programa moría y el mensaje 5 nunca aparecía. Ahora la excepción se ha detenido aquí: no sigue subiendo por la pila, el hilo no muere, y la ejecución continúa normalmente después del bloque.

La semántica exacta, punto por punto:

  • Si el try termina sin excepción, todos los catch se ignoran por completo. La ejecución sigue en la línea posterior al último bloque.
  • Si el try lanza una excepción, la ejecución del try se abandona en ese instante —las líneas siguientes no se ejecutan— y la JVM busca entre los catch el primero cuyo tipo sea compatible con la excepción lanzada.
  • Si encuentra uno, ejecuta su cuerpo y luego continúa después del bloque completo. La excepción queda consumida: deja de propagarse.
  • Si no encuentra ninguno compatible, la excepción sigue subiendo por la pila como si el try no existiera, buscando manejador en el método llamador.

Ese último punto es crucial y se olvida a menudo: un try-catch que no captura el tipo lanzado no hace absolutamente nada.

try {
    Integer.parseInt("doce");                   // lanza NumberFormatException
} catch (ArithmeticException e) {               // tipo INCOMPATIBLE
    System.out.println("Nunca llega aqui");
}
// La NumberFormatException se propaga igual: el programa muere

  1. Qué se protege y qué se salta

La regla es simple pero tiene consecuencias que conviene ver dibujadas: se abandona todo lo que quede del try a partir del punto de fallo, por profundo que esté.

package com.nexussoftware.bibliotech.presentacion;

import java.util.ArrayList;
import java.util.List;

public class QueSeSalta {

    public static void main(String[] args) {
        List<String> registradas = new ArrayList<>();
        String[] entradas = { "15", "20", "doce", "30" };

        try {
            for (String entrada : entradas) {
                int dias = Integer.parseInt(entrada);
                registradas.add(entrada + " -> " + dias + " dias");
                System.out.println("Procesada: " + entrada);
            }
            System.out.println("TODAS procesadas");            // no se ejecuta
        } catch (NumberFormatException e) {
            System.out.println("Fallo en: " + e.getMessage());
        }

        System.out.println("Registradas: " + registradas);
    }
}

Salida:

Procesada: 15
Procesada: 20
Fallo en: For input string: "doce"
Registradas: [15 -> 15 dias, 20 -> 20 dias]

Fíjate en lo que ocurrió: la excepción saltó dentro del bucle, que estaba dentro del try. Se abandonó la iteración en curso, se abandonó el bucle entero, se abandonó el resto del try (incluido el println("TODAS procesadas")) y se saltó directo al catch. La entrada "30", que era perfectamente válida, no se procesó nunca.

Esto ilustra una decisión de diseño que tendrás que tomar constantemente: dónde poner el try. Si lo que quieres es que un dato malo no impida procesar los demás, el try debe estar dentro del bucle, no fuera:

for (String entrada : entradas) {
    try {
        int dias = Integer.parseInt(entrada);
        registradas.add(entrada + " -> " + dias + " dias");
    } catch (NumberFormatException e) {
        System.out.println("  Descartada entrada invalida: " + entrada);
        // continue implicito: la siguiente iteracion sigue normalmente
    }
}
// Resultado: [15 -> 15 dias, 20 -> 20 dias, 30 -> 30 dias]
Posición del try Semántica Cuándo usarla
Fuera del bucle El primer fallo aborta todo el proceso Todo o nada: una importación transaccional
Dentro del bucle Cada elemento falla por separado; los demás siguen Procesos tolerantes: importar 1000 registros y descartar los rotos

El flujo completo, dibujado:

flowchart TB
    A["Entra al bloque try"] --> B["Ejecuta sentencia"]
    B --> C{"¿Lanza excepcion?"}
    C -->|"no, y quedan sentencias"| B
    C -->|"no, y el try termina"| G["Salta TODOS los catch"]
    C -->|"si"| D["Abandona el resto del try<br/>(bucles y llamadas incluidos)"]
    D --> E{"¿Hay un catch<br/>de tipo compatible?"}
    E -->|"si"| F["Ejecuta ese catch<br/>La excepcion queda consumida"]
    E -->|"no"| H["La excepcion SIGUE subiendo<br/>por la pila de llamadas"]
    F --> I["Continua despues del bloque"]
    G --> I

Y un matiz importante: el try protege también todo lo que ocurra dentro de los métodos que se llamen desde él, por muchos niveles que haya. Si gestor.prestar(...) llama a catalogo.buscar(...) que llama a indice.get(...) y ahí se lanza una excepción, tu catch la recibe. Eso es precisamente lo que hace útil el mecanismo: puedes manejar en un solo sitio los fallos de un subárbol entero de llamadas.

  1. El objeto excepción y sus métodos

El parámetro del catch es una referencia al objeto excepción, con todo su estado disponible. Estos son sus métodos y para qué sirve cada uno:

Método Qué devuelve Uso típico
getMessage() El mensaje de detalle, o null si no tiene Componer un mensaje de log
getLocalizedMessage() Igual, salvo que la clase lo sobrescriba con traducción Mensajes internacionalizados
toString() clase.completa: mensaje Log compacto de una línea
getClass().getSimpleName() El nombre corto de la clase Distinguir el tipo en un log
printStackTrace() (void) Imprime el trace en System.err Solo depuración local, nunca producción
getStackTrace() StackTraceElement[] con los marcos Análisis programático del origen
getCause() La excepción que la provocó, o null Bajar a la causa raíz
getSuppressed() Throwable[] de excepciones suprimidas Solo con try-with-resources (06-06)
initCause(Throwable) Establece la causa a posteriori Casos raros; mejor el constructor (06-03)

Un catch que los usa todos, para que veas la diferencia entre ellos:

package com.nexussoftware.bibliotech.presentacion;

public class AnatomiaDelCatch {

    public static void main(String[] args) {
        try {
            calcularMultaDeUnEmpleado("EMP-004", "quince");
        } catch (NumberFormatException e) {

            System.out.println("getMessage()        : " + e.getMessage());
            System.out.println("toString()          : " + e);
            System.out.println("getClass().getName(): " + e.getClass().getName());
            System.out.println("getClass().getSimpleName(): " + e.getClass().getSimpleName());
            System.out.println("getCause()          : " + e.getCause());
            System.out.println("getSuppressed().length: " + e.getSuppressed().length);

            System.out.println("\n-- Marcos de la pila (getStackTrace) --");
            StackTraceElement[] marcos = e.getStackTrace();
            for (int i = 0; i < Math.min(4, marcos.length); i++) {
                StackTraceElement m = marcos[i];
                System.out.printf("  [%d] %s.%s (%s:%d)%n",
                        i, m.getClassName(), m.getMethodName(),
                        m.getFileName(), m.getLineNumber());
            }

            // Localizar nuestro propio codigo dentro del trace
            for (StackTraceElement m : marcos) {
                if (m.getClassName().startsWith("com.nexussoftware")) {
                    System.out.println("\nPrimer marco propio: "
                            + m.getMethodName() + " en linea " + m.getLineNumber());
                    break;
                }
            }
        }
    }

    static void calcularMultaDeUnEmpleado(String idEmpleado, String diasTexto) {
        int dias = Integer.parseInt(diasTexto);
        System.out.println("Multa: " + (dias * 0.25));
    }
}

Salida:

getMessage()        : For input string: "quince"
toString()          : java.lang.NumberFormatException: For input string: "quince"
getClass().getName(): java.lang.NumberFormatException
getClass().getSimpleName(): NumberFormatException
getCause()          : null
getSuppressed().length: 0

-- Marcos de la pila (getStackTrace) --
  [0] java.lang.NumberFormatException.forInputString (NumberFormatException.java:67)
  [1] java.lang.Integer.parseInt (Integer.java:665)
  [2] java.lang.Integer.parseInt (Integer.java:781)
  [3] com.nexussoftware.bibliotech.presentacion.AnatomiaDelCatch.calcularMultaDeUnEmpleado (AnatomiaDelCatch.java:38)

Primer marco propio: calcularMultaDeUnEmpleado en linea 38

Dos advertencias sobre estos métodos:

getMessage() puede devolver null. Lo viste en 06-01: UnsupportedOperationException y ConcurrentModificationException suelen lanzarse sin mensaje. Si concatenas directamente obtendrás "Error: null", que es peor que nada. Protégete:

String detalle = (e.getMessage() != null) ? e.getMessage() : e.getClass().getSimpleName();

O simplemente usa e.toString(), que siempre incluye al menos el nombre de la clase.

printStackTrace() no es logging. Escribe en System.err sin marca de tiempo, sin nivel, sin nombre del hilo, sin posibilidad de filtrar ni desactivar, y sin ninguna relación con el sistema de registro de la aplicación. Sirve para depurar en tu máquina y para nada más. En 06-07 lo sustituirás por logger.log(Level.SEVERE, mensaje, e), que hace lo mismo pero de forma utilizable.

  1. Múltiples bloques catch y la regla de orden

Un mismo try puede llevar varios catch, cada uno para un tipo distinto. La JVM los evalúa en orden de arriba abajo y ejecuta el primero compatible, no el más específico.

package com.nexussoftware.bibliotech.presentacion;

import java.util.ArrayList;
import java.util.List;

public class VariosCatch {

    public static void main(String[] args) {
        procesar(args.length > 0 ? args[0] : "1");
    }

    static void procesar(String caso) {
        List<String> catalogo = new ArrayList<>(List.of("Java Efectivo", "Patrones de Diseno"));

        try {
            switch (caso) {
                case "1" -> System.out.println(Integer.parseInt("doce"));   // NumberFormat
                case "2" -> System.out.println(catalogo.get(9));            // IndexOutOfBounds
                case "3" -> System.out.println(30 / 0);                     // Arithmetic
                default  -> System.out.println(((String) null).length());   // NullPointer
            }
        } catch (NumberFormatException e) {
            System.out.println("Entrada no numerica: " + e.getMessage());
        } catch (IndexOutOfBoundsException e) {
            System.out.println("Indice fuera de rango: " + e.getMessage());
        } catch (ArithmeticException e) {
            System.out.println("Error aritmetico: " + e.getMessage());
        } catch (RuntimeException e) {
            // Red de seguridad para el resto de no comprobadas
            System.out.println("Otro fallo en ejecucion: " + e);
        }
    }
}

Y ahora la regla que no es opcional:

Los catch deben ordenarse de la subclase a la superclase. Si un catch de un tipo más general precede a otro más específico, el código NO COMPILA.

Compruébalo:

try {
    Integer.parseInt("doce");
} catch (RuntimeException e) {              // MAS GENERAL primero
    System.out.println("General");
} catch (NumberFormatException e) {         // error de compilacion
    System.out.println("Especifico");
}

El compilador rechaza el código:

error: exception NumberFormatException has already been caught
    } catch (NumberFormatException e) {
      ^

El razonamiento es transparente: NumberFormatException es una RuntimeException, así que el primer catch ya la habría atrapado. El segundo bloque sería código inalcanzable, y Java rechaza el código inalcanzable en tiempo de compilación en lugar de dejarte creer que funciona.

Es una de las pocas veces en que el compilador te protege de un error de lógica, y conviene agradecerlo: en lenguajes sin esta comprobación, un catch ancho colocado por descuido al principio se traga silenciosamente todos los manejadores específicos que vienen después.

La jerarquía relevante para ordenar bien:

flowchart TB
    RE["RuntimeException<br/>(el mas general de los habituales)"]
    RE --> IAE["IllegalArgumentException"]
    RE --> ISE["IllegalStateException"]
    RE --> IOB["IndexOutOfBoundsException"]
    RE --> NPE["NullPointerException"]
    IAE --> NFE["NumberFormatException<br/>(el mas especifico)"]
    IOB --> AIOB["ArrayIndexOutOfBoundsException"]

    N1["Orden CORRECTO de los catch:<br/>NumberFormatException,<br/>luego IllegalArgumentException,<br/>luego RuntimeException"]

Ojo con la trampa de este diagrama: NumberFormatException desciende de IllegalArgumentException, no directamente de RuntimeException. Por eso este orden tampoco compila:

catch (IllegalArgumentException e) { ... }
catch (NumberFormatException e)    { ... }   // ya capturada por el anterior

Es un parentesco que sorprende mucho. Si capturas IllegalArgumentException para tus validaciones de dominio (06-03), ten presente que también estás capturando todos los fallos de conversión numérica, lo que puede confundir dos problemas muy distintos.

  1. Multi-catch con |

Cuando dos o más excepciones se manejan exactamente igual, Java 7 permite combinarlas en un solo catch con la barra vertical |:

try {
    int dias = Integer.parseInt(entrada);
    Material m = materiales.get(dias);
    procesar(m);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
    System.out.println("Entrada invalida (" + e.getClass().getSimpleName() + "): " + e.getMessage());
}

Sin multi-catch tendrías que duplicar el cuerpo o extraerlo a un método privado. Con él, la intención queda clara: estos dos fallos significan lo mismo para mí.

El multi-catch tiene dos reglas propias que hay que conocer:

Regla 1: los tipos no pueden estar relacionados por herencia.

// NO COMPILA
catch (NumberFormatException | IllegalArgumentException e) { ... }
// error: Alternatives in a multi-catch statement cannot be related by subclassing
//        Alternative NumberFormatException is a subclass of IllegalArgumentException

Tiene sentido: si vas a capturar IllegalArgumentException, la NumberFormatException ya está incluida. Nombrarla otra vez sería redundante y probablemente indicaría un malentendido sobre la jerarquía.

Regla 2: la variable es implícitamente final.

try {
    // ...
} catch (NumberFormatException | ArithmeticException e) {
    e = new RuntimeException("otra cosa");   // NO COMPILA
    // error: multi-catch parameter e may not be assigned
}

En un catch normal sí puedes reasignar el parámetro (aunque casi nunca deberías); en un multi-catch no. La razón es técnica: el tipo estático de e es el supertipo común más cercano de las alternativas, y permitir la reasignación complicaría el análisis de tipos del compilador. En la práctica, la restricción no molesta: reasignar el parámetro de un catch es una mala idea de todos modos.

Qué tipo tiene e en un multi-catch. Es el ancestro común más próximo de todas las alternativas. Con NumberFormatException | IndexOutOfBoundsException, el ancestro común es RuntimeException, así que dentro del bloque solo puedes llamar a los métodos de RuntimeException (que son los de Throwable). Si necesitas un método específico de una de ellas, necesitas catch separados o un instanceof:

} catch (NumberFormatException | java.io.IOException e) {
    // Aqui el tipo comun es Exception: solo metodos de Throwable
    System.out.println(e.getMessage());

    // Si necesitas algo especifico:
    if (e instanceof java.io.IOException io) {
        System.out.println("Fallo de E/S, se reintentara mas tarde");
    }
}

Cuándo usar cada forma:

Situación Forma
Mismo tratamiento para varios tipos no emparentados Multi-catch A | B
Tratamiento distinto por tipo catch separados, del específico al general
Un tipo y todos sus descendientes Un solo catch de la superclase

  1. Bloques try anidados

Un try puede contener otro try, y la búsqueda de manejador procede de dentro hacia fuera: primero los catch del try interno, y solo si ninguno es compatible se prueban los del externo.

package com.nexussoftware.bibliotech.presentacion;

public class TryAnidados {

    public static void main(String[] args) {
        String[] lineas = { "LIB-0001;15", "LIB-0002;doce", "LIB-0003" };

        try {
            System.out.println("== Importacion del catalogo ==");

            for (String linea : lineas) {
                String[] partes = linea.split(";");
                String referencia = partes[0];

                // TRY INTERNO: un dia invalido no debe abortar la importacion
                int dias;
                try {
                    dias = Integer.parseInt(partes[1]);
                } catch (NumberFormatException e) {
                    System.out.println("  " + referencia + ": dias invalidos, se usa 15 por defecto");
                    dias = 15;
                }

                System.out.println("  " + referencia + " -> " + dias + " dias");
            }

            System.out.println("Importacion terminada");
        } catch (ArrayIndexOutOfBoundsException e) {
            // TRY EXTERNO: una linea sin ';' SI aborta la importacion (formato corrupto)
            System.out.println("FICHERO CORRUPTO: falta un campo. " + e.getMessage());
        }
    }
}

Salida:

== Importacion del catalogo ==
  LIB-0001 -> 15 dias
  LIB-0002: dias invalidos, se usa 15 por defecto
  LIB-0002 -> 15 dias
FICHERO CORRUPTO: falta un campo. Index 1 out of bounds for length 1

Este ejemplo muestra exactamente cuándo tiene sentido anidar: cuando dos fallos distintos merecen dos ámbitos de recuperación distintos. Un número mal escrito es recuperable en el sitio (usamos un valor por defecto y seguimos); una línea sin el separador significa que el fichero entero es sospechoso y hay que abortar.

Dicho esto, el anidamiento es fácil de abusar. Dos o tres niveles hacen el código ilegible. Alternativas casi siempre preferibles:

  • Extraer el try interno a un método propio con nombre descriptivo (leerDiasODefecto(String)), lo que además documenta la política de recuperación.
  • Usar try-with-resources (06-06) si el anidamiento venía de cerrar recursos.

  1. Alcance de las variables declaradas dentro del try

Un detalle de sintaxis que causa errores de compilación constantes a quien empieza:

try {
    int dias = Integer.parseInt(entrada);       // declarada DENTRO del try
} catch (NumberFormatException e) {
    System.out.println("Fallo");
}
System.out.println(dias);   // NO COMPILA: cannot find symbol

Las llaves del try son un bloque como cualquier otro: lo declarado dentro solo existe dentro. Si necesitas la variable después, decláralas fuera:

int dias;                                        // declarada fuera
try {
    dias = Integer.parseInt(entrada);            // asignada dentro
} catch (NumberFormatException e) {
    dias = DIAS_PRESTAMO;                        // valor por defecto en el catch
}
System.out.println("Dias: " + dias);             // compila y funciona

Y aquí hay una sutileza que sí conviene entender bien. Este código no compila:

int dias;
try {
    dias = Integer.parseInt(entrada);
} catch (NumberFormatException e) {
    System.out.println("Fallo");                 // el catch NO asigna dias
}
System.out.println(dias);   // error: variable dias might not have been initialized

El compilador aplica análisis de asignación definitiva: para usar dias tiene que estar seguro de que se asignó en todos los caminos posibles. Si el try falla y el catch no asigna nada, dias se queda sin valor. El compilador lo detecta y se niega.

Tienes tres salidas legales:

// A) Inicializar en la declaracion
int dias = DIAS_PRESTAMO;
try { dias = Integer.parseInt(entrada); } catch (NumberFormatException e) { /* se queda el default */ }

// B) Asignar tambien en el catch
int dias;
try { dias = Integer.parseInt(entrada); } catch (NumberFormatException e) { dias = DIAS_PRESTAMO; }

// C) Salir del metodo en el catch (return o throw): entonces no hay camino sin asignar
int dias;
try { dias = Integer.parseInt(entrada); } catch (NumberFormatException e) { return; }
System.out.println(dias);   // compila: si llegamos aqui, dias tiene valor

La opción B es la más explícita y la que mejor documenta la intención: se ve de un vistazo cuál es el valor de recuperación. La A es más compacta pero puede confundir a quien lee, porque el valor por defecto queda lejos del catch.

  1. Qué se puede hacer dentro de un catch

Esta es la pregunta que separa el manejo de excepciones real del ritual vacío. Un catch no está ahí para "hacer algo con el error": está ahí para tomar una decisión. Y las decisiones sensatas son cuatro.

Estrategia Cuándo Riesgo
Recuperarse con un valor por defecto Existe una alternativa razonable y el programa puede continuar correctamente Ocultar un problema real bajo un valor plausible
Reintentar El fallo es transitorio (red, bloqueo, fichero ocupado) Bucle infinito si no se limita
Traducir a otra excepción El fallo debe subir, pero con el vocabulario de esta capa Perder la causa original
Registrar y relanzar Aquí solo se puede documentar; la decisión es de arriba Registrar dos veces el mismo error

Y una quinta, que casi nunca es correcta pero existe: no hacer nada, cuando el fallo es genuinamente irrelevante. Requiere un comentario obligatorio que explique por qué.

Lo que no es una estrategia: e.printStackTrace() a secas. Eso no decide nada; solo escupe texto y deja el programa continuar como si nada hubiera pasado, con el estado posiblemente roto.

Veamos las cuatro con código real.

  1. Estrategia 1: recuperarse con un valor por defecto

La más frecuente en la frontera con el usuario o con la configuración. El fallo tiene una alternativa razonable y documentada:

package com.nexussoftware.bibliotech.presentacion;

/** Conversion tolerante para la entrada por consola de BiblioTech. */
public final class EntradaSegura {

    private EntradaSegura() { }   // clase de utilidad: no se instancia

    /**
     * Convierte texto a entero. Si no es convertible, devuelve el valor por defecto.
     *
     * Esta es la version "tolerante", pensada para la CAPA DE PRESENTACION,
     * donde una entrada mala del usuario es algo normal y no un fallo del sistema.
     * En la capa de dominio la politica sera la contraria: lanzar (06-03).
     */
    public static int aEnteroODefecto(String texto, int porDefecto) {
        if (texto == null || texto.isBlank()) {
            return porDefecto;
        }
        try {
            return Integer.parseInt(texto.trim());
        } catch (NumberFormatException e) {
            // Recuperacion: el valor por defecto es una respuesta VALIDA aqui.
            // Se informa al usuario para que no crea que su dato se ha usado.
            System.out.println("  [aviso] '" + texto + "' no es un numero. Se usa " + porDefecto);
            return porDefecto;
        }
    }

    public static double aDecimalODefecto(String texto, double porDefecto) {
        if (texto == null || texto.isBlank()) {
            return porDefecto;
        }
        try {
            // Se acepta la coma decimal, muy habitual al teclear en espanol
            return Double.parseDouble(texto.trim().replace(',', '.'));
        } catch (NumberFormatException e) {
            System.out.println("  [aviso] '" + texto + "' no es un decimal. Se usa " + porDefecto);
            return porDefecto;
        }
    }

    public static void main(String[] args) {
        System.out.println(aEnteroODefecto("15", 15));        // 15
        System.out.println(aEnteroODefecto("doce", 15));      // aviso + 15
        System.out.println(aEnteroODefecto("  20  ", 15));    // 20 (trim)
        System.out.println(aEnteroODefecto(null, 15));        // 15
        System.out.println(aDecimalODefecto("0,25", 0.25));   // 0.25 (coma admitida)
    }
}

La condición para que esta estrategia sea legítima: el valor por defecto tiene que ser una respuesta correcta, no una tapadera. Devolver 15 cuando el usuario escribe "doce" en un campo de días de préstamo es defendible, porque 15 es el plazo estándar de BiblioTech y además se le avisa. Devolver 0 como saldo de multa cuando falla el cálculo no lo es: estaría inventando un dato de negocio.

  1. Estrategia 2: reintentar

Cuando el fallo es transitorio —una lectura de red que se corta, un fichero temporalmente bloqueado, un servicio que responde tarde—, reintentar es la respuesta correcta. Con tres condiciones innegociables: límite de intentos, espera entre ellos y fallo definitivo al agotarlos.

package com.nexussoftware.bibliotech.servicio;

/**
 * Reintento con espera creciente (backoff) para una operacion transitoria.
 *
 * Nota: la operacion se simula. El caso real (leer un fichero, llamar a un
 * servicio) llega en los modulos 7 y 9; lo que importa aqui es la ESTRUCTURA.
 */
public class SincronizadorCatalogo {

    private static final int MAX_INTENTOS = 4;
    private static final long ESPERA_BASE_MS = 200;

    private int llamadas = 0;

    /** Simula una operacion que falla las dos primeras veces y luego funciona. */
    private String descargarCatalogoRemoto() {
        llamadas++;
        if (llamadas < 3) {
            throw new IllegalStateException("Servidor no disponible (intento " + llamadas + ")");
        }
        return "3 materiales sincronizados";
    }

    public String sincronizar() {
        IllegalStateException ultimoFallo = null;

        for (int intento = 1; intento <= MAX_INTENTOS; intento++) {
            try {
                String resultado = descargarCatalogoRemoto();
                System.out.println("OK en el intento " + intento + ": " + resultado);
                return resultado;                        // exito: se sale del bucle
            } catch (IllegalStateException e) {
                ultimoFallo = e;                         // se guarda para el fallo definitivo
                System.out.println("Intento " + intento + " fallido: " + e.getMessage());

                if (intento == MAX_INTENTOS) {
                    break;                               // no esperar tras el ultimo intento
                }

                long espera = ESPERA_BASE_MS * (1L << (intento - 1));   // 200, 400, 800 ms
                System.out.println("  Esperando " + espera + " ms antes de reintentar...");
                try {
                    Thread.sleep(espera);
                } catch (InterruptedException ie) {
                    // Buena practica del modulo 8: restaurar el flag de interrupcion
                    // y abandonar. Nunca tragarse una InterruptedException.
                    Thread.currentThread().interrupt();
                    throw new IllegalStateException("Sincronizacion interrumpida", ie);
                }
            }
        }

        // Agotados los intentos: fallar de forma clara, conservando la causa (06-03)
        throw new IllegalStateException(
                "No se pudo sincronizar el catalogo tras " + MAX_INTENTOS + " intentos",
                ultimoFallo);
    }

    public static void main(String[] args) {
        System.out.println(new SincronizadorCatalogo().sincronizar());
    }
}

Salida:

Intento 1 fallido: Servidor no disponible (intento 1)
  Esperando 200 ms antes de reintentar...
Intento 2 fallido: Servidor no disponible (intento 2)
  Esperando 400 ms antes de reintentar...
OK en el intento 3: 3 materiales sincronizados
3 materiales sincronizados

Cuatro detalles que hacen que este patrón sea correcto y no una fuente de desastres:

  1. Límite explícito (MAX_INTENTOS). Un while (true) con reintentos es una bomba: si el servicio está caído de verdad, el programa gira para siempre consumiendo CPU.
  2. Espera creciente (exponential backoff): 200, 400, 800 ms. Reintentar inmediatamente en bucle agrava la caída del servicio remoto, porque le añades carga justo cuando está en apuros.
  3. Al agotar los intentos, se falla. No se devuelve null ni un valor vacío fingiendo éxito. Y se conserva el último fallo como causa, para que el stack trace muestre el Caused by:.
  4. Solo se reintentan fallos transitorios. Reintentar una NumberFormatException o una IllegalArgumentException es absurdo: el dato seguirá siendo inválido en el cuarto intento. La regla: reintenta lo que depende del entorno, nunca lo que depende de los datos.

  1. Estrategia 3: traducir a otra excepción

Cuando el fallo debe seguir subiendo, pero expresado en el vocabulario de tu capa. Un Catalogo no debería obligar a la capa de presentación a entender de índices de mapas ni de formatos de fichero.

// En la capa de servicio de BiblioTech
public Material cargarMaterialDesdeLinea(String linea) {
    try {
        String[] campos = linea.split(";");
        String referencia = campos[0];
        int anio = Integer.parseInt(campos[2]);
        return new Libro(referencia, campos[1], anio);
    } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
        // TRADUCCION: el llamador no tiene por que saber que dentro
        // habia un split ni un parseInt. Lo que le importa es que
        // esta linea del catalogo esta mal formada.
        throw new IllegalArgumentException(
                "Linea de catalogo mal formada: '" + linea + "'", e);   // <-- la causa, conservada
    }
}

La palabra clave es el segundo argumento del constructor: e. Es la causa, y es lo que produce el Caused by: del stack trace que estudiaste en 06-01. Sin él, la información de qué falló exactamente desaparece.

La técnica completa —encadenamiento de excepciones, traducción entre capas, cuándo envolver y cuándo relanzar— es el corazón de la lección 06-03, y las excepciones de dominio a las que traducirás (MaterialNoEncontradoException y compañía) llegan en 06-04. Aquí basta con que retengas la forma y el motivo.

  1. Estrategia 4: registrar y relanzar

Cuando en este punto solo puedes dejar constancia, pero la decisión de qué hacer corresponde a una capa superior:

public void procesarDevolucion(String referencia, int dia) {
    try {
        registro.devolver(referencia, dia);
    } catch (RuntimeException e) {
        // Se deja constancia CON el contexto que solo se conoce aqui...
        System.err.println("Fallo al devolver " + referencia + " el dia " + dia);
        // ...y se deja subir para que arriba decidan.
        throw e;                              // relanzar la MISMA excepcion
    }
}

throw e; relanza el objeto original intacto: misma clase, mismo mensaje, misma pila. No se añade un marco nuevo al stack trace, así que la línea del fallo original se conserva.

El peligro de este patrón tiene nombre: el log duplicado. Si cada capa registra y relanza, un solo fallo aparece cinco veces en el registro con cinco stack traces casi idénticos, y encontrar la primera ocurrencia se vuelve una tarea arqueológica. La regla que aplicarás en 06-07:

Registra donde manejas. Si vas a relanzar, no registres el stack trace completo: como mucho, añade contexto.

  1. Anti-patrones, con demostración

Los tres que más daño hacen, con la demostración de por qué.

El catch vacío

package com.nexussoftware.bibliotech.presentacion;

import java.util.ArrayList;
import java.util.List;

public class DemoCatchVacio {

    static List<String> registrados = new ArrayList<>();

    /** VERSION MALA: el catch vacio destruye la informacion del fallo. */
    static void registrarMal(String linea) {
        try {
            String[] partes = linea.split(";");
            int copias = Integer.parseInt(partes[1]);
            for (int i = 0; i < copias; i++) {
                registrados.add(partes[0] + "-" + i);
            }
        } catch (Exception e) {
            // vacio a proposito
        }
    }

    public static void main(String[] args) {
        registrarMal("LIB-0001;2");
        registrarMal("LIB-0002;dos");        // falla... y NADIE se entera
        registrarMal("LIB-0003");            // falla... y NADIE se entera
        registrarMal("LIB-0004;1");

        System.out.println("Registrados: " + registrados);
        System.out.println("Total esperado: 6 ejemplares. Total real: " + registrados.size());
    }
}

Salida:

Registrados: [LIB-0001-0, LIB-0001-1, LIB-0004-0]
Total esperado: 6 ejemplares. Total real: 3

Esto es exactamente lo que ocurre en producción: el programa termina sin error, con un resultado incorrecto y sin la menor pista de qué pasó. No hay stack trace, no hay log, no hay código de salida distinto de cero. Un fallo así puede vivir meses en un sistema, corrompiendo datos poco a poco, hasta que alguien nota que los números no cuadran. Y para entonces no queda ningún rastro que seguir.

Un catch vacío es peor que no capturar nada. Sin catch, el programa se cae ruidosamente y sabes exactamente dónde. Con él, mientes.

El catch (Exception e) demasiado ancho

// MAL: la red se lleva peces que no queriamos pescar
try {
    Material m = catalogo.buscarPorReferencia(referencia);
    int dias = Integer.parseInt(entradaUsuario);
    gestor.prestar(m, empleado, dias);
} catch (Exception e) {
    System.out.println("Entrada invalida, intentelo de nuevo");
}

El mensaje dice "entrada inválida", pero ese catch atrapa también un NullPointerException por un bug tuyo, un IllegalStateException porque el registro está corrupto y cualquier fallo de programación del resto del bloque. El usuario recibirá "entrada inválida" ante un defecto grave del sistema, y volverá a teclear su dato correcto una y otra vez preguntándose qué hace mal.

La versión correcta captura lo que sabe manejar y deja subir lo demás:

try {
    Material m = catalogo.buscarPorReferencia(referencia);
    int dias = Integer.parseInt(entradaUsuario);
    gestor.prestar(m, empleado, dias);
} catch (NumberFormatException e) {
    System.out.println("El numero de dias no es valido: " + entradaUsuario);
}
// Un NullPointerException sube y se ve. Es un bug, y debe notarse.

La regla: captura el tipo más específico que sepas manejar. Un catch (Exception e) solo es legítimo en la frontera de errores de la aplicación —el manejador global del main—, que es donde de verdad hay que impedir que algo escape sin registrarse. Eso lo montarás en 06-07.

Usar excepciones para el flujo normal

// PROHIBIDO: recorrer provocando la excepcion de fin
try {
    int i = 0;
    while (true) {
        System.out.println(materiales.get(i++).getTitulo());
    }
} catch (IndexOutOfBoundsException fin) {
    // "ya hemos terminado"
}

Además de oscuro, es lento. Y esto tiene números.

  1. El coste real de una excepción frente a un if

La parte cara de una excepción no es lanzarla ni capturarla: es construirla, porque el constructor de Throwable llama a fillInStackTrace(), que recorre y copia la pila de llamadas completa. Cuanto más profunda sea la pila, más cuesta.

package com.nexussoftware.bibliotech.presentacion;

/**
 * Comparativa ilustrativa: comprobar con if frente a provocar y capturar.
 * ATENCION: es una medicion casera. Para medir en serio hace falta JMH (11-07),
 * porque la JIT distorsiona los micro-benchmarks manuales.
 */
public class CosteDeLasExcepciones {

    private static final int VUELTAS = 1_000_000;

    public static void main(String[] args) {
        String[] entradas = new String[VUELTAS];
        for (int i = 0; i < VUELTAS; i++) {
            entradas[i] = (i % 2 == 0) ? String.valueOf(i) : "no-numero";   // 50% invalidas
        }

        // A) Comprobar antes con una validacion manual
        long t0 = System.nanoTime();
        int validosA = 0;
        for (String s : entradas) {
            if (esEnteroSimple(s)) { validosA++; }
        }
        long tA = (System.nanoTime() - t0) / 1_000_000;

        // B) Intentar y capturar la excepcion
        long t1 = System.nanoTime();
        int validosB = 0;
        for (String s : entradas) {
            try {
                Integer.parseInt(s);
                validosB++;
            } catch (NumberFormatException e) {
                // ignorado a proposito para la medicion
            }
        }
        long tB = (System.nanoTime() - t1) / 1_000_000;

        // C) Excepcion SIN stack trace (ver 06-04): cuanto cuesta solo el mecanismo
        long t2 = System.nanoTime();
        int validosC = 0;
        for (String s : entradas) {
            try {
                if (!esEnteroSimple(s)) { throw SIN_TRAZA; }
                validosC++;
            } catch (RuntimeException e) {
                // ignorado
            }
        }
        long tC = (System.nanoTime() - t2) / 1_000_000;

        System.out.println("A) if previo                    : " + tA + " ms (" + validosA + ")");
        System.out.println("B) try/catch con parseInt       : " + tB + " ms (" + validosB + ")");
        System.out.println("C) excepcion sin stack trace    : " + tC + " ms (" + validosC + ")");
    }

    /** Excepcion preconstruida y SIN stack trace: casi gratis de lanzar. */
    private static final RuntimeException SIN_TRAZA =
            new RuntimeException("no numerico", null, false, false);

    private static boolean esEnteroSimple(String s) {
        if (s == null || s.isEmpty()) { return false; }
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (i == 0 && (c == '-' || c == '+') && s.length() > 1) { continue; }
            if (c < '0' || c > '9') { return false; }
        }
        return true;
    }
}

Resultados típicos (una máquina cualquiera; los tuyos variarán, pero el orden de magnitud se mantiene):

A) if previo                    : 12 ms (500000)
B) try/catch con parseInt       : 890 ms (500000)
C) excepcion sin stack trace    : 18 ms (500000)
Enfoque Tiempo relativo Interpretación
A) if previo 1x El coste base
B) Excepción completa ~70x Dominado por fillInStackTrace
C) Excepción sin traza ~1,5x El mecanismo de throw/catch en sí es barato

Las conclusiones correctas —y una incorrecta que hay que descartar:

  • El throw/catch en sí no es caro. Lo caro es capturar la pila al construir el objeto. La comparación A vs C lo demuestra.
  • Por tanto, usar excepciones para lo excepcional no tiene coste apreciable. Si un error ocurre una vez cada mil operaciones, esos 900 nanosegundos son irrelevantes.
  • Usarlas para el flujo normal sí lo tiene. Un millón de excepciones en un bucle es un problema medible.
  • Conclusión incorrecta que NO debes sacar: "las excepciones son lentas, mejor devolver false". El eje de la decisión es la claridad y la corrección, no estos nanosegundos. Solo en un bucle muy caliente con fallos muy frecuentes la diferencia empieza a importar, y para ese caso raro existe el truco del constructor writableStackTrace = false que verás en 06-04.

Un aviso metodológico: este benchmark casero es orientativo. La JIT puede eliminar código muerto, alinear métodos y sesgar el resultado. Para medir de verdad se usa JMH (11-07). Lo que sí es sólido aquí es la comparación relativa entre B y C, porque aísla el factor que domina.

  1. BiblioTech: el menú que ya no se rompe

Es hora de cobrar la promesa. Este era el problema del menú de 02-06:

// Version del modulo 2: el usuario escribe "doce" y la aplicacion MUERE
System.out.print("Opcion: ");
int opcion = Integer.parseInt(scanner.nextLine());   // NumberFormatException -> adios

Y esta es la versión con manejo de errores. Fíjate en que la política es distinta según el tipo de dato: la opción del menú se reintenta (el usuario está ahí, puede corregir), y los datos numéricos con valor razonable se recuperan con un defecto.

package com.nexussoftware.bibliotech.presentacion;

import java.util.Scanner;

/**
 * Menu de BiblioTech resistente a la entrada del usuario.
 *
 * Politica de errores de esta clase (capa de presentacion):
 *  - Opcion del menu invalida  -> reintentar hasta que sea valida.
 *  - Dato numerico invalido    -> reintentar con limite; si se agota, cancelar la operacion.
 *  - Cualquier otro fallo      -> se deja subir (aun). En 06-07 se manejara en la frontera.
 */
public class MenuBiblioTech {

    private static final int MAX_REINTENTOS = 3;
    private static final int DIAS_PRESTAMO = 15;

    private final Scanner scanner = new Scanner(System.in);

    public void arrancar() {
        boolean salir = false;
        while (!salir) {
            mostrarMenu();
            int opcion = leerOpcion(0, 4);

            switch (opcion) {
                case 1 -> listarCatalogo();
                case 2 -> prestarMaterial();
                case 3 -> devolverMaterial();
                case 4 -> calcularMulta();
                case 0 -> { salir = true; System.out.println("Hasta luego."); }
                default -> System.out.println("Opcion no contemplada.");
            }
        }
    }

    private void mostrarMenu() {
        System.out.println("""

                ===== BiblioTech - Nexus Software =====
                1. Listar catalogo
                2. Prestar material
                3. Devolver material
                4. Calcular multa
                0. Salir
                =======================================""");
    }

    /**
     * Lee una opcion valida, reintentando indefinidamente.
     *
     * El reintento sin limite es aceptable AQUI porque hay una persona al otro
     * lado que puede corregir. En un proceso por lotes seria un bucle infinito:
     * la misma linea invalida se releeria para siempre.
     */
    private int leerOpcion(int min, int max) {
        while (true) {
            System.out.print("Opcion: ");
            String linea = scanner.nextLine();

            try {
                int valor = Integer.parseInt(linea.trim());
                if (valor < min || valor > max) {
                    System.out.println("  Debe estar entre " + min + " y " + max + ". Reintente.");
                    continue;                       // dato valido como numero, invalido como opcion
                }
                return valor;
            } catch (NumberFormatException e) {
                // RECUPERACION POR REINTENTO: se informa con el dato culpable
                System.out.println("  '" + linea + "' no es un numero. Escriba un digito de "
                        + min + " a " + max + ".");
            }
        }
    }

    /**
     * Lee un entero con limite de reintentos.
     * Devuelve -1 si el usuario agota los intentos, para que quien llame cancele.
     *
     * (Este -1 es todavia un "codigo de retorno" de los que critico 06-01. Es
     *  aceptable dentro de una misma clase de presentacion; en la capa de
     *  servicio se sustituira por excepciones de dominio en 06-04.)
     */
    private int leerEnteroConLimite(String peticion, int min, int max) {
        for (int intento = 1; intento <= MAX_REINTENTOS; intento++) {
            System.out.print(peticion + ": ");
            String linea = scanner.nextLine();

            try {
                int valor = Integer.parseInt(linea.trim());
                if (valor < min || valor > max) {
                    System.out.printf("  Fuera de rango [%d..%d]. Intento %d de %d.%n",
                            min, max, intento, MAX_REINTENTOS);
                    continue;
                }
                return valor;
            } catch (NumberFormatException e) {
                System.out.printf("  '%s' no es un numero. Intento %d de %d.%n",
                        linea, intento, MAX_REINTENTOS);
            }
        }
        System.out.println("  Demasiados intentos fallidos. Operacion cancelada.");
        return -1;
    }

    private double leerDecimalODefecto(String peticion, double porDefecto) {
        System.out.print(peticion + " [" + porDefecto + "]: ");
        String linea = scanner.nextLine().trim();

        if (linea.isEmpty()) {
            return porDefecto;                       // Enter directo: acepta el valor propuesto
        }
        try {
            return Double.parseDouble(linea.replace(',', '.'));
        } catch (NumberFormatException e) {
            System.out.println("  Valor no numerico. Se usa " + porDefecto);
            return porDefecto;
        }
    }

    private void listarCatalogo() {
        System.out.println("  (catalogo: 3 materiales)");
    }

    private void prestarMaterial() {
        System.out.print("Referencia del material: ");
        String referencia = scanner.nextLine().trim();

        int dias = leerEnteroConLimite("Dias de prestamo (1-30)", 1, 30);
        if (dias == -1) {
            return;                                  // cancelado: no se toca el estado
        }
        System.out.println("  Prestado " + referencia + " durante " + dias + " dias.");
    }

    private void devolverMaterial() {
        System.out.print("Referencia a devolver: ");
        String referencia = scanner.nextLine().trim();

        int dia = leerEnteroConLimite("Dia de devolucion (1-365)", 1, 365);
        if (dia == -1) {
            return;
        }
        System.out.println("  Devuelto " + referencia + " el dia " + dia + ".");
    }

    private void calcularMulta() {
        int diasRetraso = leerEnteroConLimite("Dias de retraso (0-200)", 0, 200);
        if (diasRetraso == -1) {
            return;
        }
        double tarifa = leerDecimalODefecto("Tarifa diaria", 0.25);

        double multa = Math.min(diasRetraso * tarifa, 20.0);   // MULTA_MAXIMA
        System.out.printf("  Multa: %.2f EUR (plazo estandar %d dias)%n", multa, DIAS_PRESTAMO);
    }

    public static void main(String[] args) {
        new MenuBiblioTech().arrancar();
    }
}

Sesión de ejemplo:

===== BiblioTech - Nexus Software =====
1. Listar catalogo
...
Opcion: dos
  'dos' no es un numero. Escriba un digito de 0 a 4.
Opcion: 9
  Debe estar entre 0 y 4. Reintente.
Opcion: 4
Dias de retraso (0-200): muchos
  'muchos' no es un numero. Intento 1 de 3.
Dias de retraso (0-200): 22
Tarifa diaria [0.25]:
  Multa: 5.50 EUR (plazo estandar 15 dias)

Tres decisiones de diseño que merece la pena señalar, porque son las que separan este código del "poner un try por si acaso":

  1. La política de error depende del contexto, no del tipo de excepción. La misma NumberFormatException se trata de tres formas distintas: reintento infinito para la opción del menú (hay un humano), reintento limitado para los datos (evita quedarse atascado), y valor por defecto para la tarifa (hay un valor razonable).
  2. La cancelación deja el estado intacto. Cuando leerEnteroConLimite agota los intentos, prestarMaterial hace return antes de tocar nada. No hay préstamo a medias. Es un anticipo de la coherencia de estado que resolverás del todo en 06-05.
  3. Se captura solo NumberFormatException, no Exception. Si dentro del menú apareciera un NullPointerException por un bug, subiría y se vería. Ocultarlo bajo "entrada inválida" sería mentirle al usuario y a ti mismo.

Queda una fragilidad evidente que esta lección no puede resolver: la capa de servicio (GestorPrestamos, Catalogo) sigue devolviendo null y false. Para arreglar eso hace falta lanzar excepciones, no solo capturarlas. Es la lección siguiente.

Errores Comunes y Consejos

Capturar un tipo que el try no puede lanzar. Con excepciones comprobadas, el compilador lo rechaza (exception X is never thrown in body of corresponding try statement). Con las no comprobadas, compila silenciosamente y el catch nunca se ejecuta: es código muerto que da falsa sensación de seguridad.

Poner catch (Exception e) por costumbre. Atrapa fallos que no sabes manejar y convierte bugs en mensajes tranquilizadores. Captura el tipo más específico y deja subir lo demás.

Invertir el orden de los catch. El general antes que el específico no compila: exception X has already been caught. Recuerda que NumberFormatException desciende de IllegalArgumentException, un parentesco que sorprende.

Poner el try alrededor del bucle cuando querías que cada elemento fallara por separado. El primer fallo aborta el resto del proceso. Si quieres tolerancia elemento a elemento, el try va dentro.

Usar una variable declarada dentro del try después del bloque. No existe fuera. Y si la declaras fuera pero solo la asignas dentro del try, el compilador exigirá que el catch también la asigne, o que salga del método.

e.printStackTrace() como manejo. No es manejo, es un vertido de texto en System.err. En producción se pierde, no se puede filtrar y no lleva ni marca de tiempo ni contexto. En 06-07 lo sustituyes por un Logger.

Concatenar e.getMessage() sin comprobar null. Varias excepciones estándar no llevan mensaje, y obtendrás "Error: null". Usa e.toString() o un valor alternativo.

Reintentar un fallo que no es transitorio. Reintentar una NumberFormatException con el mismo dato es girar en el vacío. Reintenta lo que depende del entorno; nunca lo que depende de los datos.

Reintentar sin límite en un proceso automático. Con un humano delante es aceptable; en un proceso por lotes es un bucle infinito garantizado.

Tragarse una InterruptedException. Si capturas una interrupción y no haces nada, rompes el mecanismo de cancelación de hilos del módulo 8. Lo mínimo es Thread.currentThread().interrupt() antes de salir.

Consejo: un catch que no toma ninguna decisión es un catch mal puesto. Recuperar, reintentar, traducir o registrar y relanzar. Si no haces ninguna de las cuatro, probablemente no debías capturar ahí.

Consejo: cuando informes al usuario, incluye el dato culpable. "'doce' no es un numero" orienta; "Entrada invalida" no. El coste es el mismo.

Consejo: comenta todo catch que quede vacío a propósito. Si de verdad el fallo es irrelevante, escribe por qué. Un catch vacío sin comentario es indistinguible de un olvido, y el siguiente que lea el código no sabrá si arreglarlo.

Ejercicios

Ejercicio 1: importador tolerante del catálogo

Escribe ImportadorCatalogo en com.nexussoftware.bibliotech.servicio, que reciba un String[] de líneas con el formato referencia;titulo;anio;ejemplares y produzca un informe de importación.

Requisitos:

  • Procesa cada línea de forma independiente: un error en una no debe impedir procesar las demás.
  • Trata de forma distinta al menos tres tipos de error:
    • Falta algún campo → ArrayIndexOutOfBoundsException → se descarta la línea con motivo.
    • El año o los ejemplares no son numéricos → NumberFormatException → se descarta con motivo.
    • El año está fuera de [1450, 2100] o los ejemplares son <= 0no hay excepción: valida con un if y descarta con motivo.
  • Usa multi-catch donde el tratamiento sea idéntico, y catch separados donde no lo sea.
  • Devuelve un record InformeImportacion(int aceptadas, int rechazadas, List<String> motivos).
  • Añade un contador de líneas procesadas que sea correcto incluso cuando fallan.

Prueba con al menos: una línea correcta, una sin campos suficientes, una con año "mil novecientos", una con año 1200, una con 0 ejemplares y una línea vacía.

Ejercicio 2: caza del anti-patrón

Este método existe de verdad en el BiblioTech heredado. Tiene seis defectos relacionados con el manejo de excepciones. Encuéntralos, explica el daño que hace cada uno y reescribe el método correctamente.

public double calcularMultaTotal(List<String> referencias, Map<String, Integer> diasRetraso) {
    double total = 0;
    try {
        for (String ref : referencias) {
            try {
                int dias = diasRetraso.get(ref);
                if (dias > 0) {
                    total += dias * 0.25;
                }
            } catch (Exception e) {
            }
        }
        int media = (int) total / referencias.size();
        System.out.println("Media: " + media);
    } catch (Throwable t) {
        t.printStackTrace();
        return 0;
    }
    return total;
}

Ejercicio 3: lector de configuración con política por clave

Escribe ConfiguracionBiblioTech, que lea parámetros de un Map<String, String> (simulando un fichero de propiedades, que llega en 07-07) y aplique una política de error distinta según la criticidad de cada parámetro:

  • leerEnteroObligatorio(clave): si falta o no es numérico, lanza IllegalStateException con un mensaje que identifique la clave y el valor encontrado, conservando la causa cuando la haya.
  • leerEnteroOpcional(clave, porDefecto): si falta o no es numérico, devuelve el defecto e informa por consola.
  • leerDecimalConRango(clave, min, max, porDefecto): además de convertir, valida el rango; fuera de rango o no numérico, devuelve el defecto avisando del motivo concreto (distingue "no numérico" de "fuera de rango").
  • leerBooleano(clave, porDefecto): acepta "true"/"false"/"si"/"no"/"1"/"0" sin distinguir mayúsculas; cualquier otra cosa, defecto con aviso.

En el main, carga una configuración con valores válidos, inválidos y ausentes —incluyendo dias.prestamo=15, tarifa.diaria=cero, multa.maxima=999, umbral.leve ausente y avisos.activos=SI— y demuestra las cuatro políticas. Muestra también qué ocurre cuando falta un parámetro obligatorio, capturándolo en el main y mostrando un mensaje digno.

Soluciones

Solución 1

package com.nexussoftware.bibliotech.servicio;

import java.util.ArrayList;
import java.util.List;

/**
 * Importa lineas de catalogo tolerando errores linea a linea.
 *
 * Clave del diseno: el try va DENTRO del bucle, para que una linea
 * defectuosa no aborte la importacion completa.
 */
public class ImportadorCatalogo {

    /** Resultado de la importacion (record: 04-07). */
    public record InformeImportacion(int procesadas, int aceptadas,
                                     int rechazadas, List<String> motivos) {

        public String resumen() {
            StringBuilder sb = new StringBuilder();
            sb.append("=== INFORME DE IMPORTACION ===\n");
            sb.append("Procesadas: ").append(procesadas).append('\n');
            sb.append("Aceptadas : ").append(aceptadas).append('\n');
            sb.append("Rechazadas: ").append(rechazadas).append('\n');
            if (!motivos.isEmpty()) {
                sb.append("--- Motivos de rechazo ---\n");
                for (String m : motivos) {
                    sb.append("  * ").append(m).append('\n');
                }
            }
            return sb.toString();
        }
    }

    private static final int ANIO_MIN = 1450;
    private static final int ANIO_MAX = 2100;

    private final List<String> aceptados = new ArrayList<>();

    public InformeImportacion importar(String[] lineas) {
        List<String> motivos = new ArrayList<>();
        int procesadas = 0;
        int aceptadas = 0;

        for (String linea : lineas) {
            procesadas++;                       // se incrementa SIEMPRE, falle o no

            // Validacion previa: una linea vacia no merece ni intentarse.
            // Regla general: si se puede comprobar con un if barato, no uses excepcion.
            if (linea == null || linea.isBlank()) {
                motivos.add("Linea " + procesadas + ": vacia");
                continue;
            }

            try {
                String[] campos = linea.split(";");

                // Si faltan campos, este acceso lanza ArrayIndexOutOfBoundsException
                String referencia = campos[0].trim();
                String titulo     = campos[1].trim();
                int anio          = Integer.parseInt(campos[2].trim());
                int ejemplares    = Integer.parseInt(campos[3].trim());

                // Validaciones de negocio: NO lanzan, se comprueban con if.
                // Un valor fuera de rango no es un fallo tecnico, es un dato incorrecto.
                if (anio < ANIO_MIN || anio > ANIO_MAX) {
                    motivos.add("Linea " + procesadas + " (" + referencia + "): anio fuera de rango ["
                            + ANIO_MIN + ".." + ANIO_MAX + "]: " + anio);
                    continue;
                }
                if (ejemplares <= 0) {
                    motivos.add("Linea " + procesadas + " (" + referencia
                            + "): ejemplares debe ser positivo, era " + ejemplares);
                    continue;
                }
                if (referencia.isEmpty() || titulo.isEmpty()) {
                    motivos.add("Linea " + procesadas + ": referencia o titulo vacios");
                    continue;
                }

                aceptados.add(referencia + " | " + titulo + " | " + anio + " | " + ejemplares);
                aceptadas++;

            } catch (ArrayIndexOutOfBoundsException e) {
                // CATCH ESPECIFICO 1: faltan campos. Mensaje distinto del otro caso.
                motivos.add("Linea " + procesadas + ": faltan campos (se esperaban 4). Contenido: '"
                        + linea + "'");

            } catch (NumberFormatException e) {
                // CATCH ESPECIFICO 2: campos numericos mal escritos.
                // e.getMessage() ya dice cual era el texto ofensivo.
                motivos.add("Linea " + procesadas + ": campo numerico invalido -> " + e.getMessage());
            }
        }

        return new InformeImportacion(procesadas, aceptadas, procesadas - aceptadas, motivos);
    }

    public List<String> getAceptados() {
        return List.copyOf(aceptados);          // copia inmutable: encapsulamiento (03-07)
    }

    public static void main(String[] args) {
        String[] lineas = {
                "LIB-0001;Java Efectivo;2018;3",            // correcta
                "LIB-0002;Patrones de Diseno",              // faltan campos
                "LIB-0003;Refactorizacion;mil novecientos;2", // anio no numerico
                "LIB-0004;Libro Antiguo;1200;1",            // anio fuera de rango
                "LIB-0005;Sin Ejemplares;2020;0",           // ejemplares invalidos
                "",                                          // vacia
                "LIB-0006;Java Concurrente;2021;dos",       // ejemplares no numericos
                "LIB-0007;Clean Code;2008;5"                // correcta
        };

        ImportadorCatalogo importador = new ImportadorCatalogo();
        InformeImportacion informe = importador.importar(lineas);

        System.out.println(informe.resumen());
        System.out.println("--- Aceptados ---");
        importador.getAceptados().forEach(a -> System.out.println("  " + a));
    }
}

Salida:

=== INFORME DE IMPORTACION ===
Procesadas: 8
Aceptadas : 2
Rechazadas: 6
--- Motivos de rechazo ---
  * Linea 2: faltan campos (se esperaban 4). Contenido: 'LIB-0002;Patrones de Diseno'
  * Linea 3: campo numerico invalido -> For input string: "mil novecientos"
  * Linea 4 (LIB-0004): anio fuera de rango [1450..2100]: 1200
  * Linea 5 (LIB-0005): ejemplares debe ser positivo, era 0
  * Linea 6: vacia
  * Linea 7: campo numerico invalido -> For input string: "dos"

--- Aceptados ---
  LIB-0001 | Java Efectivo | 2018 | 3
  LIB-0007 | Clean Code | 2008 | 5

Nota de diseño: fíjate en que no se ha usado multi-catch. Al escribir los mensajes se vio que "faltan campos" y "campo numérico inválido" merecen textos distintos, así que los catch separados son la elección correcta. El multi-catch habría sido apropiado si el motivo fuera simplemente "linea mal formada" en ambos casos. Es la regla del apartado 5 en acción: multi-catch cuando el tratamiento es idéntico, no cuando los tipos son parecidos.

Solución 2

Los seis defectos:

# Defecto Daño
1 catch (Exception e) { } vacío en el bucle interno Cada referencia ausente en el mapa se descarta en silencio. El total sale mal y nadie se entera
2 catch (Exception e) demasiado ancho Atrapa el NullPointerException del autounboxing, pero también cualquier bug de programación
3 diasRetraso.get(ref) desempaquetado sin comprobar Si la clave no existe, get devuelve null, se intenta intValue() y salta NullPointerException. Es exactamente el fallo que el catch vacío oculta
4 catch (Throwable t) Atrapa OutOfMemoryError y StackOverflowError, de los que no se puede recuperar
5 t.printStackTrace() como manejo No es logging; en producción se pierde
6 return 0 tras el fallo Devuelve un importe de multa inventado. Un cero indistinguible de "no hay multa". Además, referencias.size() puede ser 0 y lanzar ArithmeticException, que ese catch(Throwable) disimula

Hay un séptimo problema de fondo, más grave que los seis: el método calcula una media que no devuelve ni usa, solo la imprime. Esa mezcla de cálculo y presentación es lo que obliga a poner el println dentro y lo que hace que la división por cero se cuele en un método de cálculo.

Versión corregida:

package com.nexussoftware.bibliotech.servicio;

import java.util.List;
import java.util.Map;
import java.util.Objects;

public class CalculadoraMultas {

    private static final double TARIFA_DIARIA = 0.25;
    private static final double MULTA_MAXIMA  = 20.0;

    /**
     * Suma la multa de las referencias indicadas.
     *
     * Politica de errores:
     *  - Argumentos nulos: fallo de programacion, se lanza (fail-fast, 06-03).
     *  - Referencia sin dato de retraso: se considera 0 dias, comprobandolo
     *    con un if. No es un fallo: es un caso normal (aun no ha vencido).
     *  - No se captura NADA aqui: este metodo no sabe que hacer con un fallo.
     *    Quien lo llame decidira.
     */
    public double calcularMultaTotal(List<String> referencias, Map<String, Integer> diasRetraso) {
        Objects.requireNonNull(referencias, "La lista de referencias no puede ser nula");
        Objects.requireNonNull(diasRetraso, "El mapa de dias de retraso no puede ser nulo");

        double total = 0;
        for (String ref : referencias) {
            // getOrDefault evita el desempaquetado de null: es un if disfrazado,
            // y es MEJOR que capturar el NullPointerException que provocaria get().
            int dias = diasRetraso.getOrDefault(ref, 0);

            if (dias > 0) {
                double multa = Math.min(dias * TARIFA_DIARIA, MULTA_MAXIMA);
                total += multa;
            }
        }
        return total;
    }

    /**
     * Media de multa por material. Separada del calculo total: cada metodo, una cosa.
     * La lista vacia se comprueba explicitamente: es un caso previsible, no un fallo.
     */
    public double calcularMultaMedia(List<String> referencias, Map<String, Integer> diasRetraso) {
        Objects.requireNonNull(referencias, "La lista de referencias no puede ser nula");

        if (referencias.isEmpty()) {
            return 0.0;                 // media de un conjunto vacio: 0 es la respuesta correcta
        }
        return calcularMultaTotal(referencias, diasRetraso) / referencias.size();
    }

    public static void main(String[] args) {
        CalculadoraMultas calc = new CalculadoraMultas();

        List<String> refs = List.of("LIB-0001", "LIB-0002", "LIB-0003", "LIB-0004");
        Map<String, Integer> retrasos = Map.of(
                "LIB-0001", 4,          // 1.00 EUR
                "LIB-0002", 0,          // sin retraso
                "LIB-0003", 120);       // 30.00 -> topado a 20.00
        // LIB-0004 no aparece en el mapa: getOrDefault devuelve 0

        System.out.printf("Total: %.2f EUR%n", calc.calcularMultaTotal(refs, retrasos));
        System.out.printf("Media: %.2f EUR%n", calc.calcularMultaMedia(refs, retrasos));
        System.out.printf("Media de lista vacia: %.2f EUR%n",
                calc.calcularMultaMedia(List.of(), retrasos));
    }
}

Salida:

Total: 21.00 EUR
Media: 5.25 EUR
Media de lista vacia: 0.00 EUR

Cambios clave y su razón:

  1. Cero try-catch. El método no sabe qué hacer con un fallo, así que no captura ninguno. Es la mejor decisión posible en muchos métodos de servicio, y por eso el catch que estaba ahí antes era un mero ritual.
  2. getOrDefault en lugar de capturar el NullPointerException. Comprobar es más barato, más claro y más rápido que provocar y atrapar (apartado 14).
  3. Objects.requireNonNull para los argumentos: fallo rápido con mensaje útil, en lugar de un NullPointerException opaco tres líneas más abajo. Se desarrolla en 06-03.
  4. La media se separa del total y la lista vacía se comprueba explícitamente: la división por cero deja de ser posible.
  5. No hay println dentro: el cálculo no imprime. Presentación y lógica, separadas (03-08).

Solución 3

package com.nexussoftware.bibliotech.servicio;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Lectura de configuracion con politica de error POR PARAMETRO.
 *
 * La leccion de diseno: no existe "la forma correcta" de manejar un error.
 * Depende de si el programa puede continuar sin ese dato.
 */
public class ConfiguracionBiblioTech {

    private final Map<String, String> propiedades;

    private static final Set<String> VERDADEROS = Set.of("true", "si", "sí", "1", "s", "yes");
    private static final Set<String> FALSOS     = Set.of("false", "no", "0", "n");

    public ConfiguracionBiblioTech(Map<String, String> propiedades) {
        // Copia defensiva: nadie modificara la configuracion por la espalda (03-07)
        this.propiedades = new HashMap<>(propiedades);
    }

    /**
     * POLITICA A: parametro OBLIGATORIO.
     * Sin el, el programa no puede funcionar correctamente: se lanza.
     * Se conserva la causa cuando existe, para no perder el detalle (06-03).
     */
    public int leerEnteroObligatorio(String clave) {
        String valor = propiedades.get(clave);

        if (valor == null) {
            throw new IllegalStateException(
                    "Falta el parametro obligatorio '" + clave + "' en la configuracion");
        }
        try {
            return Integer.parseInt(valor.trim());
        } catch (NumberFormatException e) {
            // TRADUCCION con causa: el llamador no tiene que saber que dentro
            // habia un parseInt, pero el stack trace conservara el detalle.
            throw new IllegalStateException(
                    "El parametro obligatorio '" + clave + "' debe ser un entero, y vale '"
                            + valor + "'", e);
        }
    }

    /**
     * POLITICA B: parametro OPCIONAL.
     * Hay un valor por defecto razonable: se recupera e informa.
     */
    public int leerEnteroOpcional(String clave, int porDefecto) {
        String valor = propiedades.get(clave);

        if (valor == null) {
            System.out.println("  [config] '" + clave + "' ausente. Se usa " + porDefecto);
            return porDefecto;
        }
        try {
            return Integer.parseInt(valor.trim());
        } catch (NumberFormatException e) {
            System.out.println("  [config] '" + clave + "' = '" + valor
                    + "' no es un entero. Se usa " + porDefecto);
            return porDefecto;
        }
    }

    /**
     * POLITICA C: valor por defecto CON validacion de rango.
     * Distingue dos motivos de rechazo distintos, porque para diagnosticar
     * no es lo mismo "escribiste letras" que "escribiste un numero absurdo".
     */
    public double leerDecimalConRango(String clave, double min, double max, double porDefecto) {
        String valor = propiedades.get(clave);

        if (valor == null) {
            System.out.println("  [config] '" + clave + "' ausente. Se usa " + porDefecto);
            return porDefecto;
        }

        double convertido;
        try {
            convertido = Double.parseDouble(valor.trim().replace(',', '.'));
        } catch (NumberFormatException e) {
            System.out.println("  [config] '" + clave + "' = '" + valor
                    + "' no es un decimal. Se usa " + porDefecto);
            return porDefecto;
        }

        // La validacion de rango va FUERA del try: no es un fallo de conversion,
        // y meterla dentro haria que un futuro fallo del if se confundiera.
        if (convertido < min || convertido > max) {
            System.out.printf("  [config] '%s' = %.2f fuera de [%.2f..%.2f]. Se usa %.2f%n",
                    clave, convertido, min, max, porDefecto);
            return porDefecto;
        }
        return convertido;
    }

    /**
     * POLITICA D: booleano tolerante.
     * No hay excepcion que capturar: no existe Boolean.parseBoolean que falle
     * (devuelve false ante cualquier cosa, que es justo el problema).
     * Se resuelve con conjuntos y validacion explicita.
     */
    public boolean leerBooleano(String clave, boolean porDefecto) {
        String valor = propiedades.get(clave);

        if (valor == null) {
            System.out.println("  [config] '" + clave + "' ausente. Se usa " + porDefecto);
            return porDefecto;
        }

        String normalizado = valor.trim().toLowerCase();
        if (VERDADEROS.contains(normalizado)) { return true; }
        if (FALSOS.contains(normalizado))     { return false; }

        System.out.println("  [config] '" + clave + "' = '" + valor
                + "' no es un booleano reconocible. Se usa " + porDefecto);
        return porDefecto;
    }

    // ------------------------------------------------------------------

    public static void main(String[] args) {
        Map<String, String> props = new HashMap<>();
        props.put("dias.prestamo",  "15");
        props.put("tarifa.diaria",  "cero");        // invalido
        props.put("multa.maxima",   "999");         // fuera de rango
        props.put("avisos.activos", "SI");          // valido, en mayusculas
        props.put("max.prestamos",  "3");
        // "umbral.leve" deliberadamente ausente

        ConfiguracionBiblioTech config = new ConfiguracionBiblioTech(props);

        System.out.println("== Lectura de configuracion de BiblioTech ==");

        int dias = config.leerEnteroObligatorio("dias.prestamo");
        System.out.println("DIAS_PRESTAMO   = " + dias);

        double tarifa = config.leerDecimalConRango("tarifa.diaria", 0.0, 5.0, 0.25);
        System.out.println("TARIFA_DIARIA   = " + tarifa);

        double maxima = config.leerDecimalConRango("multa.maxima", 0.0, 100.0, 20.0);
        System.out.println("MULTA_MAXIMA    = " + maxima);

        int umbral = config.leerEnteroOpcional("umbral.leve", 7);
        System.out.println("UMBRAL_LEVE     = " + umbral);

        boolean avisos = config.leerBooleano("avisos.activos", false);
        System.out.println("AVISOS_ACTIVOS  = " + avisos);

        // Demostracion de la politica A cuando el parametro obligatorio falta
        System.out.println("\n== Parametro obligatorio ausente ==");
        try {
            config.leerEnteroObligatorio("puerto.servidor");
        } catch (IllegalStateException e) {
            System.out.println("ERROR DE CONFIGURACION: " + e.getMessage());
            System.out.println("Causa subyacente: " + e.getCause());
        }

        // Demostracion de la politica A cuando el valor es invalido: hay causa
        System.out.println("\n== Parametro obligatorio invalido ==");
        try {
            config.leerEnteroObligatorio("tarifa.diaria");
        } catch (IllegalStateException e) {
            System.out.println("ERROR DE CONFIGURACION: " + e.getMessage());
            System.out.println("Causa subyacente: " + e.getCause());
        }
    }
}

Salida:

== Lectura de configuracion de BiblioTech ==
DIAS_PRESTAMO   = 15
  [config] 'tarifa.diaria' = 'cero' no es un decimal. Se usa 0.25
TARIFA_DIARIA   = 0.25
  [config] 'multa.maxima' = 999.00 fuera de [0.00..100.00]. Se usa 20.00
MULTA_MAXIMA    = 20.0
  [config] 'umbral.leve' ausente. Se usa 7
UMBRAL_LEVE     = 7
AVISOS_ACTIVOS  = true

== Parametro obligatorio ausente ==
ERROR DE CONFIGURACION: Falta el parametro obligatorio 'puerto.servidor' en la configuracion
Causa subyacente: null

== Parametro obligatorio invalido ==
ERROR DE CONFIGURACION: El parametro obligatorio 'tarifa.diaria' debe ser un entero, y vale 'cero'
Causa subyacente: java.lang.NumberFormatException: For input string: "cero"

Lo que demuestra el ejercicio:

  1. La misma excepción, cuatro políticas distintas. NumberFormatException se convierte en un IllegalStateException que aborta el arranque, o en un aviso con valor por defecto, según lo crítico que sea el parámetro. La política la marca el contexto, no el tipo de excepción.
  2. La causa se conserva al traducir (el último bloque de salida), y se pierde por completo cuando no había excepción original (el caso del parámetro ausente, con Causa subyacente: null). Esa diferencia se ve en el stack trace como la presencia o ausencia de Caused by:.
  3. No todo error necesita una excepción. leerBooleano no captura nada: usa conjuntos y validación explícita, porque Boolean.parseBoolean no lanza —simplemente devuelve false ante cualquier basura, que es un fallo silencioso peor que una excepción.

Conclusión

Ya sabes manejar una excepción, no solo leerla. Dominas la sintaxis y la semántica exactas de try-catch: qué se protege —todo el bloque, incluidos los métodos que se llamen desde él, por profundos que sean—, qué se abandona cuando salta la excepción —todo lo que quede del try, bucles incluidos— y qué ocurre si ningún catch es compatible: la excepción sigue subiendo como si el try no existiera. Y sabes que la posición del try respecto a un bucle es una decisión de diseño con dos semánticas distintas: fuera para "todo o nada", dentro para tolerar elemento a elemento.

Conoces el objeto capturado y sus métodos: getMessage —que puede ser null—, toString, getStackTrace para inspeccionar los marcos, getCause para bajar a la causa raíz y getSuppressed, que cobrará sentido en 06-06. Y sabes que printStackTrace() no es manejo de errores, sino una herramienta de depuración local que en 06-07 sustituirás por un Logger.

Tienes la regla de orden de los múltiples catch —de la subclase a la superclase, con el error de compilación exception X has already been caught si la inviertes— y sabes que NumberFormatException desciende de IllegalArgumentException, un parentesco que rompe más de un orden aparentemente correcto. Manejas el multi-catch con | y sus dos reglas: los tipos no pueden estar emparentados por herencia, y la variable es implícitamente final. Sabes cuándo anidar try —dos ámbitos de recuperación distintos— y cuándo extraer un método en su lugar. Y conoces el alcance de las variables del try y el análisis de asignación definitiva que obliga a asignar también en el catch, o a salir del método.

Lo más importante: sabes qué se puede hacer dentro de un catch. Recuperarse con un valor por defecto que sea una respuesta legítima y no una tapadera; reintentar con límite, espera creciente y fallo definitivo al agotarlos, y solo cuando el fallo depende del entorno y no de los datos; traducir al vocabulario de tu capa conservando la causa; y registrar y relanzar con throw e; cuando la decisión corresponde a alguien de arriba, sin caer en el log duplicado. Un catch que no hace ninguna de las cuatro es un catch mal puesto.

Y tienes la demostración de los anti-patrones: el catch vacío, que hace que un importador procese 3 ejemplares en lugar de 6 sin dejar el menor rastro y que es literalmente peor que no capturar nada; el catch (Exception e) demasiado ancho, que convierte tus propios bugs en mensajes tranquilizadores para el usuario; y las excepciones como flujo de control, con los números que lo condenan: unas 70 veces más lento que un if, con fillInStackTrace como responsable —y con el contraste revelador de que una excepción sin stack trace apenas cuesta un 50% más que el if, lo que prueba que el mecanismo de throw/catch en sí es barato y que las excepciones para lo excepcional no tienen coste apreciable.

BiblioTech ha ganado su primera defensa real: MenuBiblioTech ya no muere cuando Marta Ruiz escribe "doce". La opción del menú se reintenta indefinidamente porque hay una persona delante; los datos numéricos se reintentan con límite y cancelan la operación sin tocar el estado; la tarifa acepta el valor por defecto con solo pulsar Enter. Y todo ello capturando exclusivamente NumberFormatException, de modo que un bug real seguiría saliendo a la luz en vez de disfrazarse de "entrada inválida".

Pero la capa de servicio sigue mintiendo. Catalogo.registrar continúa devolviendo su false mudo, buscarPorReferencia sigue devolviendo null, y los constructores del módulo 3 siguen corrigiendo los datos inválidos con avisos por consola en lugar de rechazarlos. Capturar no basta: hay que señalar el fallo en el momento en que se detecta.

Eso es la lección siguiente, Throw y Throws: cómo lanzar una excepción explícitamente con throw y por qué el código posterior es inalcanzable; cómo declararla con throws y qué obliga eso al llamador; la validación de argumentos con IllegalArgumentException, IllegalStateException y Objects.requireNonNull, que por fin jubilará los avisos por consola de los constructores del módulo 3; el principio de fallar rápido; el encadenamiento de excepciones con la causa —y la demostración exacta de la información que se pierde si no la conservas—; la traducción entre capas para que la capa de servicio no filtre las excepciones de la capa de datos; y las reglas de throws en la sobrescritura de métodos. Al terminarla, Catalogo.buscarPorReferencia dejará de devolver null para siempre.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados