Introducción

Una excepción, conocida comúnmente como error en tiempo de ejecución, representa una situación anómala que tiene lugar en un programa y que impide continuar con la ejecución normal del mismo.

Cuando se produce una excepción, el comportamiento por defecto es la interrupción de la ejecución del programa y el envío de la traza de error (llamado también volcado de pila) a la consola de error, indicándose el tipo de excepción producida y la línea de código donde ha tenido lugar.

Este comportamiento, sin embargo, no debería ser el que deberían mostrar las aplicaciones en un entorno de producción, pues el hecho de que un programa se interrumpa bruscamente al producirse un error de ejecución y envíe ese tipo de mensajes a la interfaz, puede causar cierto rechazo por parte del usuario del mismo. Lo lógico sería que el propio programa tomara el control del error y ejecutase algún tipo de rutina de tratamiento del mismo. Este tratamiento de errores en un programa Java se lleva a cabo mediante la captura de excepciones.

Excepciones en un programa.

Cuando se produce una excepción dentro de un programa Java, se crea un objeto de la clase de excepción correspondiente y se lanza al programa. Si este no captura la excepción, la JVM provocará la interrupción del programa.

Así pues, antes de ver como capturar una excepción debemos conocer las clases de excepción existentes y como se clasifican estas en función de su naturaleza.

Clases de Excepción | Clasificación de las Excepciones

Clases de Excepción

Como se acaba de indicar, una excepción está representada en código por un objeto, objeto que pertenecerá a alguna de las clases de excepción existentes en Java y que representan a los distintos errores que se pueden producir en un programa.

La superclase de todas las clases de excepción es Throwable, de la cual derivan a su vez dos clases:

  • Error. Las excepciones de tipo Error representan errores de la máquina virtual java, como por ejemplo un fallo en uno de los módulos nativos, una falta de memoria por parte del sistema, etc., por lo que este tipo de excepción no debe ser capturada por el código de la aplicación.

  • Exception. Representa a las excepciones propiamente dichas, es decir, fallos en el programa producidos por algún error del código, un uso incorrecto de algún método o, simplemente, porque tiene lugar una situación inesperada. La figura 2 muestra la jerarquía de clases con algunas de las excepciones que se pueden producir en un programa Java.

Clasificación de las excepciones

De todas las clases de excepciones subclases de Exception, podemos establecer una división en dos grandes grupos en función del origen de estas excepciones. Así tenemos:

  • Excepciones del sistema
  • Excepciones del API Java

Excepciones del sistema

A este grupo pertenecen todas aquellas excepciones que son subclases de RuntimeException. Se les llama excepciones del sistema porque se pueden producir en cualquier tipo de programa Java y, por lo general, son debidas a un error de programación del código. Un ejemplo es la excepción NullPointerException que se produce al intentar utilizar una variable en la que no hemos establecido ninguna referencia a objeto, o la excepción NumberFormatException que se produce al intentar convertir a número una cadena de caracteres no numéricos.

Antes de implementar una captura de excepciones de este tipo, conviene depurar el código durante la fase de desarrollo para detectar y corregir los posibles fallos de programación que pueden producir estas excepciones. Para aquellas instrucciones donde se prevea que pueda producirse una excepción de este tipo debido, por ejemplo, a la manipulación de un dato externo cuyo valor desconocemos, entonces será conveniente definir un control de excepciones para tratar los posibles errores.

Excepciones del API

Este tipo de excepciones se producen tras invocar a ciertos métodos de determinadas clases del API Java y son generadas (lanzadas) desde el interior de dichos métodos como consecuencia de algún fallo durante la ejecución de los mismos.

Cuando hacemos uso de alguno de estos métodos dentro de nuestro programa, es obligatorio realizar la captura de estas excepciones, de lo contrario el programa no compilará. Si no queremos definir ningún tratamiento especial para la excepción, podemos propagar la excepción hacia la máquina virtual Java para que haga un tratamiento de la misma, tratamiento que consistirá en detener el programa y enviar un volcado de pila a la consola.

Este es el caso por ejemplo de la excepción IOException que puede producir el método readLine() de BufferedReader. Cuando utilizamos este método en los ejemplos desarrollados en los temas anteriores, debíamos incluir en la declaración del main() la sentencia: throws IOException cuya misión era simplemente propagar la excepción hacia la máquina virtual. De no utilizar esta sentencia, tendríamos que haber establecido una captura de la excepción o sino no habríamos podido compilar la clase.

La información sobre las excepciones que pueden lanzar los distintos métodos de las clases del API la encontramos en el javadoc; concretamente, en la declaración del método se indicará mediante la clausula throws las posibles excepciones que el método puede lanzar. La figura 3 nos muestra como ejemplo el aspecto de la declaración del método readLine() de la clase BufferedReader.

Captura de excepciones.

La captura de excepciones consiste en "atrapar" mediante código el objeto de excepción generado al producirse esta y, mediante un bloque de sentencias específico, definir las acciones para el tratamiento del mismo.

La captura y tratamiento de excepciones en un programa Java se lleva a cabo mediante los llamados bloques try/catch, que permiten realizar estas operaciones sin alterar la estructura de código de programa. Veamos cómo se utilizan.

Bloque try | Bloque catch | Bloque finally

Bloques Try

	try{
		sentencia 1;
		sentencia 2;
	}
				

Estas sentencias contendrán llamadas al métodos del API Java que tengan declarada alguna excepción de obligada captura, o bien alguna operación susceptible de producir una excepción RuntimeException.

En caso de que la excepción llegue a producirse, el control del programa pasará al bloque catch encargado del tratamiento de dicha excepción.

Bloques Catch

Los bloques catch se deben definir inmediatamente a continuación del bloque try. En ellos se definen las instrucciones a ejecutar para cada tipo de excepción que se quiera tratar; cada tipo de excepción tendrá su bloque catch de tratamiento:

	try{
		//sentencias
	}
	catch (tipo1 objeto){
		//tratamiento excepciones tipo1
	}
	catch (tipo2 objeto){
		//tratamiento excepciones tipo2
	}
				

Donde tipo1, tipo2, etc., son los nombres de las clases de excepción tratadas por cada bloque, mientras que cada parámetro declarado en cada catch recibirá el objeto de excepción lanzado.

Una vez finalizada la ejecución del bloque catch, el control del programa no vuelve al punto donde se produjo la excepción, sino que se continúa la ejecución por la instrucción que aparece a continuación del último catch.

El siguiente código de ejemplo realiza la captura y tratamiento de dos posibles excepciones RuntimeException:

	try{
		:
		m_array[10]=100; //posible ArrayIndexOutOfBoundsException
		int p = k / num; //posible ArithmeticException
	}
	catch (ArrayIndexOutOfBoundsException obj){
		System.out.println("La posición indicada del array no
		existe");
	}
	catch (ArithmeticException obj){
		System.out.println("Error de división entre cero");
	}
				

Si se produce una excepción para la que no hay ningún catch definido, en caso de que se trata de una subclase de RuntimeException, la JVM tomará el control de la misma, por lo que se interrumpirá la ejecución del programa y se mostrará un volcado de pila en la consola. Pero si se trata de una excepción del API Java, el programa simplemente no compilara, ya que estamos obligados a capturar ese tipo de excepciones.

Puede suceder que cuando definimos varios bloques catch para capturar distintos tipos de excepciones algunas de estas excepciones sean subclases de otras, en cuyo caso los catch de las excepciones padre deberán estar situados después de los catch de las excepciones hijas.

Por ejemplo, si tuviéramos definidos los siguientes bloques catch:

	catch (Exception obj){
		//código de captura de excepción
	}
	catch (IOException obj){
		//código de captura de excepción
	}
				

La clase donde estuviera definido este código simplemente no compilaría, pues el bloque catch de la IOException nunca llegaría a ejecutarse. El motivo es que si se produce una excepción de este tipo, dado que Exception es superclase de IOException, el programa entrará directamente en el primer bloque.

Bloque Finally

A continuación del último bloque catch existente en una captura de excepciones puede definirse un bloque finally, cuya finalidad es la de definir una serie de instrucciones de obligada ejecución.

	try{
		//sentencias
	}
	catch (tipo1 objeto){
		//tratamiento excepciones tipo1
	}
	catch (tipo2 objeto){
		//tratamiento excepciones tipo2
	}
	finally{
		//instrucciones de obligada ejecución
	}
				

Si no se produce ninguna excepción en el bloque try, el control del programa pasará directamente al bloque finally. Si se produce la excepción, después de ejecutarse el catch correspondiente e independientemente de las instrucciones definidas en él, el control del programa pasará al bloque finally.

Es decir, tanto si se produce o no la excepción, las instrucciones definidas en finally siempre se ejecutarán.

En principio puede parecer redundante la utilización del bloque finally, puesto que el programa continuará su ejecución normal a partir de la siguiente instrucción al último catch, tanto si se produce la excepción como si no.

Sin embargo, puede ocurrir que alguno de los catch incluya, por ejemplo, una instrucción de tipo return, en cuyo caso, si el programa entra en dicho catch se producirá la salida del método sin llegar a ejecutar las instrucciones siguientes al catch. Así pues, si en estos casos queremos garantizar que ciertas instrucciones se ejecuten antes de abandonar el método tendremos que incluirlas dentro de un bloque finally.

Por ejemplo, supongamos que tenemos el siguiente programa:

	public static void main(String[] args) {
		ejecuta(0);
	}
	public static void ejecuta(int i){
		try{
			int n=5/i;
		}
		catch(Exception e){
			System.out.println("error de división");
			return;
		}
		finally{
		System.out.println("sale del método");
		}
	}
				

Tras su ejecución se mostrará en pantalla lo siguiente:

error de división

sale del método

Métodos de clases de Excepción.

La clase Exception proporciona una serie de métodos que son heredados por todas las clases de excepción. Entre estos métodos, hay algunos de ellos que nos permiten obtener información sobre la excepción producida, lo cual puede resultar de interés en el bloque catch de captura de la excepción. Entre ellos destacamos:

  • getMessage(). Devuelve un mensaje de texto asociado a la excepción. Cuando se construye un objeto de excepción, se puede suministrar al constructor de la superclase (Exception) el mensaje de error que queremos que se obtenga al invocar a este método.
  • try
    {
    if (stud.getCall() != null)
    acc.Call = stud.getCall().toString();
    else
    throw new Exception("Data is null");
    }
    catch (Exception e) {
    logger.error("Some Error" + e.getMessage());
    throw new Exception("Please check the Manatadatory Field is 
    Missing" + e.getMessage());
    }				
    				
  • printStackTrace(). Envía a la salida por defecto (la consola) el volcado de pila asociado a la excepción.
  •  catch(IOException ioe)
        {
            ioe.printStackTrace();
        }				
    				
  • printStackTrace(PrintWriter pw). Envía el volcado de pila de la excepción al objeto de salida asociado al PrintWriter suministrado como parámetro.
  • StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    t.printStackTrace(pw);
    sw.toString(); // stack trace as a string				
    				

Creación de clases de Excepciones personalizadas.

Además de las excepciones de tiempo de ejecución y las del API Java, podemos crear nuestras propias clases de excepción personalizadas para ser utilizadas en aquellos contextos en que alguno de los métodos de las clases que estamos desarrollando necesitemos informar a los usuarios de las mismas sobre alguna situación anómala.

Por ejemplo, si desde una clase CuentaBancaria queremos notificar al programador que va a utilizar la clase el momento en que el saldo de un objeto de ésta desciende de una determinada cantidad, podríamos crear una clase de excepción personalizada que represente esta situación; a partir de ahí, desde el método extraer() de la clase CuentaBancaria lanzaríamos la excepción en el momento en que se produzca la situación indicada, excepción que tendrá que ser capturada desde el código que utilice la clase.

Definición de la clase de Excepción

El único requerimiento de la clase de excepción personalizada es que herede la clase Exception de Java. No es necesario añadir ningún método adicional a la nueva clase, aunque será conveniente definir un constructor en la misma que permita establecer el mensaje de error en la construcción del objeto de excepción.

El siguiente listado de ejemplo corresponde al código de una clase de excepción llamada SuperacionLimiteException:

	class SuperacionLimiteException extends Exception{
		//constructor que define un mensaje de error por defecto
		public SuperacionLimiteException(){
			super("Superación del límite");
		}
		//constructor que permite establecer un mensaje
		//de error personalizado
		public SuperacionLimiteException(String mensaje){
			super(mensaje);
		}
	}
				

Lanzamiento de la Excepción

Cuando desde el interior de un método se da la situación que debe dar lugar a la excepción, se debe proceder al lanzamiento de la misma, operación ésta que se realiza mediante la instrucción throw:

throw objeto_excepcion;

Así mismo, el tipo de excepción deberá ser declarada en la cabecera del método a través de la clausula throws. Por ejemplo, el siguiente método lanza una excepción del tipo definido anteriormente cuando el valor de un determinado atributo supera una cantidad:

	public void metodoPrueba (int valor) throws SuperacionLimiteException{
		if(valor>limite){
			SuperacionLimiteException s =
				new SuperacionLimiteException();
			throw s;
		}
	}

Ejemplos

Ejemplo 1 | Ejemplo 2

Ejemplo 1

Vamos a realizar un sencillo programa que solicite al usuario la introducción de un número que represente el total de argumentos suministrados por línea de comandos que hay que mostrar en pantalla. Después el programa presentará tantos argumentos como se indique en dicho número.

En la codificación de este programa realizaremos la captura de tres excepciones:

  • IOException, que es la excepción que puede lanzar el método readLine() de la clase BufferedReader.
  • NumberFormatException, que es la excepción que puede producirse si el valor suministrado por el usuario no es numérico.
  • ArrayIndexOutOfBoundsException, que es la excepción que puede producirse si el número de argumentos suministrados es menor al valor introducido por el usuario.

El código de la clase es el siguiente:

import java.io.*;
public class Presentacion {
	public static void main(String[] args) {
		BufferedReader bf=new BufferedReader(new
		InputStreamReader(System.in));
		System.out.println("Introduce el total de parámetros que 
		quieres mostrar");
		try{
			int total=Integer.parseInt(bf.readLine());
			for(int i=0;i<total;i++){
				System.out.println(args[i]);
			}
		}
		catch(IOException e){
			System.out.println("Error de entrada de datos");
		}
		catch(NumberFormatException e){
			System.out.println("El dato no tiene formato numérico 
			válido");
		}
		catch(ArrayIndexOutOfBoundsException e){
			System.out.println("No se han introducido tantos 
			argumentos al iniciar el programa");
		}
	}
}

Ejemplo 2

Vamos a crear una clase que nos permita almacenar y recuperar datos de un almacén permanente (fichero). La clase, a la que llamaremos AlmacenNumeros, tendrá dos métodos guardarNumero() que almacenará en el fichero el número suministrado como parámetro, y recuperarNumeros() que devolverá un ArrayList con todos los números almacenados.

Para informar al programador que utiliza la clase de las situaciones anómalas que se pueden dar, como que exista un problema con la localización del almacén permanente o los datos no se puedan recuperar como números, se crearán dos clases de excepción personalizadas que permitan al programador abstraerse de detalles relacionados con el uso de ficheros.

En próximos temas se explicarán con detenimiento las instrucciones para manipulación de ficheros. He aquí el código de la clase AlmacenNumeros:

package practica2;
import java.io.*;
import java.util.*;
public class AlmacenNumeros {
	String archivo;
	public AlmacenNumeros(String ruta){
		//se recibe como parámetro la carpeta
		//donde se quieren almacenar los datos
		archivo=ruta+"\\misdatos.txt";
	}
	public void guardarNumero(int n)throws
		DireccionInvalidaException{
		try{
			FileWriter destino=new FileWriter(archivo);
			destino.write(String.valueOf(n));
			destino.close();
		}
		catch(IOException e){
			//lanza la excepción personalizada
			//para indicar un problema con el fichero
			throw new DireccionInvalidaException();
		}
	}
	public ArrayList<Integer> recuperarNumeros() throws
		FormatoIncorrectoException, DireccionInvalidaException{
		ArrayList<Integer> nums= new ArrayList<Integer>();
		try{
			FileReader destino=new FileReader(archivo);
			BufferedReader bf=new BufferedReader(destino);
			String dato=bf.readLine();
			while((dato=bf.readLine())!=null){
				nums.add(new Integer(dato));
			}
			destino.close();
		}
		catch(IOException e){
			throw new DireccionInvalidaException();
		}
		catch(NumberFormatException e){
			throw new FormatoIncorrectoException();
		}
		finally{
			return nums;
		}
	}
}

En cuanto a las clases de excepción personalizadas, en el siguiente listado se muestra el código de las mismas:

package practica2;
public class DireccionInvalidaException extends Exception{
	public DireccionInvalidaException(){
		super("la dirección del fichero no es correcta");
	}
	public DireccionInvalidaException(String mensaje){
		super(mensaje);
	}
}

Y

package practica2;
public class FormatoIncorrectoException extends Exception{
	public FormatoIncorrectoException(){
		super("Formato del número incorrecto");
	}
	public FormatoIncorrectoException(String mensaje) {
		super(mensaje);
	}
}