Excepciones en Java

Manejo De Excepciones en Java


Manejo de Errores Utilizando Excepciones

Existe una regla de oro en el mundo de la programación: en los programas ocurren errores:
¿Qué sucede realmente después de que ha ocurrido el error?
¿Cómo se maneja el error?
¿Quién lo maneja?
¿Puede recuperarlo el programa?

El lenguaje Java utiliza excepciones para proporcionar capacidades de manejo de errores.


¿Qué es un Excepción y Por Qué Debo Tener Cuidado?

El término excepción es un forma corta da la frase "suceso excepcional" y puede definirse de la siguiente forma:

Definición:
Una excepción es un evento que ocurre durante la ejecución del programa que interrumpe el flujo normal de las sentencias.

Muchas clases de errores pueden utilizar excepciones — desde serios problemas de hardware, como la avería de un disco duro, a los simples errores de programación, como tratar de acceder a un elemento de un array fuera de sus límites. Cuando dicho error ocurre dentro de un método Java, el método crea un objeto 'exception' y lo maneja fuera, en el sistema de ejecución. Este objeto contiene información sobre la excepción, incluyendo su tipo y el estado del programa cuando ocurrió el error.

El sistema de ejecución es el responsable de buscar algún código para manejar el error. En terminología java, crear una objeto exception y manejarlo por el sistema de ejecución se llama lanzar una excepción.

Después de que un método lance una excepción, el sistema de ejecución entra en acción para buscar el manejador de la excepción. El conjunto de "algunos" métodos posibles para manejar la excepción es el conjunto de métodos de la pila de llamadas del método donde ocurrió el error.

Un manejador de excepción es considerado adecuado si el tipo de la excepción lanzada es el mismo que el de la excepción manejada por el manejador. Así la excepción sube sobre la pila de llamadas hasta que encuentra el manejador apropiado y una de las llamadas a métodos maneja la excepción, se dice que el manejador de excepción elegido captura la excepción.

Mediante el uso de excepciones para manejar errores, los programas Java tienen las siguientes ventajas frente a las técnicas de manejo de errores tradicionales.

* Ventaja 1: Separar el Manejo de Errores del Código "Normal"
* Ventaja 2: Propagar los Errores sobre la Pila de Llamadas
* Ventaja 3: Agrupar los Tipos de Errores y la Diferenciación de éstos


Ventaja 1: Separar el Manejo de Errores del Código "Normal"

En la programación tradicional, la detección, el informe y el manejo de errores se convierte en un código muy liado. Por ejemplo, supongamos que tenemos una función que lee un Archivo completo dentro de la memoria. En pseudo-código, la función queda:

leerArchivo {
    abrir el Archivo;
    determinar su tamaño;    
    asignar suficiente memoria;
    leer el Archivo a la memoria;
    cerrar el Archivo;
}

A primera vista esta función parece bastante sencilla, pero ignora todos aquello errores potenciales.

* ¿Qué sucede si no se puede abrir el Archivo?
* ¿Qué sucede si no se puede determinar la longitud del Archivo?
* ¿Qué sucede si no hay suficiente memoria libre?
* ¿Qué sucede si la lectura falla?
* ¿Qué sucede si no se puede cerrar el Archivo?

Para responder a estas cuestiones dentro de la función, tendríamos que añadir mucho código para la detección y el manejo de errores. El aspecto final de la función se parecería esto:

codigodeError leerArchivo {
    inicializar codigodeError = 0;
    abrir el Archivo;
    if (ArchivoAbierto) {
        determinar la longitud del Archivo;
        if (obtenerLongitudDelArchivo) {
            asignar suficiente memoria;
            if (obtenerSuficienteMemoria) {
                leer el Archivo a memoria;
                if (falloDeLectura) {
                    codigodeError = -1;
                }
            } else {
                codigodeError = -2;
            }
        } else {
            codigodeError = -3;
        }
        cerrar el Archivo;
        if (ArchivoNoCerrado && codigodeError == 0) {
            codigodeError = -4;
        } else {
            codigodeError = codigodeError and -4;
        }
    } else {
        codigodeError = -5;
    }
    return codigodeError;
}

En comparación con las líneas de código original, con obtenido se puede observar un incremento sumamente grande, más del 300 %, además nos podemos llegar a confundir con mucha facilidad.

Java proporciona una solución elegante al problema del tratamiento de errores: las excepciones. Las excepciones le permiten escribir el flujo principal de su código y tratar los casos excepcionales en otro lugar. Si la función leerFcihero utilizara excepciones en lugar de las técnicas de manejo de errores tradicionales se podría parecer a esto.

leerArchivo{
    try {
           abrir el Archivo;
           determinar su tamaño;
           asignar suficiente memoria;
           leer el Archivo a la memoria;
           cerrar el Archivo;
    } catch (falloAbrirArchivo) {
        hacerAlgo;
    } catch (falloDeterminacionTamaño) {
        hacerAlgo;
    } catch (falloAsignaciondeMemoria) {
        hacerAlgo;
    } catch (falloLectura) {
        hacerAlgo;
    } catch (falloCerrarArchivo) {
        hacerAlgo;
    }
}

Observa que las excepciones no evitan el esfuerzo de hacer el trabajo de detectar, informar y manejar errores. Lo que proporcionan las excepciones es la posibilidad de separar los detalles oscuros de qué hacer cuando ocurre algo fuera de la normal.


Introducción a la programación en Java

Las excepciones en Java están destinadas, al igual que en el resto de los lenguajes que las soportan, para la detección y corrección de errores. Si hay un error, la aplicación no debería morirse y generar un core (o un crash en caso del DOS). Se debería lanzar (throw) una excepción que nosotros deberíamos capturar (catch) y resolver la situación de error. Java sigue el mismo modelo de excepciones que se utiliza en C++. Utilizadas en forma adecuada, las excepciones aumentan en gran medida la robustez de las aplicaciones.


Manejo De Excepciones

Vamos a mostrar como se utilizan las excepciones, reconvirtiendo nuestro applet de saludo a partir de la versión iterativa de HolaIte.java:

import java.awt.*;
import java.applet.Applet;
 
public class HolaIte extends Applet {
    private int i = 0;
    private String Saludos[] = {
        "Hola Mundo!",
        "HOLA Mundo!",
        "HOLA MUNDO!!"
        };
 
    public void paint( Graphics g ) {
        g.drawString( Saludos[i],25,25 );
        i++;
        }
    }

Normalmente, un programa termina con un mensaje de error cuando se lanza una excepción. Sin embargo, Java tiene mecanismos para excepciones que permiten ver qué excepción se ha producido e intentar recuperarse de ella.

Vamos a reescribir el método paint() de nuestra versión iterativa del saludo:

public void paint( Graphics g ) {
    try {
        g.drawString( Saludos[i],25,25 );
        } catch( ArrayIndexOutOfBoundsException e ) {
            g.drawString( "Saludos desbordado",25,25 );
        } catch( Exception e ) {
            // Cualquier otra excepción
            System.out.println( e.toString() );  
        } finally {
            System.out.println( "Esto se imprime siempre!" );
        }
    i++;
    }

La palabra clave finally define un bloque de código que se quiere que sea ejecutado siempre, de acuerdo a si se capturó la excepción o no. En el ejemplo anterior, la salida en la consola, con i=4 sería:
Saludos desbordado
¡Esto se imprime siempre!


Generar Excepciones en Java
Cuando se produce un error se debería generar, o lanzar, una excepción. Para que un método en Java, pueda lanzar excepciones, hay que indicarlo expresamente.

void MetodoAsesino() throws NullPointerException,CaidaException

Se pueden definir excepciones propias, no hay por qué limitarse a las predefinidas; bastará con extender la clase Exception y proporcionar la funcionalidad extra que requiera el tratamiento de esa excepción.

También pueden producirse excepciones no de forma explícita como en el caso anterior, sino de forma implícita cuando se realiza alguna acción ilegal o no válida.

Las excepciones, pues, pueden originarse de dos modos: el programa hace algo ilegal (caso normal), o el programa explícitamente genera una excepción ejecutando la sentencia throw (caso menos normal). La sentencia throw tiene la siguiente forma:

throw ObtejoExcepction;
 
El objeto ObjetoException es un objeto de una clase que extiende la clase Exception.
 
El siguiente código de ejemplo origina una excepción de división por cero:
 
class melon {
    public static void main( String[] a ) {
        int i=0, j=0, k;
 
        k = i/j;    // Origina un error de division-by-zero
        }
    }

Si compilamos y ejecutamos esta aplicación Java, obtendremos la siguiente salida por pantalla:

> javac melon.java
> java melon
     java.lang.ArithmeticException: / by zero
           at melon.main(melon.java:5)

Las excepciones predefinidas, como ArithmeticException, se conocen como excepciones runtime. Actualmente, como todas las excepciones son eventos runtime, sería mejor llamarlas excepciones irrecuperables. Esto contrasta con las excepciones que generamos explícitamente, que suelen ser mucho menos severas y en la mayoría de los casos podemos recuperarnos de ellas. Por ejemplo, si un fichero no puede abrirse, preguntamos al usuario que nos indique otro fichero; o si una estructura de datos se encuentra completa, podremos sobreescribir algún elemento que ya no se necesite.


Excepciones Predefinidas

Las excepciones predefinidas y su jerarquía de clases es la que se muestra en la figura:
excepciones predefinidas

Los nombres de las excepciones indican la condición de error que representan. Las siguientes son las excepciones predefinidas más frecuentes que se pueden encontrar:
ArithmeticException

Las excepciones aritméticas son típicamente el resultado de una división por 0:

int i = 12 / 0;

NullPointerException

Se produce cuando se intenta acceder a una variable o método antes de ser definido:

class Hola extends Applet {
Image img;

paint( Graphics g ) {
g.drawImage( img,25,25,this );
}
}

IncompatibleClassChangeException

El intento de cambiar una clase afectada por referencias en otros objetos, específicamente cuando esos objetos todavía no han sido recompilados.

ClassCastException

El intento de convertir un objeto a otra clase que no es válida.

y = (Prueba)x; // donde
x no es de tipo Prueba

NegativeArraySizeException

Puede ocurrir si hay un error aritmético al intentar cambiar el tamaño de un array.

OutOfMemoryException

¡No debería producirse nunca! El intento de crear un objeto con el operador new ha fallado por falta de memoria. Y siempre tendría que haber memoria suficiente porque el garbage collector se encarga de proporcionarla al ir liberando objetos que no se usan y devolviendo memoria al sistema.

NoClassDefFoundException

Se referenció una clase que el sistema es incapaz de encontrar.

ArrayIndexOutOfBoundsException

Es la excepción que más frecuentemente se produce. Se genera al intentar acceder a un elemento de un array más allá de los límites definidos inicialmente para ese array.

UnsatisfiedLinkException

Se hizo el intento de acceder a un método nativo que no existe. Aquí no existe un método a.kk

class A {
native void kk();
}

y se llama a a.kk(), cuando debería llamar a A.kk().

InternalException

Este error se reserva para eventos que no deberían ocurrir. Por definición, el usuario nunca debería ver este error y esta excepción no debería lanzarse.


Crear Excepciones Propias

También podemos lanzar nuestras propias excepciones, extendiendo la clase System.exception. Por ejemplo, consideremos un programa cliente/servidor. El código cliente se intenta conectar al servidor, y durante 5 segundos se espera a que conteste el servidor. Si el servidor no responde, el servidor lanzaría la excepción de time-out:

class ServerTimeOutException extends Exception {}
 
public void conectame( String nombreServidor ) throws Exception {
    int exito;
    int puerto = 80;
 
    exito = open( nombreServidor,puerto );
    if( exito == -1 )
        throw ServerTimeOutException;
    }

Si se quieren capturar las propias excepciones, se deberá utilizar la sentencia try:

public void encuentraServidor() {
   ...
   try {
        conectame( servidorDefecto );
        catch( ServerTimeOutException e ) {
            g.drawString(
                "Time-out del Servidor, intentando alternativa",
                5,5 );
            conectame( servidorAlterno );
            }
    ...
    }

Cualquier método que lance una excepción también debe capturarla, o declararla como parte de la interface del método. Cabe preguntarse entonces, el porqué de lanzar una excepción si hay que capturarla en el mismo método. La respuesta es que las excepciones no simplifican el trabajo del control de errores. Tienen la ventaja de que se puede tener muy localizado el control de errores y no tenemos que controlar millones de valores de retorno, pero no van más allá.

Propagación de Excepciones

La cláusula catch comprueba los argumentos en el mismo orden en que aparezcan en el programa. Si hay alguno que coincida, se ejecuta el bloque y sigue el flujo de control por el bloque finally (si lo hay) y concluye el control de la excepción.

Si ninguna de las cláusulas catch coincide con la excepción que se ha producido, entonces se ejecutará el código de la cláusula finally (en caso de que la haya). Lo que ocurre en este caso, es exactamente lo mismo que si la sentencia que lanza la excepción no se encontrase encerrada en el bloque try.

El flujo de control abandona este método y retorna prematuramente al método que lo llamó. Si la llamada estaba dentro del ámbito de una sentencia try, entonces se vuelve a intentar el control de la excepción, y así continuamente.

Veamos lo que sucede cuando una excepción no es tratada en la rutina en donde se produce. El sistema Java busca un bloque try..catch más allá de la llamada, pero dentro del método que lo trajo aquí. Si la excepción se propaga de todas formas hasta lo alto de la pila de llamadas sin encontrar un controlador específico para la excepción, entonces la ejecución se detendrá dando un mensaje. Es decir, podemos suponer que Java nos está proporcionando un bloque catch por defecto, que imprime un mensaje de error y sale.

No hay ninguna sobrecarga en el sistema por incorporar sentencias try al código. La sobrecarga se produce cuando se genera la excepción.

Hemos dicho ya que un método debe capturar las excepciones que genera, o en todo caso, declararlas como parte de su llamada, indicando a todo el mundo que es capaz de generar excepciones. Esto debe ser así para que cualquiera que escriba una llamada a ese método esté avisado de que le puede llegar una excepción, en lugar del valor de retorno normal. Esto permite al programador que llama a ese método, elegir entre controlar la excepción o propagarla hacia arriba en la pila de llamadas. La siguiente línea de código muestra la forma general en que un método declara excepciones que se pueden propagar fuera de él:

tipo_de_retorno( parametros ) throws e1,e2,e3 { }

Los nombres e1,e2,… deben ser nombres de excepciones, es decir, cualquier tipo que sea asignable al tipo predefinido Throwable. Observar que, como en la llamada al método se especifica el tipo de retorno, se está especificando el tipo de excepción que puede generar (en lugar de un objeto exception).

He aquí un ejemplo, tomado del sistema Java de entrada/salida:

byte readByte() throws IOException;
short readShort() throws IOException;
char readChar() throws IOException;
 
void writeByte( int v ) throws IOException;
void writeShort( int v ) throws IOException;
void writeChar( int v ) throws IOException;

Lo más interesante aquí es que la rutina que lee un char, puede devolver un char; no el entero que se requiere en C. C necesita que se devuelva un int, para poder pasar cualquier valor a un char, y además un valor extra (-1) para indicar que se ha alcanzado el final del fichero. Algunas de las rutinas Java lanzan una excepción cuando se alcanza el fin del fichero.

En el siguiente diagrama se muestra gráficamente cómo se propaga la excepción que se genera en el código, a través de la pila de llamadas durante la ejecución del código:
<a href="http://s207.photobucket.com/albums/bb254/rotten_sPy/?action=view&current=excepa.gif" target="_blank"><img src="http://i207.photobucket.com/albums/bb254/rotten_sPy/excepa.gif" border="0" alt="ejemplo de excepcion"></a>

Cuando se crea una nueva excepción, derivando de una clase Exception ya existente, se puede cambiar el mensaje que lleva asociado. La cadena de texto puede ser recuperada a través de un método. Normalmente, el texto del mensaje proporcionará información para resolver el problema o sugerirá una acción alternativa. Por ejemplo:

class SinGasolina extends Exception {
SinGasolina( String s ) { // constructor
super( s );
}
….

// Cuando se use, aparecerá algo como esto

try {
        if( j < 1 )
            throw new SinGasolina( "Usando deposito de reserva" );
    } catch( SinGasolina e ) {
        System.out.println( o.getMessage() );
        }

Esto, en tiempo de ejecución originaría la siguiente salida por pantalla:

Usando deposito de reserva

Otro método que es heredado de la superclase Throwable es printStackTrace(). Invocando a este método sobre una excepción se volcará a pantalla todas las llamadas hasta el momento en donde se generó la excepción (no donde se maneje la excepción). Por ejemplo:

// Capturando una excepción en un método
class testcap {
    static int slice0[] = { 0,1,2,3,4 };
 
    public static void main( String a[] ) {
        try {
            uno();
        } catch( Exception e ) {
            System.out.println( "Captura de la excepcion en main()" );
            e.printStackTrace();
            }
        }
 
    static void uno() {
        try {
            slice0[-1] = 4;
        } catch( NullPointerException e ) {
            System.out.println( "Captura una excepcion diferente" );
            }
        }
    }

Cuando se ejecute ese código, en pantalla observaremos la siguiente salida:

Captura de la excepcion en main()
java.lang.ArrayIndexOutOfBoundsException: -1

at testcap.uno(test5p.java:19)
at testcap.main(test5p.java:9)

Con todo el manejo de excepciones podemos concluir que proporciona un método más seguro para el control de errores, además de representar una excelente herramienta para organizar en sitios concretos todo el manejo de los errores y, además, que podemos proporcionar mensajes de error más decentes al usuario indicando qué es lo que ha fallado y por qué, e incluso podemos, a veces, recuperarnos de los errores.

La degradación que se produce en la ejecución de programas con manejo de excepciones está ampliamente compensada por las ventajas que representa en cuanto a seguridad de funcionamiento de esos mismos programas.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License