pcollaog's blog

Linux, java, música y otras hierbas

Checked Y Unchecked Exception

| Comments

Dado que varios de mis lectores me han solicitado un post sobre tipos de excepciones en Java, les dejaré un par de notas para que consideren al momento de diseñar soluciones y por su puesto el cómo manejar los errores. Aquí vamos!

Primero que todo, un par de definiciones básicas y características antes de partir.

Jerarquía de Excepciones

Esta es la jerarquía de excepciones de mas alto nivel que encontramos en Java.

Unchecked Exception

Generalmente este tipo de excepciones son lanzadas por la aplicación y se generan a partir de errores en tiempo de Runtime. Este tipo de excepciones representan errores en el código y que la aplicación no es capaz de controlar. Algunos de errores causados y que lanzan este tipo de excepciones, por ejemplo, argumentos inválidos pasados a un método (argumentos null pueden causar NullPointerException), otro error común son la excepciones del tipo IndexOutOfBoundsException y que son lanzadas cuando se quieren obtener elementos de una lista y el índice que se entrega está fuera del tamaño del arreglo. Como podrán ver, son errores de programación y que generarán defectos en momento de correr la aplicación (no así al compilar).

Unchecked runtime exceptions represent conditions that, generally speaking, reflect errors in your program's logic and cannot be reasonably recovered from at runtime.

Gosling, Arnold and Holmes The Java Programming Language

Las excepciones de tipo Unchecked son subclases que heredan desde RuntimeException. Además este tipo de excepciones no tienen la obligación de ser declaradas con la cláusula throws en la cabecera del método. Otra característica es que tampoco se tiene la obligación de atraparlas con un catch como se muestra en el ejemplo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
 * Ejemplo de exception tipo {@link RuntimeException}
 *
 */
public class RuntimeDemo {

    /**
     * Método principal
     */
    public void mainMethod() {
        methodThowsRuntimeException();
        methodThowsRuntimeException2();
    }

    /**
     * Este método lanzará una excepción de tipo runtime no declarada en su
     * firma
     */
    public void methodThowsRuntimeException() {
        throw new ExampleRuntimeException();
    }

    /**
     * Este método lanzará una excepción de tipo runtime está declarada en su
     * firma (no es obligación) pero deja mas claro al desarrollador las
     * excepciones que debería manejar con la API.
     * 
     * @throws ExampleRuntimeException
     *             en caso de error
     */
    public void methodThowsRuntimeException2() throws ExampleRuntimeException {
        throw new ExampleRuntimeException();
    }

    /**
     * Clase de error tipo Runtime
     */
    public static class ExampleRuntimeException extends RuntimeException {

        public ExampleRuntimeException() {
            super();
        }
    }
}

Checked Exception

Este tipo de excepciones representan condiciones inválidas en el contexto de la línea de ejecución y que están fuera del control de dicho contexto, como por ejemplo, problemas con la base de datos, problemas de red, acceso a los archivos. También pueden ser condiciones de ingreso al sistema en donde el sistema no tiene ninguna participación, como por ejemplo, ingresar un nombre de usuario y contraseña incorrectos.

Contexto de ejecución ó scope: Corresponde al las líneas de código que están encerradas en un bloque de código, como por ejemplo, un método, un try/catch, bloque estático, etc.

Este tipo de excepciones deben ser declaradas en la firma del método. Además deben ser atrapadas dentro de los bloques de código donde se invoque un método que contenga la clausula throws.

Todas las excepciones de este tipo son subclases que heredan desde Exception, como por ejemplo:

Otra característica de este tipo de excepciones es que existe una probabilidad de recuperación de la ejecución y el método puede realizar alguna acción correctiva y/o informativa (log) en el bloque catch o simplemente relanzar la excepción y confiar en que el método invocante la atrape y haga algo con ella.

Para enviar el stacktrace al sistema de log (systemout) pueden usar los métodos de Throwable y en específico al método printStackTrace).

Un pequeño ejemplo de checked exception:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * Clase de ejemplo para checked exceptions
 */
public class CheckedExample {

    /**
     * Método que atrapa una checked exception
     */
    public void catchCheckedException() {
        try {
            throwCheckedException();
        } catch (ExampleCheckedException e) {
            // TODO: hacer algo en caso de error (recuperacion)
        }
    }

    /**
     * Método que relanza la checked exception a un método superior
     * 
     * @throws ExampleCheckedException
     *             checked exception error
     */
    public void rethrowCheckedException() throws ExampleCheckedException {
        throwCheckedException();
    }

    /**
     * Método que lanza una checked exception
     * 
     * @throws ExampleCheckedException
     *             checked exception error
     */
    private void throwCheckedException() throws ExampleCheckedException {
        throw new ExampleCheckedException();
    }

    /**
     * Clase que representa la excepcion de ejemplo
     */
    private static final class ExampleCheckedException extends Exception {

        public ExampleCheckedException() {
            super();
        }

    }
}

Error

Las excepciones de tipo Error son excepciones en las que el sistema no puede hacer nada con ellas, son clasificadas como errores irreversibles y que en su mayoría provienen desde la JVM, como por ejemplo: IOError, NoClassDefFoundError, NoSuchMethodError, OutOfMemoryError y VirtualMachineError por mencionar algunos de los errores.

Un poco de diseño y consejos para el manejo de excepciones

Para iniciar esta última parte, comenzaré con algunos ejemplos de malas prácticas con las que siempre nos encontramos cuando programamos, les dejo unas pocas:

java Ejemplo 1: El catch que no hace nada
1
2
3
4
5
try {
    //ejecución que lanza checked exceptions
} catch (ExampleCheckedException e) {
    // No hace nada
}

En el ejemplo de arriba claramente no se hace nada con la excepción dentro del try, lo recomendable es que si de verdad no vas a hacer nada con la excepción, al menos debes enviarla a tu sistema de Logger favorito con algún nivel de debug aceptable para poder revisar el log. Siempre se recomienda hacer algo en el bloque catch ya que ocurrió un error que debe ser controlado.

Ejemplo 2: Relanzar la excepción en el catch
1
2
3
4
5
try {
    //ejecución que lanza checked exceptions
} catch (ExampleCheckedException e) {
    throw e;
}

Es similar ejemplo 1 pero lo que se hace es relanzar la excepción atrapada hacia el método invocante. La recomendación es que nunca hagas eso ya que se presta para confusión al leer el código fuente y en la practica se estarían ejecutando dos bloques catch para la misma excepción (el directo y el del método invocante).

Ejemplo 3: Atrapar todas las excepciones.
1
2
3
4
5
try {
    //ejecución que lanza checked exceptions
} catch (Exception e) {
    // alguna lógica de negocio
}

Es recomendable que nunca atrapen todas las excepciones en un bloque catch y básicamente porque uno pierde la noción de por qué se produjo la excepción. Además en ese bloque también se atrapan las excepciones de tipo Runtime y ya mencionamos que estas excepciones significan errores en tu programa y que deben ser depurados (no escondidos debajo de la alfombra). Lo mejor es atrapar cada una de las excepciones y darle un tratamiento a cada una, si necesitan agrupar usen la herencia/jerarquía de las excepciones.

Ejemplo 4: instanceof en el catch
1
2
3
4
5
6
7
8
9
10
11
try {
    //ejecución que lanza checked exceptions
} catch (Exception e) {
    // alguna lógica de negocio

    if (e instanceof BlaException ){

    } else if (e instanceof FooException) {

    } else if .....
}

Ese bloque de if’s compuestos se debe transformar en varios catch para cada una de las excepciones lanzadas. No usen un control de errores manual, es mejor usar las herramientas que te provee el lenguaje.

Ejemplo 5: No abusar de las checked exceptions
1
2
3
4
5
6
7
8
9
10
11
12
13
try {
    //ejecución que lanza checked exceptions
} catch (ErrorBlaException e) {
    ...
} catch (ErrorFooException e) {
    ...
} catch (ErrorOMGException e) {
    ...
} catch (SDWException e) {
    ...
} catch (Exception e) {
    ...
}

No abusar de las checked exceptions ya que hacen nuestro código confuso y poco mantenible. Si bien es cierto, es la herramienta que nos provee el lenguaje, no abusemos de ella y convirtamos los bloques catch en pseudo programas y rutinas anexas a la lógica de negocio (que es la que vale).

Ejemplo 6: Lanzar el misil "Exception"
1
2
3
private void foo() throws Exception {
 // código de negocio   
}

Nunca lancen Exception como una excepción de su lógica de negocio y es que básicamente los catch están pensados en atrapar excepciones particulares y al lanzar Exception (de la mas alta jerarquía) jamás entrarás al bloque catch que corresponda y que pueda gestionar el error. Por otro lado el programador pierde la visibilidad de los errores particulares que debe gestionar.

Ejemplo 7: Lanzar el misil nuclear "RuntimeException"
1
2
3
4
5
6
7
private void foo() throws Exception {
    try {
        // logica de negocio
    } catch (ParserConfigurationException e) {
        throw new RuntimeException("Error");
    }
}

Jamás se debe hacer esto, jamás!. Esto romperá todo tu programa ya que al lanzar la excepción RuntimeException esta llegará sin control a la capa mas alta provocando un error. Recuerden que ese tipo de excepciones son errores sin recuperación y justamente estamos tratando de hacer lo contrario gestionar los errores de lógica de negocio.

 Mantener un árbol de excepciones

Esta sección del post quizás sea el más polémico ya que no hay receta perfecta para el manejo de excepciones y daré mis consejos (personales), puede que estén de acuerdo como puede que no.

Les recomiendo siempre mantener un árbol de excepciones que representen los errores (de negocio y de ejecución) de tu aplicación. Para que sea mas simple la mantención del árbol de excepciones, usen polimorfismo, herencia y todas las herramientas que ofrece OOP. En este punto siempre hay detractores de los árboles de excepciones con la excusa de su mantención.

Contraria a mi propuesta de manejo de errores, existen quienes mantienen sólo 1 excepción y a dicha excepción le agregan atributos y cuanta metadata puedan agregar. Qué se consigue finalmente con ese esquema de errores, es llenarte de IF por todos lados mirando los atributos que contiene la instancia de excepción y haciendo todo un control de errores manual.

Palabras al cierre

Quedan muchas cosas por mencionar de las excepciones y ahondar mucho mas en cómo diseñar y construir un árbol de excepciones, creo que sera materia para otro artículo. Demás esta decirles que esta abierta la discusión. Los comentarios bienvenidos sean.

Comments