Saltar a contenido

9. Gestión de excepciones: try-catch-finally

UD3 — Control de Flujo y Depuración · RA3 — Escribe y depura código, analizando y utilizando las estructuras de control del lenguaje

Aspecto Valor
Resultado de aprendizaje RA: 3 — Escribe y depura código, analizando y utilizando las estructuras de control del lenguaje.
Criterios de evaluación a los que contribuye esta sección CE 3.4 (escribe código utilizando control de excepciones)
CE 3.5 (crea programas ejecutables combinando estructuras de control)
CE 3.6 (prueba y depura los programas)
CE 3.7 (comenta y documenta el código).
Marco normativo RD 405/2023 · RD 450/2010 · RD 659/2023
Tecnología base Java 25 LTS (OpenJDK Temurin 25) — soporte hasta 2030+
Horas estimadas ~4 h (de las 30 h de la UD3).

Requisitos previos (lo que ya sabes): condiciones booleanas y operadores lógicos (sección 1 de UD3); if / else if / else, early return, validación con IO.readln y condicionales simples (sección 2 de UD3); switch expression (sección 3 de UD3); while y do-while (sección 4 de UD3); arrays unidimensionales (sección 5 de UD3); for clásico sobre arrays y Strings (sección 6 de UD3); for-each (sección 7 de UD3); bucles anidados, matrices int[][] y sentencias de salto (break, continue, return) — sección 8 de UD3. Tipos primitivos, var, String (con .equals(), .equalsIgnoreCase(), .length(), .trim(), .toLowerCase(), .charAt(), .isEmpty()), text blocks, final para constantes, operador ternario, conversión con Integer.parseInt / Double.parseDouble (de UD1). Conoces los nombres de las excepciones más comunes (NumberFormatException, ArrayIndexOutOfBoundsException, NullPointerException, ArithmeticException) porque las has visto pasar por la consola cuando tus programas se caían en secciones anteriores (asumíamos entrada válida). Aquí vamos a aprender a controlarlas en vez de sufrirlas.


9.1 ¿De qué va esta sección? — Dejar de temer los errores

Llegas a esta sección con un trauma: has escrito muchos programas que se han caído. Cuando el usuario escribía "abc" donde tú esperabas un número, veías algo como:

1
2
3
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
    at java.lang.Integer.parseInt(Integer.java:XXX)
    at MiPrograma.main(MiPrograma.java:7)

Y el programa terminaba. Sin mensaje amable para el usuario, sin oportunidad de volver a pedir el dato, sin limpiar recursos. Muerte súbita.

Esta sección va a cambiar eso. Vas a aprender a capturar esas excepciones y a reaccionar en vez de morir. La diferencia es enorme: un programa que se cae ante el primer error es frágil; un programa que maneja los errores es robusto y profesional.

La herramienta principal para ello es la sentencia try-catch-finally, que verás en detalle. Cubriremos, en este orden:

  1. ¿Qué es una excepción? La jerarquía Throwable → Error / Exception.
  2. Checked vs. unchecked: la distinción fundamental de Java.
  3. try-catch básico: capturar por tipo específico.
  4. Orden de los catch: de específico a general.
  5. catch múltiple y multitipo (catch (TypeA | TypeB e)).
  6. finally: el bloque que siempre se ejecuta.
  7. Relanzar con throw y declarar con throws**.
  8. Información de la excepción: getMessage(), getClass().getName(), printStackTrace().
  9. Las excepciones más comunes que has visto hasta ahora.
  10. Errores típicos del try-catch.
  11. Ejemplos resueltos paso a paso.

Vamos a empezar por entender qué es exactamente una excepción en Java.


9.2 ¿Qué es una excepción?

9.2.1 Definición

Una excepción es un evento que ocurre durante la ejecución de un programa y que interrumpe el flujo normal de las instrucciones. Cuando se produce una excepción, Java crea un objeto que contiene información sobre lo que ha pasado (el tipo de error, un mensaje, dónde ocurrió) y lo lanza (throws) por la pila de llamadas hasta que alguien lo captura (catches) o hasta que llega al principio del programa y este termina con un error.

En Java, todas las excepciones son objetos de clases que heredan de java.lang.Throwable. Eso significa que, técnicamente, una excepción es una instancia de una clase, con campos y métodos. Aquí solo vamos a capturarlas y manejarlas; la creación de tus propias excepciones (clases que heredan de Exception) se ve en UD4, cuando conozcas la herencia.

9.2.2 La jerarquía Throwable

Use mouse to pan and zoom
flowchart TD
    Throwable["`java.lang.Throwable`"]
    Throwable --> Error["`Error`"]
    Throwable --> Exception["`Exception`"]
    Exception --> RuntimeException["`RuntimeException\n(unchecked)`"]
    Exception --> IOException["`IOException\n(checked)`"]
    Exception --> SQLException["`SQLException\n(checked)`"]
    RuntimeException --> NumberFormatException
    RuntimeException --> NullPointerException
    RuntimeException --> ArrayIndexOutOfBoundsException
    RuntimeException --> ArithmeticException
    RuntimeException --> ClassCastException
    RuntimeException --> IllegalArgumentException
    IOException --> FileNotFoundException

Las dos ramas principales de Throwable:

  • Error: problemas graves que tu programa no debería intentar manejar. Ejemplos: OutOfMemoryError (se agotó la memoria), StackOverflowError (stack overflow por recursión infinita). No captures Error; significa que algo está muy mal y hay que arreglarlo, no atraparlo.
  • Exception: problemas manejables que tu programa puede capturar y gestionar. Esta es la rama con la que vamos a trabajar.

Dentro de Exception, hay dos subramas importantes:

  • RuntimeException y sus subclases: son unchecked (no obligatorias). Las verás en 9.3.
  • Cualquier otra Exception (IOException, SQLException, etc.): son checked (obligatorias). Las verás en 9.3.

9.2.3 Anatomía de un error en consola

Cuando una excepción no es capturada, Java imprime la traza de pila (stack trace) y termina el programa:

1
2
3
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
    at java.lang.Integer.parseInt(Integer.java:XXX)
    at MiPrograma.main(MiPrograma.java:7)

Partes:

  1. Exception in thread "main": indica en qué hilo ocurrió (casi siempre main).
  2. java.lang.NumberFormatException: el tipo de excepción (clase completa).
  3. For input string: "abc": el mensaje descriptivo que pasó el que la lanzó.
  4. Líneas at ...: la traza de pila. Cada línea indica un método que estaba en ejecución. La primera línea es donde se lanzó la excepción; las siguientes son los métodos llamadores, hasta llegar a main.

Aprender a leer esta traza es fundamental para depurar. La primera línea te dice qué pasó; la traza te dice dónde.

9.2.4 Excepción vs. error de compilación

Ojo: una excepción no es un error de compilación. Es un error en tiempo de ejecución. La diferencia:

Tipo de error Cuándo se detecta Lo ves...
Error de compilación Al compilar (antes de ejecutar). En IntelliJ, con un subrayado rojo. El programa no se ejecuta.
Excepción En tiempo de ejecución (mientras el programa corre). En la consola, con la traza de pila. El programa se cae.

Los errores de compilación los resuelves arreglando el código (añadir un ;, cambiar un tipo, etc.). Las excepciones las resuelves manejándolas (try-catch) o previniéndolas (validando antes con if, por ejemplo comprobando != null antes de acceder a una referencia o b != 0 antes de dividir).


9.3 Checked vs. unchecked: la distinción fundamental

Java divide las excepciones en dos grandes familias: checked y unchecked. Es una de las decisiones de diseño más importantes del lenguaje y conviene entenderla bien desde el principio.

9.3.1 Checked (excepciones comprobadas)

Una excepción checked es aquella que el compilador te obliga a manejar. Si llamas a un método que puede lanzar una excepción checked, tienes dos opciones:

  1. Capturarla con try-catch en el método llamador.
  2. Declararla en la firma del método con throws (para que la maneje quien te llama).

Si no haces ninguna de las dos, el programa no compila. El compilador te obliga a pensar en el error antes de que ocurra.

Ejemplos de checked:

Excepción Cuándo la lanza
IOException Operaciones de E/S: leer/escribir ficheros, sockets.
FileNotFoundException Subclase de IOException: fichero no encontrado.
SQLException Operaciones con bases de datos (JDBC).
ClassNotFoundException Class.forName("...") cuando la clase no existe.
InterruptedException Un hilo fue interrumpido mientras dormía o esperaba.

Estas excepciones se ven en unidades posteriores: IOException en UD5 (ficheros), SQLException en UD9 (JDBC). Aquí las mencionamos porque necesitas entender la distinción.

9.3.2 Unchecked (excepciones no comprobadas)

Una excepción unchecked es aquella que el compilador no te obliga a manejar. Puedes capturarla si quieres, pero si no lo haces, el programa compila igualmente. Si ocurre en runtime, se propaga y, si nadie la captura, el programa se cae con la traza de pila.

Ejemplos de unchecked:

Excepción Cuándo la lanza
RuntimeException Clase base de todas las unchecked.
NullPointerException Llamar a un método o acceder a un campo de una referencia null.
ArrayIndexOutOfBoundsException Acceder a un array con un índice fuera de rango.
NumberFormatException Integer.parseInt("abc") cuando la cadena no es numérica.
ArithmeticException División entera por cero (5 / 0).
ClassCastException Casting a un tipo incompatible ((String) objeto).
IllegalArgumentException Argumento inválido para un método.
StringIndexOutOfBoundsException charAt(i) con i fuera de rango en un String.

Estas son las que has estado viendo todo el curso. Son bugs del programador: indican que algo en tu código no está bien (no validaste una entrada, no comprobaste null, no controlaste un índice). La forma profesional de manejarlas es prevenir con if (validaciones) en vez de capturarlas con try-catch. Pero a veces no queda más remedio que capturarlas (por ejemplo, NumberFormatException en Integer.parseInt), como verás en los ejemplos.

9.3.3 Tabla comparativa

Característica Checked Unchecked
¿Quién la obliga a manejar? El compilador. Nadie (opcional).
¿Hereda de...? Exception (pero no de RuntimeException). RuntimeException (o Error).
¿Cuándo se ve? En tiempo de compilación (si no la manejas, no compila). En tiempo de ejecución (solo cuando ocurre).
¿Filosofía? "El error es recuperable: el programa debería manejarlo." "El error es un bug: arréglalo en el código."
¿Ejemplos típicos? IOException, SQLException. NullPointerException, NumberFormatException.
¿Dónde se ven en el curso? UD5 (IOException), UD9 (SQLException). UD3, en todas partes.

9.3.4 La regla práctica

"Checked: el compilador me obliga. Unchecked: yo decido."

Si llamas a un método que lanza IOException y no lo manejas, IntelliJ te marca error en rojo y el programa no compila. Si llamas a Integer.parseInt (que puede lanzar NumberFormatException, unchecked) y no lo manejas, IntelliJ no te marca nada; el programa compila y se cae en runtime si el usuario escribe "abc".

Esa diferencia define cómo trabajas con cada una:

  • Checked: el compilador te recuerda "gestiona esto". No puedes olvidarlo.
  • Unchecked: tienes que recordarlo tú mismo (validando con if o capturando con try-catch).

9.4 try-catch básico

9.4.1 Sintaxis

1
2
3
4
5
6
try {
    // Código que puede lanzar una excepción
} catch (TipoExcepcion nombreVariable) {
    // Código que se ejecuta si se lanza TipoExcepcion
}
// Aquí el programa continúa

Partes:

  • try { ... }: el bloque donde pones el código que puede fallar. Java lo ejecuta normalmente; si no ocurre ninguna excepción, el catch se ignora y el programa continúa.
  • catch (TipoExcepcion nombre) { ... }: el bloque que se ejecuta solo si dentro del try se lanza una excepción del tipo TipoExcepcion (o de una subclase). La excepción se asigna a la variable nombre para que puedas usarla dentro del catch.
  • Después del catch, el programa continúa su ejecución normal. La excepción ya está manejada.

9.4.2 Ejemplo: pedir un número con Integer.parseInt

public class PedirNumeroSeguro {

    public static void main(String[] args) {

        IO.print("Escribe un número: ");
        String entrada = IO.readln().trim();

        try {
            int numero = Integer.parseInt(entrada);
            IO.println("Has escrito el " + numero + ".");
        } catch (NumberFormatException e) {
            IO.println("Error: '" + entrada + "' no es un número válido.");
        }

        IO.println("Programa terminado.");   // Se ejecuta SIEMPRE, haya o no excepción
    }
}

Posibles salidas:

Con "42":

Has escrito el 42.
Programa terminado.

Con "abc":

Error: 'abc' no es un número válido.
Programa terminado.

Paso a paso cuando el usuario escribe "abc":

  1. Java ejecuta el bloque try.
  2. Llega a Integer.parseInt("abc").
  3. parseInt detecta que "abc" no es numérico y lanza NumberFormatException.
  4. Java interrumpe el try (las líneas después de parseInt en el try no se ejecutan) y busca un catch que acepte NumberFormatException.
  5. Lo encuentra: catch (NumberFormatException e). Ejecuta el bloque del catch.
  6. El programa continúa con la línea después del try-catch (IO.println("Programa terminado.")).

Sin el try-catch, al escribir "abc" verías:

1
2
3
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
    at java.lang.Integer.parseInt(...)
    at PedirNumeroSeguro.main(PedirNumeroSeguro.java:7)

Y el programa terminaría sin mostrar "Programa terminado.". Con try-catch, el programa sigue viviendo y responde con elegancia.

9.4.3 Diagrama de flujo del try-catch

Use mouse to pan and zoom
flowchart TD
    Inicio([Inicio]) --> Try["Entra al try"]
    Try --> Ejecuta["Ejecuta código del try"]
    Ejecuta --> Excepcion{"¿Se lanzó\nexcepción?"}
    Excepcion -->|No| Continua([Continúa tras el try-catch])
    Excepcion -->|Sí| BuscaCatch["Busca catch compatible"]
    BuscaCatch --> Encontrado{"¿Encontrado?"}
    Encontrado -->|Sí| EjecutaCatch["Ejecuta código del catch"]
    EjecutaCatch --> Continua
    Encontrado -->|No| Propaga([Propaga la excepción\nfuera del método])

9.4.4 La variable del catch

La variable del catch (e en catch (NumberFormatException e)) contiene la excepción lanzada. Puedes usarla dentro del bloque para obtener información:

1
2
3
4
5
6
7
try {
    int n = Integer.parseInt(entrada);
} catch (NumberFormatException e) {
    IO.println("Mensaje: " + e.getMessage());         // "For input string: \"abc\""
    IO.println("Tipo: " + e.getClass().getName());     // "java.lang.NumberFormatException"
    e.printStackTrace();   // imprime la traza completa en la consola
}

Vamos a ver estos métodos en la sección 9.10.

9.4.5 Solo se captura el tipo declarado

Si un try puede lanzar varios tipos de excepción, un catch (NumberFormatException e) solo captura NumberFormatException (y sus subclases). Si se lanza otra excepción, no la captura:

1
2
3
4
5
6
7
8
9
try {
    int n = Integer.parseInt(entrada);   // puede lanzar NumberFormatException
    int[] arr = new int[3];
    arr[n] = 99;   // puede lanzar ArrayIndexOutOfBoundsException si n >= 3
} catch (NumberFormatException e) {
    IO.println("No es un número.");
    // Si se lanzó ArrayIndexOutOfBoundsException, NO se captura aquí
    // y se propaga (el programa se cae)
}

Para capturar varios tipos, necesitas varios catch (ver 9.5) o un catch multitipo (ver 9.6).

9.4.6 Error: capturar Exception demasiado genérico

A veces verás código que captura Exception (la superclase de todas):

1
2
3
4
5
6
// TRampa: captura TODO, incluso lo que no esperabas
try {
    // código
} catch (Exception e) {
    IO.println("Algo pasó.");
}

Esto es una mala práctica porque:

  1. Captura cualquier excepción, incluso NullPointerException o ArrayIndexOutOfBoundsException que probablemente sean bugs que deberías arreglar, no atrapar.
  2. Oculta errores: si hay un bug en tu código dentro del try, el catch (Exception e) lo traga y no te enteras.
  3. El mensaje "Algo pasó" no es útil para el usuario ni para ti.

Regla: captura siempre el tipo más específico que esperas. Si esperas NumberFormatException, captura NumberFormatException, no Exception.


9.5 Orden de los catch: de específico a general

9.5.1 Varios catch en un mismo try

Puedes poner varios catch seguidos para manejar distintos tipos de excepción:

try {
    int n = Integer.parseInt(entrada);   // NumberFormatException
    int[] arr = new int[3];
    arr[n] = 99;                          // ArrayIndexOutOfBoundsException
    String s = null;
    IO.println(s.length());               // NullPointerException
} catch (NumberFormatException e) {
    IO.println("No es un número.");
} catch (ArrayIndexOutOfBoundsException e) {
    IO.println("Índice fuera de rango.");
} catch (NullPointerException e) {
    IO.println("Variable null.");
}

Java evalúa los catch en orden, de arriba abajo, y ejecuta el primero que sea compatible con la excepción lanzada. Los demás se ignoran.

9.5.2 Regla: específico antes que general

Como Java evalúa en orden, los catch más específicos tienen que ir antes que los más generales. Si los pones al revés, el compilador te da error:

1
2
3
4
5
6
7
8
// ERROR DE COMPILACIÓN: Exception atrapa todo, los demás son inalcanzables
try {
    // ...
} catch (Exception e) {
    IO.println("Cualquier cosa.");
} catch (NumberFormatException e) {   // INALCANZABLE: ya lo capturó el de Exception
    IO.println("No es un número.");
}

Error del compilador:

error: exception NumberFormatException has already been caught

Solución: pon los específicos primero:

// CORRECTO: específico antes que general
try {
    // ...
} catch (NumberFormatException e) {
    IO.println("No es un número.");
} catch (ArrayIndexOutOfBoundsException e) {
    IO.println("Índice fuera de rango.");
} catch (Exception e) {
    IO.println("Otro error: " + e.getMessage());   // captura lo que no se capturó arriba
}

9.5.3 ¿Cuándo poner un catch (Exception e) al final?

Poner un catch (Exception e) al final, después de los específicos, es legítimo como red de seguridad para excepciones que no anticipaste. Pero entonces es importante registrar la excepción (imprimir la traza) para que sepas que ocurrió algo inesperado:

try {
    // código complejo
} catch (NumberFormatException e) {
    IO.println("Error: debes escribir un número.");
} catch (ArrayIndexOutOfBoundsException e) {
    IO.println("Error interno: índice fuera de rango.");
} catch (Exception e) {
    // Red de seguridad: algo que no anticipamos
    IO.println("Error inesperado: " + e.getMessage());
    e.printStackTrace();   // imprime la traza para que podamos investigar
}

Ese e.printStackTrace() es importante: si solo imprimes "Error inesperado", cuando un usuario te diga "el programa me dio un error" no tendrás forma de saber qué pasó. Con printStackTrace, verás en la consola la traza y podrás diagnosticar.


9.6 catch múltiple y multitipo

9.6.1 catch multitipo (Java 7+)

Si quieres manejar varios tipos de excepción de la misma forma, puedes usar la sintaxis multitipo con |:

1
2
3
4
5
6
7
try {
    int n = Integer.parseInt(entrada);
    int[] arr = new int[3];
    arr[n] = 99;
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
    IO.println("Entrada no válida: escribe un número entre 0 y 2.");
}

Aquí un único catch captura dos tipos distintos (NumberFormatException y ArrayIndexOutOfBoundsException) y los trata igual. La variable e es del tipo común más específico (en este caso, RuntimeException, porque ambas heredan de ella).

Cuándo usarlo:

  • Cuando dos excepciones distintas se manejan exactamente igual (mismo mensaje, misma recuperación).
  • Cuando quieres reducir la repetición.

Cuándo NO usarlo:

  • Cuando quieres mensajes distintos para cada excepción. En ese caso, usa varios catch separados (ver 9.5).

9.6.2 Restricciones del catch multitipo

Los tipos en un catch multitipo no pueden estar relacionados por herencia. Por ejemplo, NumberFormatException hereda de IllegalArgumentException, así que no puedes escribir:

// ERROR: IllegalArgumentException es padre de NumberFormatException
catch (NumberFormatException | IllegalArgumentException e) { ... }

El compilador da error porque es redundante: si capturas IllegalArgumentException, ya estás capturando NumberFormatException automáticamente.

9.6.3 Cuándo usar varios catch vs. multitipo

Caso Recomendación
Varias excepciones con mensajes distintos. Varios catch separados.
Varias excepciones con el mismo manejo. catch multitipo con \|.
Una sola excepción esperada. Un solo catch específico.
Muchas excepciones distintas y un fallback genérico. Varios catch específicos + catch (Exception e) al final.

9.7 finally: el bloque que siempre se ejecuta

9.7.1 Sintaxis

1
2
3
4
5
6
7
try {
    // código que puede lanzar excepción
} catch (TipoExcepcion e) {
    // manejo de la excepción
} finally {
    // código que se ejecuta SIEMPRE, haya o no excepción
}

finally es un bloque que se ejecuta siempre, ocurra lo que ocurra en el try:

  • Si el try termina sin excepción → finally se ejecuta.
  • Si el try lanza una excepción y el catch la captura → finally se ejecuta.
  • Si el try lanza una excepción y no hay catch que la capture → finally se ejecuta (y luego la excepción se propaga).
  • Si dentro del try o del catch haces return → finally se ejecuta antes de salir del método.

9.7.2 ¿Para qué sirve finally?

finally se usa para limpieza: cerrar ficheros, liberar recursos, liberar conexiones a base de datos, etc. Es decir, código que tiene que ejecutarse sí o sí, incluso si hubo un error.

Aquí tienes un ejemplo motivacional (no ejecutable todavía, porque Scanner se ve en UD2, pero ilustra la idea):

// (Pseudocódigo ilustrativo)
Scanner sc = new Scanner(System.in);   // abrimos recurso
try {
    int n = Integer.parseInt(sc.nextLine());   // puede lanzar NumberFormatException
    IO.println("Has escrito: " + n);
} catch (NumberFormatException e) {
    IO.println("No es un número.");
} finally {
    sc.close();   // SIEMPRE cerramos el Scanner, haya o no excepción
    IO.println("Recurso cerrado.");
}

Sin finally, si parseInt lanzaba NumberFormatException, el sc.close() no se ejecutaría y el recurso quedaría abierto. Con finally, nos aseguramos de que se cierre siempre.

9.7.3 finally sin catch

finally puede aparecer sin catch: cuando no quieres manejar la excepción aquí, pero sí quieres limpiar recursos antes de propagarla:

1
2
3
4
5
try {
    int n = Integer.parseInt(entrada);
} finally {
    IO.println("Esto se ejecuta pase lo que pase.");
}   // Si parseInt lanzó NumberFormatException, se propaga (no hay catch)

Aquí finally se ejecuta, y luego la excepción se propaga (si la hubo). Útil cuando quieres limpiar pero dejar que la excepción la maneje alguien más arriba.

9.7.4 Error: finally que lanza excepción

Si dentro del finally lanzas otra excepción, se pierde la original:

1
2
3
4
5
6
7
try {
    int n = Integer.parseInt("abc");   // NumberFormatException
} finally {
    int[] arr = new int[1];
    arr[5] = 99;   // ArrayIndexOutOfBoundsException en el finally
    // La NumberFormatException original se pierde; se propaga la del finally
}

Regla: mantén el finally simple (cerrar un recurso, una operación que no falle). Si tiene lógica compleja, refactoriza.

9.7.5 try-with-resources: la alternativa moderna

En Java 7+ se introdujo try-with-resources, que automatiza el finally para cerrar recursos. Es la forma moderna de manejar recursos y elimina la necesidad de escribir finally manualmente para cierre. Se verá en la unidad 5.


9.8 Relanzar con throw y declarar con throws

9.8.1 throw: lanzar una excepción

La sentencia throw lanza una excepción. Hasta ahora, las excepciones las lanzaba Java internamente (cuando parseInt recibía "abc", por ejemplo). Pero tú también puedes lanzarlas explícitamente:

public class ValidarEdad {

    public static void main(String[] args) {

        IO.print("Edad: ");
        int edad = Integer.parseInt(IO.readln().trim());

        if (edad < 0 || edad > 120) {
            // Lanzamos una excepción unchecked
            throw new IllegalArgumentException("Edad no válida: " + edad);
        }

        IO.println("Tienes " + edad + " años.");
    }
}

Si el usuario escribe 150, verás:

1
2
3
Edad: 150
Exception in thread "main" java.lang.IllegalArgumentException: Edad no válida: 150
    at ValidarEdad.main(ValidarEdad.java:9)

Sintaxis: throw new TipoExcepcion("mensaje"); crea una nueva excepción (con new) y la lanza. El programa se interrumpe en ese punto y la excepción se propaga por la pila.

9.8.2 ¿Por qué lanzar una excepción en vez de usar if?

A veces es más limpio lanzar una excepción que devolver un código de error. Comparación:

// Opción A: devolver -1 para indicar error
static int buscar(int[] arr, int objetivo) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == objectif) return i;
    }
    return -1;   // convención: -1 significa "no encontrado"
}

// Opción B: lanzar excepción si no se encuentra
static int buscar(int[] arr, int objetivo) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == objectif) return i;
    }
    throw new IllegalArgumentException("Elemento no encontrado");
}

Cada enfoque tiene su sitio:

  • Devolver -1 es útil cuando "no encontrado" es un caso normal esperado.
  • Lanzar excepción es útil cuando "no encontrado" es un error que el llamador debería manejar.

La creación de excepciones propias (como ElementoNoEncontradoException) se ve en UD4. Aquí usamos las predefinidas del API.

9.8.3 throws: declarar que un método puede lanzar una excepción checked

Si un método puede lanzar una excepción checked, debes declararlo con throws en la firma. Esto avisa al compilador (y al llamador) de que el método puede fallar de esa forma:

1
2
3
4
// Método que declara que puede lanzar una IOException
static void leerFichero(String ruta) throws java.io.IOException {
    // ... código que lanza IOException ...
}

Si llamas a leerFichero desde otro método, el compilador te obliga a:

  1. Capturar la IOException con try-catch, O
  2. Declararla con throws IOException también en tu método.

Esto se ve a fondo en UD5 (cuando leas ficheros). Aquí lo mencionamos porque necesitas entender la palabra clave throws cuando la veas en la firma de un método.

9.8.4 Diferencia entre throw y throws

Palabra Dónde va Qué hace
throw Dentro de un método, como sentencia. Lanza una excepción concreta (con new).
throws En la firma de un método, después de los parámetros. Declara qué excepciones checked puede lanzar el método.
1
2
3
4
5
// throws: declaración en la firma
public void metodo() throws IOException {
    // throw: lanzamiento dentro del cuerpo
    throw new IOException("Algo falló");
}

Memoriza: throw lanza; throws declara.


9.9 Información de la excepción

Cuando capturas una excepción, la variable del catch contiene información útil. Vamos a ver los métodos más importantes.

9.9.1 getMessage()

Devuelve el mensaje descriptivo de la excepción. Es lo que pasa el que la lanzó:

1
2
3
4
5
6
try {
    int n = Integer.parseInt("abc");
} catch (NumberFormatException e) {
    IO.println(e.getMessage());
    // Imprime: "For input string: \"abc\""
}

Útil para mostrar al usuario o para log. Es lo que más vas a usar.

9.9.2 getClass().getName()

Devuelve el nombre completo de la clase de la excepción:

1
2
3
4
5
6
try {
    int n = Integer.parseInt("abc");
} catch (NumberFormatException e) {
    IO.println(e.getClass().getName());
    // Imprime: "java.lang.NumberFormatException"
}

Útil para depurar o para logging, pero normalmente no se lo muestras al usuario (no significa nada para él).

9.9.3 printStackTrace()

Imprime la traza de pila completa en la consola (en System.err). Es lo que ves cuando una excepción no capturada termina el programa:

1
2
3
4
5
try {
    int n = Integer.parseInt("abc");
} catch (NumberFormatException e) {
    e.printStackTrace();
}

Salida (aproximada):

1
2
3
java.lang.NumberFormatException: For input string: "abc"
    at java.lang.Integer.parseInt(Integer.java:XXX)
    at MiPrograma.main(MiPrograma.java:7)

printStackTrace() es muy útil durante el desarrollo para depurar. En producción, normalmente usas logging (UD5 / UD7) en vez de imprimir directamente, pero printStackTrace te vale para el curso.

9.9.4 getCause()

Algunas excepciones envuelven a otra excepción (la "causa"). getCause() devuelve la excepción original:

1
2
3
4
5
6
7
try {
    // código que puede lanzar una excepción con causa
} catch (Exception e) {
    if (e.getCause() != null) {
        IO.println("Causa original: " + e.getCause().getMessage());
    }
}

Lo verás sobre todo en UD5 y UD9, cuando trabajes con APIs que envuelven excepciones.

9.9.5 Resumen de métodos

Método Qué devuelve Cuándo usarlo
getMessage() Mensaje descriptivo. Mostrar al usuario, logging básico.
getClass().getName() Nombre completo de la clase. Logging, depuración.
printStackTrace() Traza completa en consola. Depuración durante el desarrollo.
getCause() Excepción original (si existe). Cuando la excepción envuelve a otra.

9.10 Las excepciones más comunes que has visto

Aquí tienes una lista de las excepciones unchecked más comunes que te aparecerán en este curso. Para cada una, cuándo ocurre y cómo evitarla.

9.10.1 NumberFormatException

Cuándo: Integer.parseInt("abc") o Double.parseDouble("xyz") cuando la cadena no se puede convertir a número.

Cómo evitarla: captúrala con try-catch para mostrar un mensaje amable y volver a pedir el dato (patrón que verás en 9.13.1). Esta es la forma profesional de validar entrada numérica y la que vas a usar a partir de ahora.

9.10.2 ArrayIndexOutOfBoundsException

Cuándo: acceder a arr[i] con i fuera del rango [0, arr.length - 1].

Cómo evitarla: comprueba i < arr.length antes de acceder (patrón del for clásico). Es la trampa nº 1 con arrays (ver sección 5).

9.10.3 NullPointerException

Cuándo: llamar a un método o acceder a un campo de una referencia null. Por ejemplo:

String s = null;
IO.println(s.length());   // NullPointerException

Cómo evitarla: comprueba != null antes de usar referencias que pueden ser null. En arrays de String inicializados con new, los elementos son null por defecto (ver sección 5).

9.10.4 ArithmeticException

Cuándo: división entera por cero (5 / 0). Con double no lanza excepción; devuelve Infinity o NaN.

Cómo evitarla: comprueba b != 0 antes de dividir. Es lo que hicimos en la calculadora de la sección 2.

9.10.5 StringIndexOutOfBoundsException

Cuándo: texto.charAt(i) con i fuera del rango [0, texto.length() - 1].

Cómo evitarla: comprueba i < texto.length() antes de acceder. Es el equivalente de ArrayIndexOutOfBoundsException para Strings.

9.10.6 ClassCastException

Cuándo: casting a un tipo incompatible. Por ejemplo:

Object o = "hola";
Integer n = (Integer) o;   // ClassCastException

Cómo evitarla: en UD3 no vas a usar casting (necesitas POO). Lo verás en UD7 con pattern matching, que es la forma moderna de evitarlo.

9.10.7 IllegalArgumentException

Cuándo: la lanza un método cuando le pasas un argumento inválido. Por ejemplo, Integer.parseInt lanza NumberFormatException, que hereda de IllegalArgumentException.

Cómo evitarla: lee la documentación del método para saber qué argumentos espera.

9.10.8 InputMismatchException (si usas Scanner)

Cuándo: sc.nextInt() cuando el usuario escribe algo que no es un entero (por ejemplo, "abc").

Cómo evitarla: en este curso usamos IO.readln() y convertimos con Integer.parseInt, así que no la verás mucho. Si usas Scanner.nextInt(), captúrala con try-catch o prevalida con sc.hasNextInt().

9.10.9 Tabla resumen

Excepción Tipo Cuándo Cómo evitarla
NumberFormatException unchecked parseInt("abc") try-catch (validación robusta de entrada)
ArrayIndexOutOfBoundsException unchecked arr[5] en un array de 3 i < arr.length
NullPointerException unchecked s.length() con s == null != null
ArithmeticException unchecked 5 / 0 b != 0
StringIndexOutOfBoundsException unchecked "hola".charAt(10) i < texto.length()
ClassCastException unchecked casting incompatible (POO — UD7)
IllegalArgumentException unchecked argumento inválido documentación del método
InputMismatchException unchecked Scanner.nextInt() con no-entero IO.readln + parseInt o hasNextInt
IOException checked E/S ficheros UD5 (try-catch obligatorio)
SQLException checked JDBC UD9 (try-catch obligatorio)

9.11 Errores típicos del try-catch

Antes de pasar a los ejemplos, repasamos las trampas más típicas.

9.11.1 capturar Exception demasiado genérico

Ya la vimos en 9.4.6: captura todo, oculta bugs. Solución: captura el tipo específico que esperas.

9.11.2 orden de catch incorrecto

Ya la vimos en 9.5.2: el general antes que el específico da error de compilación. Solución: específico antes que general.

9.11.3 catch vacío (tragar la excepción)

1
2
3
4
5
6
// MAL: traga la excepción, no se ve el error
try {
    int n = Integer.parseInt(entrada);
} catch (NumberFormatException e) {
    // vacío: no hacemos nada
}

Solución: siempre haz algo en el catch: muestra un mensaje, loguea, lanza de nuevo, o devuelve un valor por defecto. Un catch vacío es peor que no tener try-catch, porque oculta el error.

9.11.4 return dentro del try sin finally

1
2
3
4
5
6
7
8
// Si parseInt no lanza excepción, 'recurso.close()' NO se ejecuta
try {
    int n = Integer.parseInt(entrada);
    return n;   // salimos del método
} catch (NumberFormatException e) {
    return -1;
}
// 'recurso.close()' iría aquí, pero nunca se ejecuta si hay 'return' arriba

Solución: usa try-with-resources para cierre automático, o pon el close() en un finally.

9.11.5 capturar para prevenir bugs en vez de arreglarlos

// MAL: capturas NullPointerException que es un bug
try {
    String s = metodoQueDevuelveNull();
    IO.println(s.length());
} catch (NullPointerException e) {
    IO.println("Algo fue null.");
}
// Mejor: arregla el bug comprobando null
String s = metodoQueDevuelveNull();
if (s != null) {
    IO.println(s.length());
} else {
    IO.println("El método devolvió null.");
}

Regla: las unchecked son bugs. Arréglalas con if, no las captures. Las checked son errores esperados (fichero no existe, conexión caída): esas sí se capturan.

9.11.6 olvidar que finally se ejecuta siempre

Si pones un return dentro del try, el finally se ejecuta antes de salir:

1
2
3
4
5
6
7
int metodo() {
    try {
        return 10;
    } finally {
        IO.println("Esto se imprime antes de devolver 10.");
    }
}

Salida antes de devolver 10:

Esto se imprime antes de devolver 10.

Solución: ten en cuenta el finally cuando uses return dentro de un try.


9.12 Ejemplos resueltos paso a paso

Cinco ejemplos completos que integran todo lo aprendido.

9.12.1 Ejemplo 1: pedir un número con try-catch y volver a pedir

public class PedirNumeroBucle {

    public static void main(String[] args) {

        int numero = 0;
        boolean valido = false;

        while (!valido) {
            IO.print("Escribe un número entero: ");
            String entrada = IO.readln().trim();

            try {
                numero = Integer.parseInt(entrada);
                valido = true;   // si llegamos aquí, no hubo excepción
            } catch (NumberFormatException e) {
                IO.println("Error: '" + entrada + "' no es un número. Intenta de nuevo.");
            }
        }

        IO.println("Has escrito el " + numero + ".");
    }
}

Salida de ejemplo:

1
2
3
4
5
6
Escribe un número entero: abc
Error: 'abc' no es un número. Intenta de nuevo.
Escribe un número entero: 12.5
Error: '12.5' no es un número. Intenta de nuevo.
Escribe un número entero: 42
Has escrito el 42.

Puntos clave:

  1. while (!valido): repite hasta que se consiga un número válido.
  2. try-catch: si parseInt lanza NumberFormatException, el catch muestra el mensaje y valido sigue false, así que el bucle se repite.
  3. Si no lanza excepción, numero = Integer.parseInt(entrada) tiene éxito y valido = true, así que el bucle termina.
  4. Integer.parseInt lanza en 12.5 porque los decimales con punto no son enteros. Si quisieras aceptar decimales y convertir a int, harías Double.parseDouble y luego cast.

9.12.2 Ejemplo 2: varios catch para distintos errores

public class VariosCatch {

    public static void main(String[] args) {

        int[] arr = {10, 20, 30};

        IO.print("Índice: ");
        String entrada = IO.readln().trim();

        try {
            int i = Integer.parseInt(entrada);   // puede lanzar NumberFormatException
            IO.println("Elemento: " + arr[i]);   // puede lanzar ArrayIndexOutOfBoundsException
        } catch (NumberFormatException e) {
            IO.println("Error: el índice debe ser un número entero.");
        } catch (ArrayIndexOutOfBoundsException e) {
            IO.println("Error: el índice debe estar entre 0 y " + (arr.length - 1) + ".");
        }
    }
}

Salidas posibles:

Entrada Salida
"1" Elemento: 20
"abc" Error: el índice debe ser un número entero.
"5" Error: el índice debe estar entre 0 y 2.

Puntos clave:

  1. Dos posibles excepciones en el try: NumberFormatException (del parseInt) y ArrayIndexOutOfBoundsException (del arr[i]).
  2. Dos catch específicos, cada uno con su mensaje.
  3. El orden no importa aquí porque las dos excepciones son "hermanas" (no heredan una de otra), pero pon siempre las específicas antes que las generales.

9.12.3 Ejemplo 3: catch multitipo

public class CatchMultitipo {

    public static void main(String[] args) {

        int[] arr = {10, 20, 30};

        IO.print("Índice: ");
        String entrada = IO.readln().trim();

        try {
            int i = Integer.parseInt(entrada);
            IO.println("Elemento: " + arr[i]);
        } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
            // Un único catch para ambos tipos
            IO.println("Entrada no válida: escribe un número entre 0 y " + (arr.length - 1) + ".");
        }
    }
}

Puntos clave:

  1. catch (NumberFormatException | ArrayIndexOutOfBoundsException e): un solo catch captura ambos tipos.
  2. Mensaje unificado: como ambos errores significan "entrada no válida", un único mensaje es suficiente.
  3. e es del tipo común más específico (aquí RuntimeException, padre de ambas).

9.12.4 Ejemplo 4: finally para limpieza

public class FinallyLimpieza {

    public static void main(String[] args) {

        IO.println("=== Inicio del programa ===");

        try {
            IO.print("Escribe un número: ");
            String entrada = IO.readln().trim();
            int n = Integer.parseInt(entrada);   // puede lanzar NumberFormatException
            IO.println("Has escrito: " + n);
        } catch (NumberFormatException e) {
            IO.println("Error: no es un número.");
        } finally {
            // Se ejecuta SIEMPRE, haya o no excepción
            IO.println("=== Fin del bloque try-catch ===");
        }

        IO.println("=== Fin del programa ===");
    }
}

Salida con "42":

1
2
3
4
5
=== Inicio del programa ===
Escribe un número: 42
Has escrito: 42
=== Fin del bloque try-catch ===
=== Fin del programa ===

Salida con "abc":

1
2
3
4
5
=== Inicio del programa ===
Escribe un número: abc
Error: no es un número.
=== Fin del bloque try-catch ===
=== Fin del programa ===

Puntos clave:

  1. === Fin del bloque try-catch === se imprime siempre, tanto si parseInt tuvo éxito como si lanzó excepción.
  2. finally se ejecuta después del try (si no hay excepción) o después del catch (si la hay).
  3. Aquí no hay recurso que cerrar, así que es un ejemplo didáctico. En la práctica, finally se usa para sc.close() o similar; pero para eso usamos try-with-resources.

9.13 Buenas prácticas del try-catch

9.13.1 Las 10 reglas del manejo de excepciones

  1. Captura el tipo específico que esperas, no Exception a secas.
  2. Orden de los catch: específico antes que general.
  3. Nunca dejes un catch vacío: muestra un mensaje, loguea, o lanza de nuevo.
  4. Las unchecked son bugs: arréglalas con if, no las captures (salvo casos puntuales como NumberFormatException en parseInt).
  5. Las checked son errores esperados: captúralas o decláralas con throws.
  6. Muestra mensajes útiles al usuario, no "Algo pasó". Incluye qué se esperaba y qué se recibió.
  7. En el catch, loguea la excepción: e.printStackTrace() durante el desarrollo; logging en producción.
  8. No captures para ignorar: si no sabes qué hacer con una excepción, mejor déjala propagar.
  9. Mantén el try pequeño: solo lo que puede lanzar la excepción. No metas en el try código que no tiene nada que ver con la excepción.

9.13.2 Cuándo refactorizar

Síntoma Refactoriza a
catch (Exception e) que captura todo. Varios catch específicos.
catch vacío. Añadir mensaje, log, o lanza de nuevo.
try-catch dentro de un while para validar. Es el patrón correcto: cuando llegues a UD2/UD6 conocerás String.matches para prevalidar sin excepción, pero para validar entrada numérica el try-catch es la forma estándar.
catch que captura unchecked que es un bug. Arregla con if antes del try.

9.13.3 Comentar el try-catch

// MAL: no explica qué excepción esperas ni por qué
try {
    int n = Integer.parseInt(entrada);
} catch (NumberFormatException e) {
    IO.println("Error.");
}

// BIEN: explica la intención
// parseInt puede lanzar NumberFormatException si el usuario escribe texto;
// la capturamos para mostrar un mensaje amable y volver a pedir el dato
try {
    int n = Integer.parseInt(entrada);
} catch (NumberFormatException e) {
    IO.println("Error: debes escribir un número entero. Intenta de nuevo.");
}

9.14 Glosario rápido

Término Significado
Excepción Evento que interrumpe el flujo normal; representado como un objeto de subclase de Throwable.
Throwable Superclase de todas las excepciones y errores en Java.
Error Subclase de Throwable para problemas graves no manejables (OutOfMemoryError, StackOverflowError).
Exception Subclase de Throwable para problemas manejables.
RuntimeException Subclase de Exception que agrupa las excepciones unchecked.
Checked Excepción que el compilador obliga a manejar (IOException, SQLException).
Unchecked Excepción que no obliga a manejar (NullPointerException, NumberFormatException).
try Bloque que contiene código que puede lanzar una excepción.
catch Bloque que se ejecuta si se lanza una excepción del tipo declarado.
finally Bloque que se ejecuta siempre, haya o no excepción.
throw Sentencia que lanza una excepción (con new).
throws Declaración en la firma de un método: indica que puede lanzar una excepción checked.
Traza de pila (stack trace) Lista de métodos por los que pasó la excepción; la imprime printStackTrace().
getMessage() Método de la excepción que devuelve el mensaje descriptivo.
getClass().getName() Devuelve el nombre completo de la clase de la excepción.
printStackTrace() Imprime la traza completa en System.err.
getCause() Devuelve la excepción original (si la excepción envuelve a otra).
NumberFormatException Unchecked: parseInt recibe una cadena no numérica.
NullPointerException Unchecked: llamar a método de una referencia null.
ArrayIndexOutOfBoundsException Unchecked: acceder a array con índice fuera de rango.
ArithmeticException Unchecked: división entera por cero.
ClassCastException Unchecked: casting a tipo incompatible.
IOException Checked: error de E/S (ficheros, sockets) — UD5.
SQLException Checked: error de base de datos — UD9.

9.15 Actividades propuestas

Cinco actividades graduadas. Hazlas en orden. Las soluciones se publicarán en Aules.

9.15.1 Actividad 1 — Pedir un número hasta que sea válido

Objetivo: practicar try-catch con Integer.parseInt en un bucle.

Enunciado: Escribe un programa que pida al usuario un número entero hasta que escriba uno válido. Si escribe algo que no es un entero, muestra "Error: no es un número. Intenta de nuevo." y vuelve a pedirlo. Cuando sea válido, muestra el número y termina.

Requisitos:

  • Usa while (true) con break (sección 8) dentro del try para salir cuando el número sea válido.
  • Usa try-catch con NumberFormatException.
  • Aquí sí queremos practicar try-catch: el patrón "pedir hasta que sea válido" es el objetivo de la actividad. No prevalides con otros métodos.

Entrega: archivo PedirNumeroValido.java en el paquete jams.programacion.ud3.

Criterios evaluados: CE 3.4, CE 3.5, CE 3.7.

9.15.2 Actividad 2 — Calculadora con varios catch

Objetivo: practicar varios catch para distintos tipos de excepción.

Enunciado: Escribe una calculadora simple que pida dos números (como String) y una operación (+, -, *, /). Convierte los números con Double.parseDouble y muestra el resultado. Captura las siguientes excepciones con mensajes específicos:

  • NumberFormatException: "Error: debes escribir números válidos."
  • ArithmeticException: "Error: división por cero." (aunque double / 0.0 no lanza ArithmeticException, sí lo hace int / 0. Si usas double, comprueba el divisor con if (b == 0) en vez de capturar.)

Requisitos:

  • Usa dos catch separados (no multitipo): uno para NumberFormatException, uno para ArithmeticException.
  • Si la operación no es una de las cuatro, muestra "Operación no válida." (con switch y default).
  • Después de un error, el programa termina (no vuelve a pedir).

Entrega: archivo CalculadoraExcepciones.java en el paquete jams.programacion.ud3.

Criterios evaluados: CE 3.4, CE 3.5, CE 3.7.

9.15.3 Actividad 3 — catch multitipo y finally

Objetivo: combinar catch multitipo con finally.

Enunciado: Escribe un programa que:

  1. Pida un índice (como String).
  2. Conviértalo a int con Integer.parseInt (puede lanzar NumberFormatException).
  3. Acceda a array[índice] donde array = {10, 20, 30, 40, 50} (puede lanzar ArrayIndexOutOfBoundsException).
  4. Muestre el elemento si todo va bien.
  5. Use un catch multitipo para NumberFormatException | ArrayIndexOutOfBoundsException con un mensaje unificado: "Entrada no válida: escribe un número entre 0 y 4."
  6. Use un finally que imprima "Operación terminada." siempre, haya o no excepción.

Entrega: archivo CatchMultitipoFinally.java en el paquete jams.programacion.ud3.

Criterios evaluados: CE 3.4, CE 3.5, CE 3.7.

9.15.4 Actividad 4 — Cuestionario de repaso

Responde por escrito:

  1. ¿Qué es una excepción en Java?
  2. ¿Cuál es la diferencia entre Error y Exception?
  3. ¿Cuál es la diferencia entre checked y unchecked? Pon un ejemplo de cada una.
  4. Escribe la sintaxis básica de try-catch.
  5. ¿Por qué los catch específicos tienen que ir antes que los generales?
  6. ¿Qué es un catch multitipo y cuándo lo usarías?
  7. ¿Para qué sirve finally? ¿Cuándo se ejecuta?
  8. ¿Qué es AutoCloseable?
  9. ¿Cuál es la diferencia entre throw y throws?
  10. ¿Qué hace e.getMessage(), e.getClass().getName() y e.printStackTrace()?
  11. ¿Por qué nunca debes dejar un catch vacío?
  12. Nombra 5 excepciones unchecked y di cuándo se lanzan.
  13. ¿Por qué se dice que las unchecked son "bugs" y las checked son "errores esperados"?
  14. ¿Qué excepción lanza Integer.parseInt("abc")? ¿Y new int[3][5] accediendo a [3][0]?

Entrega: archivo cuestionario_seccion9.md con tus respuestas.

Criterios evaluados: CE 3.4, CE 3.7.


Cierre de la sección. Llegar hasta aquí es mérito. Has pasado de temer los errores a manejarlos con elegancia. Eso es un cambio de mentalidad importante: ya no escribes programas que "esperan que todo vaya bien", sino programas que anticipan que algo puede fallar y reaccionan en vez de morir. La regla más importante que te llevas de esta sección es: las unchecked son bugs, arréglalas con if; las checked son errores esperados, captúralas con try-catch. Si interiorizas esa distinción, vas a escribir código mucho más robusto que la media de los principiantes. Y recuerda: nunca dejes un catch vacío. Un catch vacío es peor que no tener try-catch, porque oculta el error. Nos vemos en la sección 10, donde vas a conocer el depurador de IntelliJ, la herramienta que va a cambiar tu forma de programar para siempre.