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
- Sintaxis y semántica del
try-catch - Qué se protege y qué se salta
- El objeto excepción y sus métodos
- Múltiples bloques
catchy la regla de orden - Multi-catch con
| - Bloques
tryanidados - Alcance de las variables declaradas dentro del
try - Qué se puede hacer dentro de un
catch - Estrategia 1: recuperarse con un valor por defecto
- Estrategia 2: reintentar
- Estrategia 3: traducir a otra excepción
- Estrategia 4: registrar y relanzar
- Anti-patrones, con demostración
- El coste real de una excepción frente a un
if - BiblioTech: el menú que ya no se rompe
- Errores Comunes y Consejos
- Ejercicios
- Sintaxis y semántica del
try-catch
try-catchLa 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:
- Las llaves son obligatorias, incluso con una sola sentencia. A diferencia de
ifofor, aquí no hay versión sin llaves. - Un
tryno puede ir solo. Debe llevar al menos uncatch, o unfinally(06-05), o ser untry-with-resources(06-06).try { ... }a secas no compila. - El parámetro del
catchse declara como el de un método: tipo y nombre. Por convención se llamae,exo algo descriptivo comofalloonoEncontrado.
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
trytermina sin excepción, todos loscatchse ignoran por completo. La ejecución sigue en la línea posterior al último bloque. - Si el
trylanza una excepción, la ejecución deltryse abandona en ese instante —las líneas siguientes no se ejecutan— y la JVM busca entre loscatchel 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
tryno 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
- 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.
- 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:
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.
- Múltiples bloques
catch y la regla de orden
catch y la regla de ordenUn 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
catchdeben ordenarse de la subclase a la superclase. Si uncatchde 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 anteriorEs 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.
- 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 IllegalArgumentExceptionTiene 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 |
- Bloques
try anidados
try anidadosUn 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
tryinterno 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.
- Alcance de las variables declaradas dentro del
try
tryUn 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 symbolLas 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 funcionaY 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 initializedEl 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 valorLa 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.
- Qué se puede hacer dentro de un
catch
catchEsta 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.
- 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.
- 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:
- Límite explícito (
MAX_INTENTOS). Unwhile (true)con reintentos es una bomba: si el servicio está caído de verdad, el programa gira para siempre consumiendo CPU. - 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.
- Al agotar los intentos, se falla. No se devuelve
nullni un valor vacío fingiendo éxito. Y se conserva el último fallo como causa, para que el stack trace muestre elCaused by:. - Solo se reintentan fallos transitorios. Reintentar una
NumberFormatExceptiono unaIllegalArgumentExceptiones 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.
- 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.
- 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.
- 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:
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.
- El coste real de una excepción frente a un
if
ifLa 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/catchen 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 constructorwritableStackTrace = falseque 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.
- 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 -> adiosY 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":
- La política de error depende del contexto, no del tipo de excepción. La misma
NumberFormatExceptionse 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). - La cancelación deja el estado intacto. Cuando
leerEnteroConLimiteagota los intentos,prestarMaterialhacereturnantes 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. - Se captura solo
NumberFormatException, noException. Si dentro del menú apareciera unNullPointerExceptionpor 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<= 0→ no hay excepción: valida con unify descarta con motivo.
- Falta algún campo →
- Usa multi-catch donde el tratamiento sea idéntico, y
catchseparados 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, lanzaIllegalStateExceptioncon 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:
Cambios clave y su razón:
- 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 elcatchque estaba ahí antes era un mero ritual. getOrDefaulten lugar de capturar elNullPointerException. Comprobar es más barato, más claro y más rápido que provocar y atrapar (apartado 14).Objects.requireNonNullpara los argumentos: fallo rápido con mensaje útil, en lugar de unNullPointerExceptionopaco tres líneas más abajo. Se desarrolla en 06-03.- La media se separa del total y la lista vacía se comprueba explícitamente: la división por cero deja de ser posible.
- No hay
printlndentro: 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:
- La misma excepción, cuatro políticas distintas.
NumberFormatExceptionse convierte en unIllegalStateExceptionque 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. - 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 deCaused by:. - No todo error necesita una excepción.
leerBooleanono captura nada: usa conjuntos y validación explícita, porqueBoolean.parseBooleanno lanza —simplemente devuelvefalseante 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
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
