Excepciones en Java — teoría con notas, alumnos y trazas en rojo (y el «7,5» que tumbó mi programa entero)
Mi primer programa de notas leía las calificaciones como texto y calculaba la media. Lo probé con "8.5", "6" y "9" y funcionaba perfecto. Luego se lo pasé a un compañero, que escribió las notas como las escribe cualquiera en España, con coma: "7,5". Y la consola se llenó de rojo:
Exception in thread "main" java.lang.NumberFormatException: For input string: "7,5" at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054) at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110) at java.base/java.lang.Double.parseDouble(Double.java:792) at Media.convertir(Media.java:4) at Media.media(Media.java:10) at Media.main(Media.java:17)
Una sola nota mal escrita y el programa entero se paró, sin calcular nada. En ese momento esas líneas me parecieron un castigo. Hoy sé que son lo más útil que te puede dar Java: te dicen qué ha pasado, dónde y cómo llegaste ahí. En este artículo vamos a aprender a leerlas y, sobre todo, a evitar que una nota rara tumbe el programa.
Seguimos con los alumnos de clases y objetos y, al final, usaremos lo que viste en herencia para crear nuestras propias excepciones.
Tabla de Contenidos
Qué es una excepción
Una excepción es un objeto que Java crea cuando algo sale mal mientras el programa se ejecuta: convertir "7,5" a número, acceder a la posición 5 de un array de 3, llamar a un método sobre null… Java «lanza» ese objeto y, si nadie lo captura, el programa se detiene y muestra la traza.
Este es el programa de mi historia:
public class Media {
static double convertir(String texto) {
return Double.parseDouble(texto.trim());
}
static double media(String[] notas) {
double suma = 0;
for (String n : notas) {
suma += convertir(n);
}
return suma / notas.length;
}
public static void main(String[] args) {
String[] notas = {"8.5", "7,5", "6"};
System.out.println("Media: " + media(notas));
}
}
Fíjate en que no aparece "Media: " en ningún momento. En cuanto salta la excepción, Java abandona convertir, después media y después main, sin terminar ninguno.
Ya te has encontrado con varias: ArrayIndexOutOfBoundsException en arrays, NullPointerException en clases y objetos, ClassCastException en herencia. Hasta ahora las evitábamos comprobando antes. Hoy aprendemos a tratarlas cuando no se pueden evitar.
Cómo leer una traza
La traza (stack trace) se lee así:
- Primera línea: el tipo de excepción y el mensaje.
NumberFormatException: For input string: "7,5"ya lo dice casi todo: se intentó convertir"7,5"a número y no se pudo. - **Líneas
at …: la pila de llamadas, de la más reciente (arriba) a la más antigua** (abajo). Cada una dice método, archivo y línea. - Busca tu código: las líneas que empiezan por
java.base/son de Java por dentro; no las vas a tocar. La primera línea que es tuya (Media.convertir(Media.java:4)) es donde saltó el problema en tu programa. - Sigue hacia abajo para saber cómo llegaste:
main(línea 17) llamó amedia(línea 10), que llamó aconvertir(línea 4).
No leas la traza entera de golpe. Primera línea (qué) → primera línea de tu código (dónde) → el resto solo si necesitas saber el camino. En IntelliJ, el nombre del archivo y la línea son un enlace que te lleva directo.
Los números de línea de java.base/… cambian según tu versión de Java. Los de tu código, no: Media.java:4 es siempre la línea 4 de tu archivo.
try y catch: capturar la excepción
Para que el programa no se pare, metemos el código que puede fallar en un bloque try y decimos qué hacer si falla en un catch:
String[] notas = {"8.5", "7,5", "6"};
double suma = 0;
int validas = 0;
for (String t : notas) {
try {
double n = Double.parseDouble(t);
suma += n;
validas++;
System.out.println("OK: " + n);
} catch (NumberFormatException e) {
System.out.println("Ignorada -> " + e.getMessage());
}
}
System.out.println("Media de " + validas + " notas: " + suma / validas);
Salida:
OK: 8.5 Ignorada -> For input string: "7,5" OK: 6.0 Media de 2 notas: 7.25
Qué se ejecuta y qué no
Este es el punto que más cuesta. Cuando una línea del try lanza una excepción:
- **El resto del
tryse salta.** Con"7,5", nisuma += n, nivalidas++, ni elprintlnde «OK» se ejecutan. - **Java busca un
catchque encaje** con el tipo de la excepción. Aquí,NumberFormatException: encaja, y se ejecuta su bloque. - El programa sigue después del
try/catch, como si nada. El bucle pasa a"6"con normalidad.
e es el objeto excepción. Lo más útil que tiene es e.getMessage(), el mensaje (aquí, For input string: "7,5").
Poner el try fuera del bucle. Entonces, al fallar "7,5", se salta el resto del try… que es el bucle entero, y "6" ya no se procesa. Si quieres que cada elemento tenga su oportunidad, el try va dentro del bucle.
Varios catch
Un mismo try puede tener varios catch, uno por tipo de problema:
String[] datos = {"8", "abc"};
int[] posiciones = {0, 1, 5};
for (int p : posiciones) {
try {
double n = Double.parseDouble(datos[p]);
System.out.println("Posición " + p + ": " + n);
} catch (NumberFormatException e) {
System.out.println("Posición " + p + ": no es un número");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Posición " + p + ": " + e.getMessage());
}
}
Posición 0: 8.0 Posición 1: no es un número Posición 5: Index 5 out of bounds for length 2
Java prueba los catch de arriba abajo y entra en el primero que encaje. Si ninguno encaja, la excepción sigue su camino como si no hubiera try.
Poner primero un catch (Exception e) y debajo otro más concreto. Exception es la madre de casi todas, así que lo captura todo y el de abajo nunca podría ejecutarse. Java no lo permite: exception NumberFormatException has already been caught. Regla: de lo más concreto a lo más general.
Si dos excepciones se tratan igual, puedes juntarlas con | (multi-catch): catch (NumberFormatException | ArrayIndexOutOfBoundsException e).
finally: lo que se ejecuta siempre
Un bloque finally se ejecuta siempre al salir del try: haya excepción o no, e incluso si dentro hay un return.
static double leer(String texto) {
try {
System.out.println("1. Intento convertir \"" + texto + "\"");
double n = Double.parseDouble(texto);
System.out.println("2. Convertido: " + n);
return n;
} catch (NumberFormatException e) {
System.out.println("3. No es un número");
return -1;
} finally {
System.out.println("4. finally: esto sale siempre");
}
}
System.out.println(leer("9"));
System.out.println(leer("nueve"));
1. Intento convertir "9" 2. Convertido: 9.0 4. finally: esto sale siempre 9.0 1. Intento convertir "nueve" 3. No es un número 4. finally: esto sale siempre -1.0
Fíjate en el orden: con "9" no se pasa por el 3; con "nueve" no se pasa por el 2. Pero el 4 sale las dos veces, antes de que el println de fuera reciba el valor devuelto: el return espera a que termine el finally.
¿Para qué sirve? Para cosas que hay que hacer pase lo que pase: cerrar un fichero, liberar una conexión, dejar un mensaje en un registro.
La familia de las excepciones
Las excepciones son clases, y forman una jerarquía de herencia, como la de Persona → Alumno:
Throwable
├── Error (problemas graves de la máquina: no se tratan)
└── Exception
├── IOException (checked: ficheros, red…)
│ └── FileNotFoundException
├── (tus excepciones propias, si heredan de Exception)
└── RuntimeException (unchecked: errores de programación)
├── NullPointerException
├── ArithmeticException
├── ClassCastException
├── IllegalArgumentException
│ └── NumberFormatException
└── IndexOutOfBoundsException
└── ArrayIndexOutOfBoundsException
Como es herencia, un catch (IllegalArgumentException e) también captura una NumberFormatException: es una IllegalArgumentException. Es el mismo instanceof que viste en herencia.
| Excepción | Cuándo salta | Ejemplo |
|---|---|---|
NumberFormatException | Convertir texto que no es un número | Integer.parseInt("8a") |
ArrayIndexOutOfBoundsException | Índice fuera del array | notas[5] con 3 elementos |
NullPointerException | Usar algo que vale null | nombre.length() con nombre = null |
ArithmeticException | División entera entre 0 | 10 / 0 |
ClassCastException | Cast a un tipo que no es | (Alumno) profesor |
IllegalArgumentException | Un argumento no válido (la lanzas tú) | setNota(12) |
FileNotFoundException | El fichero no existe | new FileReader("x.txt") |
10.0 / 0 no lanza nada: con double da Infinity. ArithmeticException solo aparece con enteros.
Checked y unchecked
Esta distinción es la que más confunde, y en realidad es sencilla:
- Unchecked (
RuntimeExceptiony sus hijas): normalmente son errores de programación que se evitan comprobando antes (un índice mal calculado, unnull). Java no te obliga a tratarlas. - Checked (el resto de
Exception, comoIOException): son problemas externos que pueden pasar aunque tu código esté perfecto (el fichero no existe, se cae la red). Java te obliga a decidir qué hacer con ellas.
Si llamas a algo que lanza una checked y no haces nada, ni compila:
FileReader r = new FileReader("notas.txt");
error: unreported exception FileNotFoundException; must be caught or declared to be thrown
El mensaje te da las dos opciones:
- Capturarla (caught): rodearla con
try/catch. - Declararla (declared to be thrown): añadir
throwsa tu método para que la trate quien te llame.
static void abrir() throws FileNotFoundException { // "yo no la trato: que la trate quien me llame"
FileReader r = new FileReader("notas.txt");
}
throws (con s, en la firma del método) avisa de que algo puede salir. throw (sin s, dentro del código) lanza la excepción. Son dos cosas distintas que se confunden mucho; ahora vamos con throw.
throw: lanzar tus propias excepciones
Hasta ahora las excepciones las lanzaba Java. Pero tú también puedes lanzarlas cuando recibes datos que no tienen sentido:
static void validarNota(double nota) {
if (nota < 0 || nota > 10) {
throw new IllegalArgumentException("Nota fuera de rango: " + nota);
}
}
try {
validarNota(7);
System.out.println("7 es válida");
validarNota(12);
System.out.println("Esta línea no se ejecuta");
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
7 es válida Error: Nota fuera de rango: 12.0
throw new … crea el objeto excepción (con su mensaje) y lo lanza. Desde ese momento el método se corta, igual que si fuera un return, pero en lugar de devolver un valor «manda un aviso» hacia arriba.
¿Por qué es mejor que el println("Nota no válida") que usábamos en clases y objetos? Porque un println solo lo ve una persona en la consola; la excepción la puede detectar y tratar el código que llamó al método, y decidir qué hacer (pedir otra nota, saltarse ese alumno, avisar en la interfaz…).
Excepciones propias
IllegalArgumentException sirve para casi todo, pero a veces conviene una excepción con nombre propio, que diga exactamente qué ha pasado y lleve datos útiles. Se crea con herencia:
public class NotaInvalidaException extends Exception {
private final double nota; // guardamos el dato que falló
public NotaInvalidaException(double nota) {
super("Nota fuera de rango: " + nota + " (debe estar entre 0 y 10)");
this.nota = nota;
}
public double getNota() { return nota; }
}
- Hereda de
Exception, así que es checked: quien usesetNotaestará obligado a tratarla. (Si heredara deRuntimeException, sería unchecked.) super(mensaje)le pasa el texto al constructor deException, y asígetMessage()funciona sin escribirlo tú.- Puede tener atributos y métodos propios, como
getNota().
Ahora el Alumno la lanza:
public class Alumno {
private final String nombre;
private double nota;
public Alumno(String nombre) {
this.nombre = nombre;
}
public void setNota(double nota) throws NotaInvalidaException {
if (nota < 0 || nota > 10) {
throw new NotaInvalidaException(nota); // se corta aquí: la nota no cambia
}
this.nota = nota;
}
public String getNombre() { return nombre; }
public double getNota() { return nota; }
}
Y quien lo usa decide qué hacer con cada problema:
public class Matricula {
// No la trata: la deja pasar hacia arriba con throws
static void ponerNota(Alumno a, String texto) throws NotaInvalidaException {
double nota = Double.parseDouble(texto); // puede lanzar NumberFormatException
a.setNota(nota); // puede lanzar NotaInvalidaException
}
public static void main(String[] args) {
Alumno ana = new Alumno("Ana");
String[] intentos = {"12", "ocho", "8.5"};
for (String t : intentos) {
try {
ponerNota(ana, t);
System.out.println("Nota guardada: " + ana.getNota());
} catch (NotaInvalidaException e) {
System.out.println(e.getMessage() + " -> me diste " + e.getNota());
} catch (NumberFormatException e) {
System.out.println("\"" + t + "\" no es un número");
}
}
}
}
Nota fuera de rango: 12.0 (debe estar entre 0 y 10) -> me diste 12.0 "ocho" no es un número Nota guardada: 8.5
Propagar: la excepción sube por la pila
ponerNota no tiene try: declara throws NotaInvalidaException y la deja subir hasta main, que es quien la trata. Esto se llama propagar. Si nadie la tratara, llegaría hasta arriba del todo y veríamos su traza. Por ejemplo, con un main que también la declara con throws:
Exception in thread "main" NotaInvalidaException: Nota fuera de rango: 12.0 (debe estar entre 0 y 10) at Alumno.setNota(Alumno.java:12) at SinTratar.ponerNota(SinTratar.java:4) at SinTratar.main(SinTratar.java:9)
Ahora la traza tiene todo tu código: se lanzó en la línea 12 de Alumno (el throw), pasando por ponerNota y por main.
Tratar todas las excepciones con un catch (Exception e) {} vacío «para que no moleste». El programa ya no se para, pero tampoco te enteras de que algo ha ido mal, y el error aparece mucho después y en otro sitio. Si capturas, haz algo: informa, corrige o vuelve a lanzar.
try-with-resources: cerrar ficheros sin olvidarte
Un fichero abierto hay que cerrarlo, haya error o no. Antes se hacía en un finally; desde Java 7 hay una forma más cómoda: declarar el recurso entre paréntesis después del try, y Java lo cierra solo al salir:
try (BufferedReader lector = new BufferedReader(new FileReader("notas.txt"))) {
System.out.println(lector.readLine());
} catch (IOException e) {
System.out.println("No se pudo leer: " + e.getMessage());
}
Si notas.txt no existe, la salida es parecida a esta (el texto exacto depende del sistema operativo; en Windows en español dice El sistema no puede encontrar el archivo especificado):
No se pudo leer: notas.txt (No such file or directory)
FileNotFoundException es hija de IOException, por eso el catch (IOException e) la captura. Lo usarás a fondo en la práctica.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta el código línea a línea y dibuja las variables y la pila de llamadas. Elige Java en el desplegable, pega este programa y avanza con Next:
public class Tutor {
static double convertir(String t) {
return Double.parseDouble(t); // aquí puede saltar
}
static double media(String[] notas) {
double suma = 0;
for (String t : notas) {
suma += convertir(t);
}
return suma / notas.length;
}
public static void main(String[] args) {
String[] notas = {"8", "6", "7,5"};
try {
System.out.println(media(notas));
} catch (NumberFormatException e) {
System.out.println("Error: " + e.getMessage());
} finally {
System.out.println("Fin");
}
}
}
Salida:
Error: For input string: "7,5" Fin
Qué observar:
- Con
"8"y"6"verás la pila crecer y encogerse:main→media→convertiry vuelta.sumallega a 14. - En la tercera vuelta, la llamada a
convertir("7,5")no devuelve nada: la excepción salta y el marco deconvertirdesaparece de golpe. - Justo después desaparece también el marco de
media, sin llegar alreturn. Es la propagación: la excepción «sube» porque nadie la captura ahí. - La ejecución reaparece en el
catchdemain. La variablesumase ha perdido con el marco demedia: por eso nunca se imprime ninguna media. - Por último pasa por el
finallye imprime «Fin».
Resumen y tabla rápida
| Concepto | En Java | Si vienes de Python |
|---|---|---|
| Intentar algo que puede fallar | try { … } | try: |
| Capturar un tipo concreto | catch (NumberFormatException e) | except ValueError as e: |
| Lo que se ejecuta siempre | finally { … } | finally: |
| Lanzar una excepción | throw new IllegalArgumentException("…") | raise ValueError("…") |
| Avisar de que un método puede lanzarla | throws NotaInvalidaException | no existe |
| Excepción propia | class X extends Exception | class X(Exception): |
| Cerrar recursos solo | try (BufferedReader r = …) | with open(…) as f: |
| Obligación de tratarla | Sí, si es checked | Nunca |
Tres ideas para quedarte:
- Lee la traza de arriba abajo: primera línea (qué), primera línea de tu código (dónde), el resto (cómo llegaste).
- **Cuando salta una excepción, el resto del
tryse salta**; Java busca el primercatchque encaje y después sigue.finallyse ejecuta siempre. - Checked obliga, unchecked no: las checked se capturan o se declaran con
throws; conthrowlanzas tú las tuyas, y las propias se crean heredando deException.
En la práctica construirás tres programas que no se caen con datos raros: un importador de notas, un cajero con excepciones propias y un lector de ficheros.
Siguiente paso: prácticaExcepciones en Java — práctica con un importador de notas, un cajero y un lector de ficheros (y el «Error» que no decía nada)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

4 comentarios