Herramientas de usuario

Herramientas del sitio


patrones:excepciones

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)

CheckedUncheckedExceptionRuntimeException

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 {
    public double dividir(double a, double b) throws Exception {
        if (b == 0) {
            throw new Exception("El argumento b no puede ser 0");
        }
 
        return a / b;
    }
}

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 {
        public static void main(String[] args) {
            Matematicas matematicas=new Matematicas();
            try {
                double c=matematicas.dividir(-1.6, 0);
            } catch (Exception ex) {
                //Tratar la excepción
            }
        }    
    }

    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 {
        public static void main(String[] args) throws Exception {
            Matematicas matematicas=new Matematicas();
     
            double c=matematicas.dividir(-1.6, 0);
        }    
    }

    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 {
    public double dividir(double a, double b) {
        if (b == 0) {
            throw new RuntimeException("El argumento b no puede ser 0");
        }
 
        return a / b;
    }
}

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 {
    public static void main(String[] args) {
        Matematicas matematicas=new Matematicas();
 
        double c=matematicas.dividir(-1.6, 0);
    }    
}

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.
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 en 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 {
    double c=matematicas.dividir(-1.6, 0);
} catch (Exception ex) {
    throw new RuntimeException("Fallo al llamar a 'dividir'",ex)
}

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 {
    double c=matematicas.dividir(-1.6, 0);
} catch (Exception ex) {
    throw new RuntimeException("Fallo al llamar a 'dividir'",ex)
}

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.

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 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 {
    double c=matematicas.dividir(-1.6, 0);
} catch (Exception ex) {
    throw new RuntimeException("Falló al llamar a 'dividir':" + ex.getMessage());
}

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 generica 2) que no aporta nada respecto a RuntimeException 3) y usarla en vez de RuntimeException

1
public class MiExcepcion extends RuntimeException { 
  public MiExcepcion (String msg) {
      super(msg);
  }
}

Creando una excepción personalizada que no aporta nada.

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

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 siguiente 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 {
    public static void main(String[] args) throws Exception {
        Matematicas matematicas=new Matematicas();
 
        double c=matematicas.dividir(-1.6, 0);
    }    
}

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 {
    double c=matematicas.dividir(-1.6, 0);
} catch (Exception ex) {
 
}

Vemos en la línea 4 como no se realiza ninguna acción al capturar la excepción, por lo tanto la ejecución seguirá como si no hubiera pasado nada.

Log

Acabamos de ver en el apartado anterior que nunca 4) se debe ignorar la excepción. Una forma de ignorarla pero que aparenta ser mejor solución es guardar en el log la excepción que se ha producido pero sin hacer nada mas.

1
try {
    double c=matematicas.dividir(-1.6, 0);
} catch (RuntimeException ex) {
    Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
}

En la línea 4 guardamos en el log la excepción pero el programa sigue ejecutándose. Como ya hemos comentado no debería seguir la ejecución del programa puesto que los datos pueden haber quedado en un estado erróneo.

La idea de guardar en el log la excepción es buena por si misma. Sin embargo no suele ser necesario guardarla explícitamente ya que en muchas ocasiones ya se guarda automáticamente. Por ejemplo en aplicaciones Web , el propio contenedor al detectar 5) una excepción guardará en el log la excepción que se ha producido.

Consola

Otra forma erronea de guardar las excepciones consiste en mostrar la traza del error por la consola.

1
try {
    double c=matematicas.dividir(-1.6, 0);
} catch (RuntimeException ex) {
    ex.printStackTrace();
}

En la línea 4 mostramos por la consola la traza de la excepción pero el programa sigue ejecutándose. Como ya se ha dicho no debería seguir la ejecución del programa puesto que los datos pueden haber quedado en un estado erróneo.

En aplicaciones Web o aplicaciones de Ventanas , esta opción tiene menos sentido aun que la opción del log, ya que guardando la traza en un fichero de log está mucho más accesible para poder averiguar el origen del problema que mostrándolo por consola ya que la consola la puede cerrar el usuario y perder toda la traza o simplemente que no tengamos acceso a la consola.

Imprimirla

Por último pero no por ello menos usada está la técnica de imprimir el mensaje de la excepción.

1
try {
    double c=matematicas.dividir(-1.6, 0);
} catch (RuntimeException ex) {
    System.out.println("Falló al dividir");
}

Esta forma tiene los mismos problemas del anterior de imprimirla por consola pero ésta es aun peor, hemos perdido toda la información de la excepción.

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 {
    double c=matematicas.dividir(-1.6, 0);
} catch (Exception ex) {
    throw new RuntimeException("Fallo al llamar a 'dividir'",ex)
}

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

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

Exception in thread "main" java.lang.RuntimeException: Fallo al llamar a 'dividir'
	at ejemplo.Main.main(Main.java:17)
Caused by: java.lang.Exception: El argumento b no puede ser 0
	at ejemplo.Matematicas.dividir(Matematicas.java:14)
	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 {
    public double dividir(double a, double b) throws Exception {
        if (a<0) {
            throw new RuntimeException("El argumento a no puede ser negativo");
        }        
        if (b<0) {
            throw new RuntimeException("El argumento b no puede ser negativo");
        }        
 
        if (b == 0) {
            throw new Exception("El argumento b no puede ser 0");
        }
 
        return a / b;
    }
}

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:

Exception in thread "main" java.lang.RuntimeException: Fallo al llamar a 'dividir'
	at ejemplo.Main.main(Main.java:17)
Caused by: java.lang.RuntimeException: El argumento a no puede ser negativo
	at ejemplo.Matematicas.dividir(Matematicas.java:14)
	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 {
    public static void main(String[] args) {
        Matematicas matematicas=new Matematicas();
        try {
            double c=matematicas.dividir(-1.6, 0);
        } catch (RuntimeException ex) {
            throw ex;            
        } catch (Exception ex) {
            throw new RuntimeException("Fallo al llamar a 'dividir'",ex);
        }
    }    
}

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:

Exception in thread "main" java.lang.RuntimeException: El argumento a no puede ser negativo
	at ejemplo.Matematicas.dividir(Matematicas.java:14)
	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.

Por los ejemplos puede parecer un poco exagerado querer que no se encapsulen las excepciones pero pongo aquí un ejemplo de excepciones en Hibernate:
Exception in thread "main" org.hibernate.MappingException: Could not get constructor for org.hibernate.persister.entity.SingleTableEntityPersister
	at org.hibernate.persister.internal.PersisterFactoryImpl.create(PersisterFactoryImpl.java:185)
	at org.hibernate.persister.internal.PersisterFactoryImpl.createEntityPersister(PersisterFactoryImpl.java:135)
	at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:367)
	at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1740)
	at curshibernatet03e01.CursHibernateT03E01.main(CursHibernateT03E01.java:23)
Caused by: org.hibernate.HibernateException: Unable to instantiate default tuplizer [org.hibernate.tuple.entity.PojoEntityTuplizer]
	at org.hibernate.tuple.entity.EntityTuplizerFactory.constructTuplizer(EntityTuplizerFactory.java:138)
	at org.hibernate.tuple.entity.EntityTuplizerFactory.constructDefaultTuplizer(EntityTuplizerFactory.java:188)
	at org.hibernate.tuple.entity.EntityMetamodel.<init>(EntityMetamodel.java:341)
	at org.hibernate.persister.entity.AbstractEntityPersister.<init>(AbstractEntityPersister.java:502)
	at org.hibernate.persister.entity.SingleTableEntityPersister.<init>(SingleTableEntityPersister.java:144)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:525)
	at org.hibernate.persister.internal.PersisterFactoryImpl.create(PersisterFactoryImpl.java:163)
	... 4 more
Caused by: java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:525)
	at org.hibernate.tuple.entity.EntityTuplizerFactory.constructTuplizer(EntityTuplizerFactory.java:135)
	... 13 more
Caused by: org.hibernate.PropertyNotFoundException: Could not find a getter for domicilio in class curshibernatet03e01.Cliente
	at org.hibernate.property.BasicPropertyAccessor.createGetter(BasicPropertyAccessor.java:316)
	at org.hibernate.property.BasicPropertyAccessor.getGetter(BasicPropertyAccessor.java:310)
	at org.hibernate.mapping.Property.getGetter(Property.java:320)
	at org.hibernate.tuple.entity.PojoEntityTuplizer.buildPropertyGetter(PojoEntityTuplizer.java:436)
	at org.hibernate.tuple.entity.AbstractEntityTuplizer.<init>(AbstractEntityTuplizer.java:200)
	at org.hibernate.tuple.entity.PojoEntityTuplizer.<init>(PojoEntityTuplizer.java:82)
	... 18 more

Realmente la información mas útil la encontramos justamente en la primera excepción que se produce. Sin embargo está justamente al final de toda la traza:

PropertyNotFoundException: Could not find a getter for domicilio in class curshibernatet03e01.Cliente

Así que si no se hubiera encapsulado la excepción se vería en la primera línea en vez de ir a buscarla al final de la traza.

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 {
 
    public static RuntimeException lanzar( Exception ex){
        RelanzadorExcepciones.<RuntimeException>lanzarComoUnchecked(ex);
 
        throw new AssertionError("Esta línea  nunca se ejecutará pero Java no lo sabe");
    }
 
    private static <T extends Exception> void lanzarComoUnchecked(Exception toThrow) throws T{
        throw (T) toThrow;
    }
}

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

1
public class Main {
    public static void main(String[] args) {
        Matematicas matematicas=new Matematicas();
        try {
            double c=matematicas.dividir(3, 0);           
        } catch (Exception ex) {
            RelanzadorExcepciones.lanzar(ex);
        }
    }    
}

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.

Referencias

1)
Hay realmente mas como java.lang.Error pero que no trataremos aqui
2)
No me refiero a excepciones concretas que tienen significado específico como NullPointerException
3)
Lo mismo se aplica a Exception
4)
Siempre hay excepciones para todo
5)
Siempre que no la detengamos antes
6)
o en el sistema de log
patrones/excepciones.txt · Última modificación: 2023/04/07 21:26 por 127.0.0.1