Excepciones en Java

COMPARTIR EN REDES SOCIALES

En esta entrada vamos a profundizar sobre el uso de Excepciones en Java. Las excepciones en Java son un mecanismo que permite controlar errores que normalmente harían que el programa finalizase de forma abrupta, es decir, con un error. Utilizando los mecanismos que nos proveen las excepciones podemos recuperar estos errores y por lo tanto, permitir que nuestro programa siga funcionando con normalidad.

¿ Qué tipos de Excepciones existen ?

Generalmente existen 2 tipos de Excepciones:

  1. Excepciones NO recuperables
  2. Excepciones recuperables

Las excepciones que no son recuperables son aquellas en las que no existe una manera de recuperar la ejecución de nuestro programa, claros ejemplos de este tipo de excepciones son un mal funcionamiento de la máquina virtual de Java donde se está ejecutando nuestro programa, una excepción de memoria no disponible lanzada por el Sistema Operativo con el consecuente cierre de la aplicación por parte del mismo, así como errores de Hardware.

Las excepciones recuperables son aquellas que no son críticas para el sistema, pero sí lo son para la ejecución de nuestros programas en Java, claros ejemplos de este tipo de Excepciones son: acceder a un índice que no existe en un Array, acceder a una variable no instanciada, un error de entrada/salida cuando leemos o escribimos datos en un fichero o por red, etc…

Dado que no hay mucho que hacer con las excepciones NO recuperables, en esta entrada vamos a profundizar sobre las recuperables.

Dentro de las Excepciones recuperables existen principalmente 2 tipos:

  1. Excepciones que pueden ser ignorables.
  2. Excepciones que deben ser capturadas y tratadas sí o sí.

Las Excepciones en Java

Para poder comprender las excepciones en Java es necesario tener nociones básicas de programación orientada a objetos, ya que todas se componen de la siguiente jerarquía:

Excepciones en Java. Jerarquía de Excepciones

Como podéis observar, tenemos los errores (excepciones no recuperables), y luego todas aquellas que heredan de «Exception» que son recuperables.

Si nos fijamos en la Excepción «FileNotFoundException» la jerarquía sería la siguiente:

Throwable >> Exception >> IOException >> FileNotFoundException

Es importante comprender que toda Excepción en Java es una clase. Podemos acceder a la documentación de Java para ver información acerca de ellas, vamos a ver que nos dice la documentación de Java acerca de «FileNotFoundException»:

Clase FileNotFoundException en Java

Como podéis ver se trata de una clase, con la jerarquía que hemos descrito anteriormente y cuya clase padre es IOException.

Vale, y una vez visto esto, ¿ cómo se trabaja con ellas ?

Uso de bloques try catch

Los bloques «try catch» nos permiten capturar y tratar excepciones. Para que veáis como se utiliza este mecanismo vamos a ver un ejemplo intentando leer un fichero que no existe:

File miFichero = new File("fichero_que_no_existe.txt");
FileReader fr;
try {
	fr = new FileReader(miFichero);
	BufferedReader br = new BufferedReader(fr);
	System.out.println("Esta línea nunca se va a ejecutar!!!");
} catch (FileNotFoundException e) {
	//Aquí capturamos la excepción, y podemos mostrar un mensaje personalizado al usuario
	System.out.println(e.getMessage());
}
		 
System.out.println("Nuestro programa termina con normalidad");

La línea 4 es la encargada de lanzar la excepción, la sentencia intentará abrir el fichero «fichero_que_no_existe.txt» y como no existe, lanzará la excepción, acto seguido el flujo de ejecución del programa pasará al catch que captura la excepción que lanza la línea 4, en este caso el flujo de ejecución saltará a la línea 7. Es importante comprender el flujo de las excepciones debido a que toda línea posterior a una excepción y dentro del mismo bloque try no se ejecutará, como en este caso la línea 6.

Veamos la salida de nuestro programa:

fichero_que_no_existe.txt (El sistema no puede encontrar el archivo especificado)
Nuestro programa termina con normalidad

La sintaxis de los bloques try catch

La sintaxis de los bloques try catch es la siguiente:

try{
    linea que pueda lanzar Excepcion1
    linea que pueda lanzar Excepcion2
    línea que puede lanzar Excepcion3
} catch (Excepcion1 ex1){
    tratamiento de la excepcion 1
}catch (Excepcion2 ex2){
    tratamiento de la excepcion 2
}catch (Excepcion3 ex3){
    tratamiento de la excepción 3
}finally{
    código que se ejecuta siempre se produzca o no excepción
}

Dentro de un bloque try puede haber tantos catch como tipos de excepciones diferentes puedan lanzar las sentencias dentro del try, y la cláusula finally sirve para asegurarnos de que el código dentro de esta sección se ejecute siempre, veámoslo con el ejemplo anterior, en este caso con un fichero que si existe:

File miFichero = new File("fichero_que_existe.txt");
FileReader fr;
try {
	fr = new FileReader(miFichero);
	BufferedReader br = new BufferedReader(fr);
	String linea = "";
	
	while ((linea = br.readLine()) != null ) {
		System.out.println(linea);
	}
	
	System.out.println("Esta línea ahora si se va a ejecutar porque no se produce excepción!!!");
} catch (FileNotFoundException e) {
	//Aquí capturamos la excepción, y podemos mostrar un mensaje personalizado al usuario
	System.out.println(e.getMessage());
} catch (IOException e) {
	//Aquí capturamos una excepción de entrada salida que puede lanzar br.readLine()
	System.out.println(e.getMessage());
}finally {
	System.out.println("Una sentencia dentro del bloque finally siempre se ejecuta independientemente de que haya habido excepción o no!!!");
}
 
System.out.println("Nuestro programa termina con normalidad");

La salida de nuestro programa ahora será la siguiente:

Hola!
Hola2!
Hola3!
Esta línea ahora si se va a ejecutar porque no se produce excepción!!!
Una sentencia dentro del bloque finally siempre se ejecuta independientemente de que haya habido excepción o no!!!
Nuestro programa termina con normalidad

Como podéis apreciar en la salida, primero se leen y muestran las líneas del fichero de texto, después de muestra el «System.out.println» de la línea 12, posterior a esto se ejecuta el bloque «finally» y por último nuestro programa termina con normalidad.

¿ Cómo sería la salida con un fichero que no existe ?

La salida sería la siguiente:

fichero_que_no_existe.txt (El sistema no puede encontrar el archivo especificado)
Una sentencia dentro del bloque finally siempre se ejecuta independientemente de que haya habido excepción o no!!!
Nuestro programa termina con normalidad

Fijaos que el bloque «finally» se ejecuta siempre, y toda línea posterior a la línea 4, que ha producido la excepción, no se ejecuta.

Ejemplo de una calculadora que solo divide enteros con tratamiento de Excepciones

Scanner sc = new Scanner(System.in);
boolean ejecucionCorrecta = false;

do {
	System.out.println("Introduce el dividendo");
	int dividendo = sc.nextInt();
	System.out.println("Introduce el divisor");
	int divisor = sc.nextInt();
	
	try {
		int resultado = dividendo / divisor;
		ejecucionCorrecta = true;
		System.out.println("El resultado es: " + resultado);
	}catch (ArithmeticException ae) {
		System.out.println("No existe la división entre 0. Por favor vuelve a repetir la operación cambiando el divisor");
	}
	
}while (!ejecucionCorrecta);

La salida de nuestro programa

Introduce el dividendo
4
Introduce el divisor
0
No existe la división entre 0. Por favor vuelve a repetir la operación cambiando el divisor
Introduce el dividendo
4
Introduce el divisor
0
No existe la división entre 0. Por favor vuelve a repetir la operación cambiando el divisor
Introduce el dividendo
4
Introduce el divisor
2
El resultado es: 2

Excepciones Ignorables y No Ignorables

Las excepciones ignorables son aquellas que el compilador nos permite ignorarlas y no tratarlas obligatoriamente mediante un bloque try catch, un claro ejemplo de esta excepción es «NullPointerException», veamos un ejemplo:

//Declaramos un String a nulo
String strNulo = null;
//Intentamos imprimir en numero de caracteres que tiene nuestro String
System.out.println(strNulo.length());

La salida de nuestro programa

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "strNulo" is null
	at Main.main(Main.java:14)

Si os fijáis hemos podido ejecutar nuestro programa sin utilizar un bloque «try catch», por eso se trata de una excepción ignorable, lo cual no hace que nuestro programa no explote, como habéis podido ver en la salida.

Las excepciones no ignorables, por el contrario, es obligatorio tratarlas con un bloque «try catch» o no se puede compilar y ejecutar nuestro programa, veamos un ejemplo gráfico:

Excepción no ignorable no tratada

El compilador nos señala que hay un error en la sentencia donde intentamos instanciar el «FileReader» y no compila nuestro código. Para poder compilar nuestro código es necesario que añadamos un bloque «try catch»:

Excepción no ignorable tratada.

Una vez tratada ya se puede compilar nuestro código.

Excepciones personalizadas

Java nos permite crear nuestra propias excepciones aprovechándonos de la herencia, vamos a crear una Excepción que en el apartado siguiente utilizaremos:

public class CantidadInsuficienteException extends Exception {

	public CantidadInsuficienteException(String message) {
		super(message);
	}
	
}

La metodología es la siguiente:

  1. Creamos una clase con el nombre que consideremos oportuno.
  2. Heredamos de la clase «Exception» con «extends».
  3. En el constructor recibimos un mensaje de error que mostraremos al usuario.
  4. Llamamos al constructor de la clase padre pasando nuestro mensaje de error personalizado.

¿ Cómo se lanza una Excepción personalizada ?

Para comprender el mecanismo de lanzamiento de excepciones lo haremos con nuestra famosa clase «Producto»:

public class Producto{
	
		private int id;
		private String nombre;
		int cantidad;
		
		public Producto(int id, String nombre, int cantidad) throws CantidadInsuficienteException {
			
			if (cantidad != 0) {
				this.id = id;
				this.nombre = nombre;
				this.cantidad = cantidad;
			}else {
				throw new CantidadInsuficienteException("No puedes añadir un producto con una cantida de 0");
			}
		}

		public int getId() {
			return id;
		}

		public void setId(int id) {
			this.id = id;
		}

		public String getNombre() {
			return nombre;
		}

		public void setNombre(String nombre) {
			this.nombre = nombre;
		}

		public int getCantidad() {
			return cantidad;
		}

		public void setCantidad(int cantidad) throws CantidadInsuficienteException {
			if (cantidad == 0) {
				throw new CantidadInsuficienteException("No puedes añadir un producto con una cantida de 0");
			}else {
				this.cantidad = cantidad;	
			}
		}
	}

Lo que hemos hecho en esta clase es controlar que el usuario no pueda introducir una «cantidad» con valor igual a 0, lo hemos controlado tanto en el constructor como en el «set» de cantidad.

¿ Cómo lo hemos controlado ?

Exploremos primero el constructor:

public Producto(int id, String nombre, int cantidad) throws CantidadInsuficienteException {
			
			if (cantidad != 0) {
				this.id = id;
				this.nombre = nombre;
				this.cantidad = cantidad;
			}else {
				throw new CantidadInsuficienteException("No puedes añadir un producto con una cantida de 0");
			}
		}

Mediante la línea 8 lanzamos nuestra excepción personalizada y mediante la línea 1 obligamos a que cualquiera que instancie nuestra clase esté obligado a capturar o propagar nuestra excepción. Luego veremos todo el tema de la propagación de excepciones…

En el «set» hemos hecho lo mismo:

public void setCantidad(int cantidad) throws CantidadInsuficienteException {
	if (cantidad == 0) {
		throw new CantidadInsuficienteException("No puedes añadir un producto con una cantida de 0");
	}else {
		this.cantidad = cantidad;	
	}
}

Vamos a intentar crear una clase Producto

Producto p = null;
		
try {
	p = new Producto(1, "Tuerca", 0);
	System.out.println("El producto se ha creado");
} catch (CantidadInsuficienteException e) {
	System.out.println(e.getMessage());
}

if (p == null) {
	System.out.println("No se ha podido crear el producto");
}

try {
	p = new Producto(1,"Tuerca", 10);
	System.out.println("El producto se ha creado correctamente");
} catch (CantidadInsuficienteException e) {
	System.out.println(e.getMessage());
}

try {
	p.setCantidad(0);
} catch (CantidadInsuficienteException e) {
	System.out.println(e.getMessage());
}

System.out.println("Nuestro programa finaliza correctamente");

La salida de nuestro programa

No puedes añadir un producto con una cantida de 0
No se ha podido crear el producto
El producto se ha creado correctamente
No puedes añadir un producto con una cantida de 0
Nuestro programa finaliza correctamente
  1. Primero intentamos crear un producto con una cantidad de 0, salta nuestra excepción personalizada.
  2. La capturamos y mostramos el mensaje de error.
  3. Comprobamos que «p == null» y mostramos que el producto no se ha creado.
  4. Después creamos el producto correctamente, ya que pasamos como cantidad 10, por lo tanto, nuestro producto se crea correctamente.
  5. Por último intentamos poner al producto creado una cantidad de 0 con el método «set». Salta otra vez la excepción con nuestro mensaje personalizado.

La propagación de Excepciones

Antes hemos dicho que la cláusula «throws» en el prototipo de los métodos obliga a capturar la excepción a quien llame a ese método o a propagarla. Veamos el prototipo de nuestra función «setCantidad» para hacer memoria:

public void setCantidad(int cantidad) throws CantidadInsuficienteException {
	if (cantidad == 0) {
		throw new CantidadInsuficienteException("No puedes añadir un producto con una cantida de 0");
	}else {
		this.cantidad = cantidad;	
	}
}

En la línea 1 es donde declaramos que nuestro método puede lanzar una excepción de tipo «CantidadInsuficienteException».

Vamos a ver la propagación con el siguiente ejemplo de código:

public static void main(String[] args) {
	try {
		crearProducto();
	} catch (CantidadInsuficienteException e) {
		System.out.println(e.getMessage());
	}
	
	System.out.println("Nuestro programa finaliza correctamente");
}

public static void crearProducto() throws CantidadInsuficienteException {
	Producto p = new Producto(1, "Tuerca", 0);
}

Fijaos que ahora es en el método «crearProducto» donde creamos el producto, y en vez de capturar la excepción, mediante la cláusula «throws» la propagamos hacía arriba. ¿ Y que quiere decir que la propagamos hacía arriba ? Pues quiere decir que obligamos que quien llame a nuestro método «crearProducto» capture y trate la excepción, por eso en el «main» es donde ponemos el bloque «try catch».

La salida de nuestro programa

No puedes añadir un producto con una cantida de 0
Nuestro programa finaliza correctamente

Cabe destacar que es a elección del programador decidir si propaga la excepción o la captura en la sentencia que la provoca.

Entendiendo las trazas de una excepción

Cuando salta una excepción, si pintamos en la consola la traza de dicha excepción podemos saber con exactitud en que línea se ha producido así como la pila de llamadas, veámoslo con el ejemplo anterior modificándolo para que pinte la traza:

public static void main(String[] args) {
	try {
		crearProducto();
	} catch (CantidadInsuficienteException e) {
		e.printStackTrace();
	}
	
	System.out.println("Nuestro programa finaliza correctamente");
}

public static void crearProducto() throws CantidadInsuficienteException {
	Producto p = new Producto(1, "Tuerca", 0);
}

La traza que se pinta en consola

CantidadInsuficienteException: No puedes añadir un producto con una cantida de 0
	at Producto.<init>(Producto.java:14)
	at Main.crearProducto(Main.java:22)
	at Main.main(Main.java:13)
Nuestro programa finaliza correctamente

Fijaos que nos dice que la excepción la produce la línea «14» de la clase «Producto», que la línea donde se llama a Producto es en la línea «22» del método «crearProducto» y que se captura en la línea «13» del método «main».

Y esto es todo con respecto a Excepciones en Java. Si os ha gustado la entrada dejad un comentario en la caja de comentarios y compartirla por vuestras redes!

Un saludo programadores!


COMPARTIR EN REDES SOCIALES

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *