¡Esta es una revisión vieja del documento!


Tratamiento de Excepciones

El tratamiento de excepciones en Java demasiadas veces se realiza de forma errónea. Veamos ahora la forma de hacer un correcto tratamiento de excepciones.

Excepciones en Java

Empecemos recordando los dos tipos de excepciones que hay en Java1)

PlantUML Graph

Excepciones Checked

Las excepciones Checked son aquellas que deben declararse en el método mediante la palabra throws y que obligan al que lo llama a hacer un tratamiento de dicha excepción.

Son excepciones Checked cualquier objeto de la clase Exception o de cualquier otra clase que herede de ella, excepto si el objeto es de la clase RuntimeException o cualquier otra clase que herede de ésta última.

 1: public class Matematicas {
 2:     public double dividir(double a, double b) throws Exception {
 3:         if (b == 0) {
 4:             throw new Exception("El argumento b no puede ser 0");
 5:         }
 6:
 7:         return a / b;
 8:     }
 9: }

El método dividir lanza en la línea 4 una Checked Exception por lo tanto el método lo declara mediante throws.

Ahora cualquiera que llame al método dividir está obligado a tratar la excepción Exception de dos formas:

  • Mediante un try-cath:
     1: public class Main {
     2:     public static void main(String[] args) {
     3:         Matematicas matematicas=new Matematicas();
     4:         try {
     5:             double c=matematicas.dividir(-1.6, 0);
     6:         } catch (Exception ex) {
     7:             //Tratar la excepción
     8:         }
     9:     }
    10: }
    

    En las líneas 4,6,7 y 8, se usa un try-catch para atrapar la excepción que puede producir la llamada el método dividir.

  • Declarándo a su vez en el método que puede lanzar dicha excepción:
    1: public class Main {
    2:     public static void main(String[] args) throws Exception {
    3:         Matematicas matematicas=new Matematicas();
    4:
    5:         double c=matematicas.dividir(-1.6, 0);
    6:     }
    7: }
    

    En la línea 2 vemos como se declara´que el método main puede lanzar una excepción del tipo Exception ya que dividir lo lanza y no se atrapa con un try-catch.

Excepciones Unchecked

Las excepciones Unchecked son aquellas que no se declaran en el método y que no obligan al que lo llama a hacer un tratamiento de dicha excepción.

Son excepciones Unchecked todas aquellas excepciones que heredan de la clase RuntimeException.

 1: public class Matematicas {
 2:     public double dividir(double a, double b) {
 3:         if (b == 0) {
 4:             throw new RuntimeException("El argumento b no puede ser 0");
 5:         }
 6:
 7:         return a / b;
 8:     }
 9: }

El método dividir lanza en la línea 4 una unchecked Exception por lo que no es necesario hacer nada especial con ella.

1: public class Main {
2:     public static void main(String[] args) {
3:         Matematicas matematicas=new Matematicas();
4:
5:         double c=matematicas.dividir(-1.6, 0);
6:     }
7: }

Como la excepción que lanza dividir es Unchecked no necesitamos tratarla en el método main de ninguna forma especial.

Cuando usar excepciones Checked o Unchecked y que hacer con ellas

Una vez visto como Java trata las excepciones de ambos tipos pasemos a explicar cuando debemos usar las de un tipo u otro.

Checked Exceptions

Las excepciones Checked son condiciones excepcionales del flujo del programa pero que no son debido a un error del propio programa. Es decir si se lanza una excepción checked el programa no tiene ningún tipo de error y sigue funcionando correctamente, simplemente se ha producido una situación excepcional que hemos marcado como una excepción.

Por ejemplo si tenemos una función que borra ficheros y al ir a borrarlo , dicho fichero ya no existe, deberemos lanzar una FileNotFoundException indicando que el fichero no existe. Que el fichero ya no exista no es ningún error en el programa, puede haberse borrado justo un instante antes , sin embargo, si que es una situación excepcional porque la mayoría de las veces cuando vayamos a borrarlo deberá estar el fichero si hemos acabado de seleccionarlo.

De ahí que al ser usa situación legitima del programa pero poco probable los creadores de Java han decidido que es necesario declararla en el método y que sea obligatorio tratarla. Las Checked Exceptions no tienen nada de malo y son muy útiles para realizar correctamente los programas.

La Checked Exceptions forman parte del flujo lógico de nuestro programa y al programar debemos tenerlas en cuenta y tratarlas adecuadamente no viéndolas como un problema del que debemos deshacernos.

Lo que nunca se debe hacer con una Checked Exception es imprimir la traza del error y/o seguir con la ejecución. Si la forma correcta de tratar excepción es no hacer nada, ¿para que imprimes la traza? y si hay que hacer algo con el excepción ¿era necesario imprimir la traza?.

Así que los siguientes trozos de código son erroneos:

1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (Exception ex) {
4:     //Ignorar la excepción
5: }
1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (Exception ex) {
4:     Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
5: }
1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (Exception ex) {
4:     ex.printStackTrace();
5: }

Aunque hemos puesto el mismo ejemplo del método dividir tanto para explicar las excepciones Checked o Uncheked tras la explicación debe quedar claro que la excepción que debe lanzar el método dividir debe ser del tipo unchecked.

Unchecked Exceptions

Este tipo de error es mucho mas sencillo de entender. Una excepción unchecked es aquella que se lanza cuando hay un error en el programa.

Por ejemplo en el caso anterior de la división por cero, es un error de programación que alguien nos pase como dividendo un cero, en ese caso lanzaremos un RuntimeException.

Al ser las unchecked Exceptions errores en el programa , poco podemos hacer por arreglarlas. Por ello no es obligatorio tratarlas con un try-catch. Lo único que podemos hacer en la mayoría de casos es hacer que el programa acabe fallando y que se muestre un mensaje de error al usuario. Es decir, si el programa funciona mal , es mejor que no siga funcionando antes que hacer que continúe funcionado pero usando datos erróneos, ya que ne ese caso los resultados que generara también podrían ser erróneos.

Las unchecked exceptions no debemos tratarlas ya que no forma parte de la lógica de la aplicación y no debemos hacer nada con ella, simplemente debemos preocuparnos en el mensaje que acabará llegando al usuario. En una aplicación Web es tan sencillo como tener páginas de error personalizadas.

El problema con las excepciones

Hasta ahora hemos visto los dos tipos de excepciones que hay , como las trata el compilador de Java y cuando usar una u otra. Sin embargo hay un problema con las excepciones que hace que no se traten de forma correcta.

El problema de las excepciones es que en muchas APIs de Java se lanzan checked cuando deberían ser unchecked.

Por ejemplo, en el API de JDBC al ejecutar una consulta mediante executeQuery() se lanzar la excepción java.sql.SQLException que es de tipo Checked si la SQL es erronea. ¿Acaso eso no es un error de programación y por lo tanto debería ser de tipo unchecked? Es decir que tenemos un problema ya que deberemos tratar excepciones Checked pero no se pueden hacer ningún tratamiento para solucionar lo que generó dicha excepción.

El verdadero problema de Java es que muchas API lanzan Checked exceptions cuando deberían haber sido uncheked exceptions.

Como acabamos de decir, eso nos lleva a los programadores a capturar dichas excepciones y tener que tratarlas. Pero ¿como tratamos dichas excepciones Checked si realmente son errores de programa y por lo tanto excepciones unchecked? Pues solo hay una forma de hacerlo, es transformando las excepciones Checked en unchecked.

1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (Exception ex) {
4:     throw new RuntimeException("Fallo al llamar a 'dividir'",ex)
5: }

Vemos en la línea 4 como lanzamos una nueva excepción del tipo unchecked pero dentro contiene la excepción original que se ha producido ya que se le pasa como argumento. Con ésto hemos solucionado el problema. Pero vuelvo a repetir ésta construcción solo debe hacerse con aquellas excepciones checked que consideramos que deberían haber sido unchecked.

Esta construcción tan sencilla que consiste en que las excepciones checked ,pero que deberían haber sido uncheked, trasformarlas en excepciones uncheked nos soluciona prácticamente todos nuestros problemas.

1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (Exception ex) {
4:     throw new RuntimeException("Fallo al llamar a 'dividir'",ex)
5: }

Mas problemas

Veamos ahora otro problema de las excepciones.Hay excepciones unchecked , que según su significado, deberían haber sido del tipo checked ya que no son debido a errores del programa (lo contrario del caso anterior).

La excepción ConstraintViolationException se lanza cuando alguna validación de una entidad no se ha cumplido. Se suele utilizar para indicar que el usuario ha introducido algún dato erroneo en la aplicación. Entonces, ¿porqué es una excepción unchecked? Según lo que se está explicando no hay duda de que debería ser del tipo Checked ya que no se lanza por un error de programación sino a causa de un dato mal introducido por el usuario.¡Es decir que tenemos otro problema!

El problema que se genera con ésto es que la excepción ConstraintViolationException no sabemos que método la va a lanzar a no ser que nos leamos la documentación, pero es importante capturarla para poder mostrar un mensaje al usuario. Si hubiera sido declarada como Checked, el propio compilador nos avisaría de cuando capturarla. Todo ésto nos puede llevar a que el usuario acabe viendo nuestra página de error de la aplicación simplemente porque se ha dejado un campo sin rellenar.

Otro problema de Java es que hay APIs que lanzan UnChecked Exceptions cuando deberían haber sido Cheked Exceptions.

La solución a este problema consiste simplemente en leerse la documentación para ver cuando se lanza una excepción unchecked , que debería haber sido Checked, para capturarla y hacer el correcto tratamiento con ella.

Mas información sobre la excepción ConstraintViolationException y como solucionar el problema lo tenemos en 03_excepciones

Formas erróneas de tratar las excepciones

Veamos ahora una serie de errores que se cometen al tratar las excepciones.

Perder la excepción original

Quizás otro origen del desconocimiento de las excepciones es que hasta Java 1.4 no se podía encapsular una excepción dentro de otra. Lo podemos ver en la clase Throwable de la que hereda Exception.

  • Throwable en Java 1.3: No existen un constructor que acepte como argumento otra excepción ni el método Throwable getCause() que obtiene la excepción encapsulada.
  • Throwable en Java 1.4: Ya dispone de un constructor al que pasarle otra excepción y el método Throwable getCause().

Esto último nos ha llevado a soluciones erróneas como la siguiente:

1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (Exception ex) {
4:     throw new RuntimeException("Falló al llamar a 'dividir':" + ex.getMessage());
5: }

En la línea 4 vemos como se crea una RuntimeException pero de la excepción original se ha pedido la traza completa y solo vamos a conseguir el mensaje de error, pero las excepciones pueden tener mucha mas información que simplemente el mensaje y la traza.

Por ejemplo la excepción SQLException dispone de los métodos getSQLState() y getErrorCode() que contiene información sobre el error concreto que ha ocurrido en la base de datos. Usando el código anterior también lo habríamos perdido.

Crear una excepción inutil

Otro segundo error es crear una nueva excepción que no aporta nada respecto a RuntimeException y usarla en vez de RuntimeException

1: public class MiExcepcion extends RuntimeException {
2:   public MiExcepcion (String msg) {
3:       super(msg);
4:   }
5: }

Creando una excepción personalizada que no aporta nada.

1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (Exception ex) {
4:     throw new MiExcepcion ("Falló al llamar a 'dividir'.");
5: }

En la línea 4 se lanza la nueva excepción MiExcepcion que no hace nada que no hiciera ya RuntimeException e incluso seguimos perdiendo la excepción original.

Pasar el problema hacia arriba

La última forma errónea de tratar la excepciones es simplemente hacer que el método superior a su vez declare que lanza esas excepciones checked pero que deberían haber sido uncheked. Con ésto lo único que hacemos es expandir el problema hacia arriba sin solucionarlo.

1: public class Main {
2:     public static void main(String[] args) throws Exception {
3:         Matematicas matematicas=new Matematicas();
4:
5:         double c=matematicas.dividir(-1.6, 0);
6:     }
7: }

En la línea 2 vemos ahora como el método Main ahora también lanza una excepción checked.

Ignorarla

Este es el peor error que podemos cometer al tratar una excepción.Consiste simplemente en ignorar la excepción y que continúe la ejecución del programa. Esto es tan peligroso ya que si se ha producido algún fallo en el programa lo más seguro es hacer que se detenga cuanto antes y no seguir haciendo operaciones con datos que quizás sean erróneos.

1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (RuntimeException ex) {
4:     //Ignorar la excepción
5: }
1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (RuntimeException ex) {
4:     Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
5: }
1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (RuntimeException ex) {
4:     ex.printStackTrace();
5: }

En los 3 casos estamos ignorando la excepción aunque en los 2 últimos dejemos una bonita traza del error. ¡El problema es que sigue ejecutándose el programa!

Trazas

Acabamos de ver en el apartado anterior como aun ignorando la excepción se guardaban 2) la traza de la excepción. Normalmente no es necesario guardar explícitamente la excepción.

En aplicaciones Web no suele ser necesario guardar en el log la excepción ya que lo suele hacer automáticamente el servidor de aplicaciones.

Mejoras en el tratamiento de excepciones

Ya hemos visto como es la forma correcta de tratar las excepciones Checked que realmente deberían ser unchecked:

1: try {
2:     double c=matematicas.dividir(-1.6, 0);
3: } catch (Exception ex) {
4:     throw new RuntimeException("Fallo al llamar a 'dividir'",ex)
5: }

Este código tiene unas pegas al respecto de las trazas que se generara por pantalla 3) cuando se produce una excepción.

Si ejecutamos el código se produce la siguiente traza:

1: Exception in thread "main" java.lang.RuntimeException: Fallo al llamar a 'dividir'
2:     at ejemplo.Main.main(Main.java:17)
3: Caused by: java.lang.Exception: El argumento b no puede ser 0
4:     at ejemplo.Matematicas.dividir(Matematicas.java:14)
5:     at ejemplo.Main.main(Main.java:15)

Como podemos ver en la traza, se muestran dos excepciones, la excepción original en la línea 3 y la RuntimeExcepcion que encapsula a la excepción original en la línea 1. Esto es inevitable así que por ahora lo vamos a asumir.

El problema viene si el método dividir puede lanzar tanto una Exception como una RuntimeException.

Veamos el código modificado de dividir para que en caso de que algún número sea negativo se produzca una RuntimeException:

 1: public class Matematicas {
 2:     public double dividir(double a, double b) throws Exception {
 3:         if (a<0) {
 4:             throw new RuntimeException("El argumento a no puede ser negativo");
 5:         }
 6:         if (b<0) {
 7:             throw new RuntimeException("El argumento b no puede ser negativo");
 8:         }
 9:
10:         if (b == 0) {
11:             throw new Exception("El argumento b no puede ser 0");
12:         }
13:
14:         return a / b;
15:     }
16: }

Vemos en las líneas de la 3 a la 8 como ahora también se lanza un RuntimeException.

Si volvemos ahora a ejecutar el programa se genera la siguiente traza de error:

1: Exception in thread "main" java.lang.RuntimeException: Fallo al llamar a 'dividir'
2:     at ejemplo.Main.main(Main.java:17)
3: Caused by: java.lang.RuntimeException: El argumento a no puede ser negativo
4:     at ejemplo.Matematicas.dividir(Matematicas.java:14)
5:     at ejemplo.Main.main(Main.java:15)

Ahora la traza si que ha quedado un poco mal.Hemos encapsulado una RuntimeException (línea 3) dentro de otra RuntimeException (línea 1) lo que tiene muy poco sentido. Es decir que la traza ha quedado poco clara cuando no hay necesidad para ello. Esto ocurre ya que RuntimeException hereda de Exception por lo que el catch también atrapa la RuntimeException y la encapsula en otra RuntimeException.

La solución a ésto, es bastante sencilla.Si nos llega una RuntimeException simplemente la volvemos a lanzar en vez de encapsularla.

 1: public class Main {
 2:     public static void main(String[] args) {
 3:         Matematicas matematicas=new Matematicas();
 4:         try {
 5:             double c=matematicas.dividir(-1.6, 0);
 6:         } catch (RuntimeException ex) {
 7:             throw ex;
 8:         } catch (Exception ex) {
 9:             throw new RuntimeException("Fallo al llamar a 'dividir'",ex);
10:         }
11:     }
12: }

En la línea 6 ahora capturamos la RuntimeException y al tratarla simplemente relanzamos la misma excepción (línea 7).

Si ahora volvemos a ejecutar el código, la traza resultante es:

1: Exception in thread "main" java.lang.RuntimeException: El argumento a no puede ser negativo
2:     at ejemplo.Matematicas.dividir(Matematicas.java:14)
3:     at ejemplo.Main.main(Main.java:15)

Es decir que ahora la traza para una RuntimeException es como debería ser ,ya que no queda encapsulada por ninguna otra excepción.

La solución definitiva

La solución definitiva es que las excepciones checked tenga también una traza limpia como las unchecked. Antes hemos comentado que es imposible pero hay un truco en Java que permite hacerlo, no voy a explicar como funciona el truco simplemente voy a explicar como hacerlo.

Se debe crear la siguiente clase:

 1: public  class RelanzadorExcepciones {
 2:
 3:     public static RuntimeException lanzar( Exception ex){
 4:         RelanzadorExcepciones.<RuntimeException>lanzarComoUnchecked(ex);
 5:
 6:         throw new AssertionError("Esta línea  nunca se ejecutará pero Java no lo sabe");
 7:     }
 8:
 9:     private static <T extends Exception> void lanzarComoUnchecked(Exception toThrow) throws T{
10:         throw (T) toThrow;
11:     }
12: }

Ahora nuestro código Main se modificará quedando de la siguiente forma:

 1: public class Main {
 2:     public static void main(String[] args) {
 3:         Matematicas matematicas=new Matematicas();
 4:         try {
 5:             double c=matematicas.dividir(3, 0);
 6:         } catch (Exception ex) {
 7:             RelanzadorExcepciones.lanzar(ex);
 8:         }
 9:     }
10: }

Vemos como ahora para relanzar la excepción en la línea 22 usamos el nuevo método que hemos creado RelanzadorExcepciones.lanzar.

Si ejecutamos ahora la aplicación y vemos la traza, queda de la siguiente forma:

Exception in thread "main" java.lang.Exception: El argumento b no puede ser 0
    at ejemplo.Matematicas.dividir(Matematicas.java:21)
    at ejemplo.Main.main(Main.java:15)

¡¡Es decir que únicamente se ve la excepción checked sin que quede encapsulada por nadie.!!

Por fin con este ultimo truco hemos conseguido simplificar la traza con todas las excepciones.

Puede parecer un poco exagerado querer limpiar la traza para dejarla mas clara , pero en programas muy extensos se acumulan muchas excepciones dentro de otras excepciones y al final queda poco claro que ha ocurrido viendo la traza.

Pero si no quieres complicar el código, usa simplemente la solución anterior a ésta que queda todo más estándar.

Referencias

1) Hay una tercera llamada java.lang.Error que no trataremos aqui
2) en el log o por consola
3) o en el sistema de log
patrones/excepciones.1348601586.txt.gz · Última modificación: 2016/07/03 20:18 (editor externo)
Ir hasta arriba
CC Attribution-Noncommercial-Share Alike 3.0 Unported
chimeric.de = chi`s home Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0