Excepciones
Hasta ahora hemos creado programas que normalmente siguen el flujo que esperamos:
Inicio
↓
Ejecutar instrucciones
↓
Mostrar resultado
↓
Fin
Pero en un programa pueden ocurrir situaciones inesperadas.
Por ejemplo:
- Intentar dividir entre
0. - Intentar acceder a una posición que no existe en un
ArrayList. - Intentar abrir un archivo que no existe.
- Recibir un dato incorrecto del usuario.
Cuando ocurre un problema durante la ejecución del programa, Java puede producir una excepción.
Por ejemplo, al intentar dividir un número por 0:
int resultado = 10 / 0;
Esto provocará una excepción:
ArithmeticException
Las excepciones nos permiten detectar problemas y decidir qué hacer cuando ocurren.
¿Qué es una excepción?
Una excepción es un problema que ocurre durante la ejecución de un programa.
Por ejemplo:
int numero = 10;
int resultado = numero / 0;
No podemos dividir un número entre 0.
Cuando Java llega a esta instrucción:
numero / 0
se produce una excepción.
Java muestra información similar a:
Exception in thread "main" java.lang.ArithmeticException: / by zero
Además, el programa dejará de ejecutar las instrucciones posteriores si la excepción no se controla.
Por ejemplo:
System.out.println("Inicio");
int resultado = 10 / 0;
System.out.println("Fin");
La salida será:
Inicio
Después ocurre la excepción. Por lo que la línea:
System.out.println("Fin");
no llegará a ejecutarse.
Cada excepción tiene un nombre predefinido por Java, como es ArithmeticException. Nos iremos encontrando con las excepciones más comunes pero hay muchas más.
A continuación veremos qué hacer cuando nos encontramos con un código que creemos puede producir alguna excepción.
try
Para intentar controlar una excepción podemos utilizar:
try
La palabra try significa que vamos a indicar un bloque de código que podría producir una excepción.
Por ejemplo, dividir entre 0 puede producir una excepción, por lo que meteremos esa instrucción entre las llaves del try:
try {
int resultado = 10 / 0;
}
Pero try normalmente se utiliza junto con catch.
La estructura básica es:
try {
// Código que puede producir una excepción
} catch (Exception e) {
// Código que se ejecuta si ocurre la excepción
}
Podemos imaginarlo así:
try
↓
Intentar ejecutar código
↓
¿Ocurre una excepción?
├── NO → continuar
│
└── SÍ → pasa a evaluar el catch
catch
La palabra:
catch
permite capturar una excepción.
Por ejemplo:
try {
int resultado = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("No se puede dividir entre cero");
}
Ahora, cuando ocurre la excepción:
int resultado = 10 / 0;
Java entra en el bloque:
catch
y ejecuta:
System.out.println("No se puede dividir entre cero");
La salida será:
No se puede dividir entre cero
Esto evita que el programa termine directamente, ya que cuando ocurre una ArithmeticException, en lugar de mostrarse y parar completamente el código, se ejecuta el catch y sigue con normalidad.
Podemos visualizarlo:
try
10 / 0
↓
ArithmeticException
↓
catch
"No se puede dividir entre cero"
De manera que el código anterior:
System.out.println("Inicio");
int resultado = 10 / 0;
System.out.println("Fin");
que mostrava solamente:
Exception in thread "main" java.lang.ArithmeticException: / by zero
y no continuava el código. Ahora si le añadimos el try catch:
System.out.println("Inicio");
try {
int resultado = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("No se puede dividir entre cero");
}
System.out.println("Fin");
El programa podrá continuar con normalidad si salta la excepción:
Inicio
No se puede dividir entre cero
Fin
El objeto de la excepción
En:
catch (ArithmeticException e)
tenemos por un lado el tipo de excepción, en este caso ArithmeticException y la variable e, que nos permite acceder a información sobre lo que ha ocurrido.
Por ejemplo en:
try {
int numerador = 10;
int denominador = 0;
int resultado = numerador / denominador;
System.out.println("Resultado: " + resultado);
} catch (ArithmeticException e) {
String mensaje = e.getMessage();
System.out.println("Error al dividir: " + mensaje);
}
usamos el método e.getMessage(), que nos muestra el mensaje predefinido del error en cuestión, en este caso:
Error al dividir: / by zero
Capturar diferentes tipos de excepciones
No todos los problemas producen el mismo tipo de excepción.
Por ejemplo:
int resultado = 10 / 0;
produce:
ArithmeticException
Mientras que:
ArrayList<String> nombres = new ArrayList<>();
nombres.get(10);
puede producir:
IndexOutOfBoundsException
Y:
Integer.parseInt("hola");
produce:
NumberFormatException
Varios catch
También podemos tener varios bloques catch.
Por ejemplo:
try {
// Código que puede producir diferentes excepciones
} catch (ArithmeticException e) {
System.out.println("Error matemático");
} catch (NumberFormatException e) {
System.out.println("El formato del número es incorrecto");
} catch (IndexOutOfBoundsException e) {
System.out.println("La posición no existe");
}
Java comprobará qué tipo de excepción se ha producido y ejecutará el catch correspondiente.
Podemos imaginarlo así:
try
│
↓
Ocurre una excepción
│
┌──────────┼──────────┐
↓ ↓ ↓
ArithmeticException NumberFormatException IndexOutOfBoundsException
│ │ │
↓ ↓ ↓
catch 1 catch 2 catch 3
finally
Además de try y catch, podemos utilizar:
finally
El bloque finally contiene código que queremos ejecutar al finalizar el bloque try, haya ocurrido o no una excepción.
Por ejemplo:
try {
System.out.println("Intentando ejecutar");
} catch (Exception e) {
System.out.println("Ha ocurrido un error");
} finally {
System.out.println("Fin del proceso");
}
Si no ocurre ninguna excepción:
Intentando ejecutar
Fin del proceso
Si ocurre una excepción:
Intentando ejecutar
Ha ocurrido un error
Fin del proceso
El bloque finally se ejecuta en ambos casos.
Podemos visualizarlo:
try
│
┌──────┴──────┐
↓ ↓
Sin error Con error
│ │
↓ ↓
continuar catch
│ │
└──────┬──────┘
↓
finally
throw
Hasta ahora hemos visto cómo controlar excepciones que se producen automáticamente.
Pero también podemos provocar nosotros mismos una excepción.
Para ello utilizamos:
throw
Por ejemplo:
throw new Exception();
Esto significa:
Crear una excepción y lanzarla.
Podemos utilizarlo para comprobar si un dato cumple una determinada condición.
Es decir, podemos crear nosotros nuestras propias excepciones. Por ejemplo, por defecto introducir una edad negativa no es ningún error de Java. Pero nosotros podemos decidir que sí lo sea, lanzando una exepción si se introdujese una edad negativa:
int edad = -5;
if (edad < 0) {
throw new IllegalArgumentException("La edad no puede ser negativa");
}
throw con try y catch
También podemos lanzar una excepción y capturarla.
Por ejemplo:
try {
int edad = -5;
if (edad < 0) {
throw new IllegalArgumentException("Edad no válida");
}
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
La salida será:
Edad no válida
Aquí tenemos dos conceptos diferentes:
throw
↓
Lanza una excepción
catch
↓
Captura una excepción
throws
Aunque pueda parecer lo mismo, antes hemos hablado de throw.
throw y throws tienen nombres parecidos, pero no significan lo mismo.
Utilizamos:
throw
para lanzar una excepción concreta.
Por ejemplo:
throw new Exception("Ha ocurrido un error");
Mientras que:
throws
se utiliza en la declaración de un método para indicar que ese método puede lanzar una determinada excepción.
Por ejemplo:
public static void comprobarEdad(int edad) throws Exception {
if (edad < 0) {
throw new Exception("Edad no válida");
}
}
Aquí:
throws Exception
indica:
Este método puede lanzar una excepción de tipo
Exception.
Mientras que:
throw new Exception(...)
es el momento en el que realmente lanzamos la excepción.
Podemos recordarlo así:
throws
↓
Declara que puede ocurrir
throw
↓
Lanza la excepción
Ejemplo con throws
Podemos crear un método:
public static void comprobarEdad(int edad) throws Exception {
if (edad < 0) {
throw new Exception("La edad no puede ser negativa");
}
System.out.println("Edad correcta");
}
Ahora podemos llamarlo:
comprobarEdad(20);
Esto mostrará:
Edad correcta
Pero si hacemos:
comprobarEdad(-5);
se lanzará la Exception.
Por norma general cuando queramos que nuestro código lanze una excepción con throw necesitaremos añadirle a dicha clase el throws Exception para que esté preparado para lanzarla.
Excepciones personalizadas
En algunos programas necesitaremos crear nuestros propios tipos de excepciones.
Por ejemplo, imaginemos que estamos creando un programa para gestionar cuentas bancarias.
Podríamos querer una excepción específica para indicar que no hay suficiente dinero:
SaldoInsuficienteException
Para ello podemos crear nuestra propia clase de excepción. Lo bueno es que la sintaxis para crear tus propias excepciones es siempre muy similar:
public class NombreDeLaExcepcion extends Exception {
public NombreDeLaExcepcion(String mensaje) {
super(mensaje);
}
}
Por ejemplo:
public class SaldoInsuficienteException extends Exception {
public SaldoInsuficienteException(String mensaje) {
super(mensaje);
}
}
Nuestra excepción:
SaldoInsuficienteException
│
│ extends
↓
Exception
Ahora podemos utilizarla en nuestro programa. Se recomienda crear un archivo .java por cada excepción creada.
Utilizar una excepción personalizada
Podemos crear una clase Cuenta:
public class Cuenta {
double saldo;
public Cuenta(double saldo) {
this.saldo = saldo;
}
public void retirar(double cantidad) throws SaldoInsuficienteException {
if (cantidad > saldo) {
throw new SaldoInsuficienteException(
"No hay suficiente saldo"
);
}
saldo -= cantidad;
}
}
Aquí comprobamos:
if (cantidad > saldo)
Si no hay suficiente dinero:
throw new SaldoInsuficienteException(...)
lanzamos nuestra excepción personalizada con el mensaje que indiquemos entra los paréntesis.
El método también indica:
throws SaldoInsuficienteException
porque puede producir esta excepción.
Capturar una excepción personalizada
Ahora podemos utilizar nuestra clase:
Cuenta cuenta = new Cuenta(100);
try {
cuenta.retirar(150);
} catch (SaldoInsuficienteException e) {
System.out.println(e.getMessage());
}
La salida será:
No hay suficiente saldo
Podemos visualizar todo el proceso:
Cuenta
saldo = 100
↓
retirar(150)
↓
150 > 100
↓
throw
↓
SaldoInsuficienteException
↓
catch
↓
"No hay suficiente saldo"
Ejemplo completo
Vamos a crear un pequeño programa para validar la edad de una persona.
Primero creamos nuestra excepción:
public class EdadInvalidaException extends Exception {
public EdadInvalidaException(String mensaje) {
super(mensaje);
}
}
Después creamos un método para comprobar la edad:
public static void comprobarEdad(int edad) throws EdadInvalidaException {
if (edad < 0) {
throw new EdadInvalidaException(
"La edad no puede ser negativa"
);
}
if (edad > 120) {
throw new EdadInvalidaException(
"La edad no puede ser mayor de 120"
);
}
System.out.println("Edad válida");
}
Ahora podemos utilizarlo:
public static void main(String[] args) {
try {
comprobarEdad(150);
} catch (EdadInvalidaException e) {
System.out.println(e.getMessage());
}
}
Errores frecuentes
Utilizar throw y throws indistintamente
No son lo mismo.
Recordemos:
throw
↓
Lanza una excepción
throws
↓
Declara que un método puede lanzarla
Utilizar try sin catch ni finally
No podemos escribir simplemente:
try {
// Código
}
Un try debe estar acompañado de al menos un:
catch
o:
finally
Por ejemplo:
try {
// Código
} catch (Exception e) {
// Controlar error
}
O:
try {
// Código
} finally {
// Código final
}
Capturar una excepción demasiado general
Podemos escribir:
catch (Exception e)
para capturar muchas excepciones.
Pero si conocemos el tipo concreto, normalmente es mejor especificarlo.
Por ejemplo:
catch (NumberFormatException e)
es más específico que:
catch (Exception e)
Esto permite controlar cada tipo de error de una manera diferente.
Por ejemplo:
try {
int numero = Integer.parseInt("hola");
} catch (NumberFormatException e) {
System.out.println("Debes introducir un número");
}
Crear excepciones personalizadas sin necesidad
No siempre necesitamos crear nuestra propia excepción.
Si Java ya dispone de una excepción adecuada, podemos utilizarla.
Por ejemplo:
throw new IllegalArgumentException(
"La edad no puede ser negativa"
);
Puede ser suficiente para un programa pequeño.
Las excepciones personalizadas son especialmente útiles cuando queremos representar errores específicos de la lógica de nuestra aplicación.
Resumen
Las excepciones permiten controlar situaciones inesperadas que pueden ocurrir durante la ejecución de un programa.
La estructura más habitual es:
try {
// Código que puede producir una excepción
} catch (Exception e) {
// Controlar la excepción
} finally {
// Código que queremos ejecutar al finalizar
}
Podemos lanzar excepciones manualmente utilizando:
throw
Por ejemplo:
throw new Exception("Ha ocurrido un error");
Y podemos indicar que un método puede lanzar una excepción utilizando:
throws
Por ejemplo:
public void comprobar() throws Exception {
}
También podemos crear nuestras propias excepciones:
public class MiExcepcion extends Exception {
public MiExcepcion(String mensaje) {
super(mensaje);
}
}
Las excepciones nos permiten evitar que cualquier problema inesperado termine directamente nuestro programa y nos permiten decidir cómo queremos responder ante cada situación.