Formación informática

Java | Joomla | MySQL

Curso de Java - Tema 27: tratamiento de excepciones

La excepción que confirma la regla

Antes de avanzar a un contenido que nos permita realizar ejercicios de un nivel superior, vamos a ver la teoría básica sobre el tratamiento de excepciones puesto que es una parte fundamental en cualquier lenguaje de programación. Así que todo programador debe saber como lanzarlas y capturarlas. Este artículo es una introducción para tratar excepciones que se producen en el día a día.

El tratamiento de excepciones en Java es un mecanismo del lenguaje que permite gestionar errores y situaciones excepcionales. Sin embargo, es necesario conocer con cierta profundidad el propósito por el cual tenemos disponible dicho mecanismo, además de hacer un uso correcto de esta funcionalidad para que la aplicación funcione correctamente.

Cuando se crea una aplicación Java, ésta será usada en último término por personas. Es una regla infalible. Existen situaciones que no podemos controlar cuando creamos una aplicación pero que sí que podemos evitar que produzcan errores que terminen con la ejecución de la aplicación de una forma abrupta. Por ejemplo, el caso más típico a tratar es cuando se pide la introducción de datos al usuario. En el caso de que hayamos declarado un atributo de una clase como tipo entero y el usuario introduzca una cadena de caracteres entonces Java nos devolvería un error diciendo que el campo no admite ese tipo de datos y se pararía el programa.

Existen muchos más casos dónde tenemos que evitar que la aplicación se pare: cuando se accede a un fichero, cuando se escribe, cuando se usan objetos. En el caso de usar una aplicación como NetBeans, ésta nos avisa de cuando hay que realizar el tratamiento de excepciones.

Concepto de excepción

Una excepción es un error o situación excepcional que se produce durante la ejecución de un programa. Algunos ejemplos de errores y situaciones excepcionales son:

  • Leer un fichero que no existe
  • Acceder al valor N de una colección que contiene menos de N elementos
  • Enviar/recibir información por red mientras se produce una perdida de conectividad

Todas las excepciones en Java se representan a través de objetos que heredan en última instancia de la clase java.lang.Throwable.

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

  • Separa el manejo de errores del código normal de la aplicación. Estará en una zona separada donde podremos tratar las excepciones como un código ‘especial’ en un bloque separado. Además, simplifica el código a escribir una barbaridad aunque al principio pueda paracer lo contrario.
  • Propaga los errores sobre la pila de llamadas hasta que llegamos al error. Supongamos una clase en la cuál el método leerFichero es el cuarto método en una serie de llamadas a métodos anidadas realizadas por un programa principal: metodo1 llama a metodo2, que llama a metodo3, que finalmente llama a leerFichero. Supongamos también que metodo1 es el único método interesado en el error que ocurre dentro de leerFichero. Tradicionalmente las técnicas de notificación del error forzarían a metodo2 y metodo3 a propagar el código de error devuelto por leerFichero sobre la pila de llamadas hasta que el código de error llegue finalmente a metodo1, el único método que está interesado en él.
  • Agrupa los errores y los diferencia mediante clases. Gracias a esto tenemos todos los posibles errores juntos y podemos pensar una manera de tratarlos que sea adecuado. Como todas las excepciones lanzadas dentro de los programas Java son objetos de primera clase, agrupar o categorizar las excepciones es una salida natural de las clases y las superclases. Las excepciones Java deben ser ejemplares de la clase Throwable, o de cualquier descendiente de ésta. Como de las otras clases Java, se pueden crear subclases de la clase Throwable y subclases de estas subclases. Cada clase 'hoja' (una clase sin subclases) representa un tipo específico de excepción y cada clase 'nodo' (una clase con una o más subclases) representa un grupo de excepciones relacionadas.

Formas de hacerlo

Las excepciones se pueden tratar en el lugar dónde se producen o se puede enviar al método superior lanzándole la excepción.

La más sencilla es realizarlo en el bloque dónde se produce englobando las instrucciones en un bloque try catch. Cuando sabemos que un código podría lanzar un error, como por ejemplo una división entre cero o la introducción de datos de un tipo erróneo, debemos encerrarla entre un bloque try-catch. En la parte del try se escriben las variables y/o instrucciones que pueden provocar el fallo. En la parte del catch se muestra un mensaje indicativo del error. Cuando se utiliza una aplicación como NetBeans, nos escribe por defecto la parte del catch pero nosotros podemos adecuarla de acuerdo a nuestras necesidades. La sintaxis genérica en este caso es:

try{
   instrucciones
}catch{
   mensaje_de_error
}finally{
	Instrucciones que se ejecutan tanto si se produce excepción cómo si no
}

La otra forma consiste en escribir la palabra reservada throable en el método que incluye el tratamiento de excepciones para que Java lance ese tratamiento a un nivel superior. Esto implica que en algún momento se tienen que tratar para evitar la paralización de la aplicación. Esta cláusula advierte de las excepciones que podría lanzar un método, van entre la declaración del método y su cuerpo. Muy importante, pueden ser varias.

El bloque finally se utiliza cuando el programador solicita ciertos recursos al sistema que se deben liberar, y se coloca después del último bloque catch. El código contenido en finally se ejecutará tras terminar el bloque try, haya habido o no excepción, lo que permite liberar los recursos reservados para la operación o cerrará objetos usados durante algún proceso concreto como leer o escribir en un fichero de texto.

Tipos de excepciones

El lenguaje Java diferencia claramente entre tres tipos de excepciones: errores; comprobadas, verificadas o  checked; y no comprobadas, no verificadas o unchecked. El gráfico que se muestra a continuación muestra el árbol de herencia de las excepciones en Java:

Java - Árbol de herencia del tratamiento de excepciones

La clase principal de la cual heredan todas las excepciones Java es Throwable; ésta a su vez hereda de Object. De ella nacen dos ramas: Error y Exception.

Error representa errores de una magnitud tal que una aplicación nunca debería intentar realizar nada con ellos. Por ejemplo son errores de la máquina virtual de Java, desbordamientos de buffer, etc. Este tipo de excepciones no se trata en este apartado.

La segunda rama está encabezada por Exception y representa aquellos errores que normalmente si solemos gestionar y a los que comunmente solemos llamar excepciones. Las Excepciones derivadas de Exception sí que deben ser tratadas, y en algunos casos es obligatorio hacerlo para que el programa compile. De Exception nacen múltiples ramas: ClassNotFoundException, IOException, ParseException, SQLException y otras muchas, todas ellas de tipo checked. La única excepción es RuntimeException que es de tipo unchecked y encabeza todas las de este tipo y no es necesario tratarlas: ArithmeticException, IndexOutOfBoundsException, NullPointerException o SecurityException son algunos ejemplos.

A pesar de que la diferencia entre las excepciones de tipo checked y unchecked es muy importante, es también a menudo uno de los aspectos menos entendidos dentro del tratamiento de excepciones. Veamos cada una de ellas con un poco más de detalle.

Excepciones checked

Una excepción de tipo checked representa un error del cual técnicamente podemos recuperarnos. Por ejemplo, una operación de lectura/escritura en disco puede fallar porque el fichero no exista, porque éste se encuentre bloqueado por otra aplicación, etc. Todos estas situaciones, además de ser inherentes al propósito del código que las lanza (lectura/escritura en disco) son totalmente ajenas al propio código, y deben ser declaradas y manejadas mediante excepciones de tipo checked y sus mecanismos de control.

En ciertos momentos y a pesar de la promesa de recuperabilidad, nuestro código no estará preparado para gestionar la situación de error, o simplemente no será su responsabilidad. En estos casos lo más razonable es relanzar la excepción y confiar en que un método superior en la cadena de llamadas sepa gestionarla.

Por tanto, todas las excepciones de tipo checked deben ser capturadas o relanzadas. En el primer caso, utilizamos el más que conocido bloque try-catch. Por ejemplo cuando escribimos eun fichero de texto puedo ocurrir que el fichero no exista o esté bloqueado:

import java.io.FileWriter; 
import java.io.IOException; 

public class Main { 
    public static void main(String[] args) {         
        FileWriter fichero; 
        try { 
            // Las siguientes dos líneas pueden lanzar una excepción de tipo IOException al no existir el fichero o no estar accesible
            fichero = new FileWriter("ruta"); 
            fichero.write("Esto se escribirá en el fichero"); 
        } catch (IOException ex) { 
            // Aquí capturamos cualquier excepción IOException que se lance (incluidas sus subclases) 
            System.out.println(ex.getMessage()); 
        } 
         
    } 
}

En caso de querer relanzar la excepción, debemos declarar dicha intención en la firma del método que contiene las sentencias que lanzan la excepción, y lo hacemos mediante la claúsula throws:

import java.io.FileWriter; 
import java.io.IOException; 

public class Main { 
    // En lugar de capturar una posible excepción, la relanzamos 
    public static void main(String[] args) throws IOException {         
        FileWriter fichero = new FileWriter("ruta"); 
        fichero.write("Esto se escribirá en el fichero");     
    } 
}

Hay que tener presente que cuando se relanza una excepción estamos forzando al código cliente de nuestro método a capturarla o relanzarla. Una excepción que sea relanzada una y otra vez hacia arriba terminará llegando al método primigenio y, en caso de no ser capturada por éste, producirá la finalización de su hilo de ejecución (thread). Las dos preguntas que debemos hacernos en este momento es: ¿Cuándo capturar una excepción? ¿Cuándo relanzarla? La respuesta es muy simple. Capturamos una excepción cuando tenemos que realizar algún tratamiento del propio error. Esto ocurre cuando:

  • Podemos recuperarnos del error y continuar con la ejecución.
  • Queremos registrar el error.
  • Queremos relanzar el error con un tipo de excepción distinto.

Por contra, relanzamos una excepción cuando no es competencia nuestra ningún tratamiento de ningún tipo sobre el error que se ha producido.

Excepciones unchecked

Una excepción de tipo unchecked representa un error de programación. Uno de los ejemplos más tipicos es el de intentar leer en un array de N elementos un elemento que se encuentra en una posición mayor que N. Esto se puede evitar fácilmente usando métodos del objeto para comprobar su tamaño y no leer cuando se supera éste. En este caso se produce una excepción de tipo ArrayIndexOutOfBoundsException puesto que se accede a una posición inexistente.

El aspecto más destacado de las excepciones de tipo unchecked es que no deben ser forzosamente declaradas ni capturadas. Es decir, no son comprobadas. Por ello no son necesarios bloques try-catch ni declarar formalmente en la firma del método el lanzamiento de excepciones de este tipo. Ésto, por supuesto, también afecta a métodos y/o clases más hacia arriba en la cadena invocante.

Creando nuestras propias excepciones

Aprovechando dos de las características más importantes de Java, la herencia y el polimorfismo, podemos crear nuestras propias excepciones de forma muy simple. Tanto de tipo comprobadas como no comprobadas. Al ser una característica muy avanzada aquí sólo vamos a conocer su existencia sin ver su aplicación práctica.

Malas prácticas de uso

Para convertirnos en maestros de las excepciones, debemos evitar el uso de aquellas malas practicas que se han generalizado a los largo de los años. Y por supuesto no inventar las nuestras...

Ignorar la excepción

La primera que vamos a ver es la más peligrosa y, a pesar de ello, también la más común: ignorar cualquier excepción que se lance dentro del bloque try. O siendo más conciso, capturar toda excepción lanzada dentro del bloque try pero silenciarla sin hacer nada. De esta forma, obviamos el principal propósito de la gestión de excepciones checked: gestionarla o relanzarla. Cualquier error de diseño, de programación o de funcionamiento en estas líneas de código pasará inadvertido tanto para el programador como para el usuario:

 try { 
    // Código que declara lanzar excepciónes 
     } catch(Exception ex) {
	}

Lo mínimamente aceptable dentro de un bloque catch es un mensaje de log informando del error que se ha producido lo cuál se consigue mediante el siguiente código. También se puede mostrar el erro producido sólamente.

try { 
    // Código que declara lanzar excepciónes 
} catch(Exception ex) { 
    logging.log("Se ha producido el siguiente error: " + ex.getMessage()); 
    logging.log("Se continua la ejecución"); 
}

Algo más razonable sería pintar una traza completa del error mediante uno de los métodos informativos de Throwable:

try { 
    // Código que declara lanzar excepciónes 
} catch(Excepcion ex) { 
    ex.printStackTrace();    // Podemos añadir cualquier tratamiento adicional antes y/o después de esta línea 
}

Mejorar el rendimiento

Otro abuso del mecanismo de tratamiento de excepciones es cuando se está intentando escribir código que mejore el rendimiento de la aplicación. Un ejemplo típico es cuando iteramos sobre un array de números primos sin preocuparnos de los límites del mismo, tal y como se haría de manera formal con un bucle for, hasta sobrepasar el índice máximo, momento en el cual se lanzará una excepción de tipo ArrayIndexOutOfBoundsException que será capturada y silenciada. Esto es un error porque:

  • El tratamiento de excepciones está diseñado para gestionar excepciones y no para realizar optimizaciones.
  • El código dentro de bloques try-catch no dispone de ciertas optimizaciones de las JVM más modernas (por ejemplo, y aplicable a nuestro caso, iteración de colecciones).
  • El resultado es estéticamente horrible.

Lanzar excepciones genéricas

La siguiente mala práctica que vamos a ver está intimamente relacionada con la anterior, y es la de lanzar excepciones de forma genérica. Es un absoluto ERROR. Los clientes de tu método no sabrán jamás con que condiciones especiales se pueden encontrar y, lo que es más importante, no podrán gestionarlas; no tendrán más remedio que informar del error y detener la ejecución.

Convertir excepción check en uncheck

Por último existe una técnica para convertir toda excepción checked en unchecked:

public void noLanzoExcepcionesChecked() { 
    try { 
        // Código que lanza una o más excepciones de tipo checked 
    } catch(Exception ex) { 
        throw new RuntimeException("Se ha producido una excepción con el mensaje: " + ex.getMessage(), ex); 
    } 
}	

El método del código anterior convierte cualquier excepción de tipo checked en una excepción de tipo unchecked, de manera que ningún cliente suyo esté forzado a declarar/gestionar ninguna de ellas. El tratamiento de excepciones de tipo checked nos otorga control sobre los errores que se producen a través de estructuras basadas en código legible y con un proposito claro. La razón es que no sólo estamos aniquilando toda posibilidad de un tratamiendo de excepciones que sea razonablemente útil, sino que lo más probable es que nuestra nueva y flamante excepción uncheked vaya subiendo hacia arriba en la cadena de llamadas y termine deteniendo el hilo de ejecución actual. Mi consejo final es: ¡NO CONVIERTAS EXCEPCIONES CHECKED EN UNCHECKED! salvo que realmente sepas lo que haces.

Recomendaciones de uso

Hay algunos principios de uso que debemos ver desde la perspectiva del haz esto en lugar del no hagas esto.

Un buen uso del tratamiento de excepciones es usar excepciones que ya existen, en lugar de crear las tuyas propias, siempre que ambas fueran a cumplir el mismo cometido. Se suelen usar excepciones que ya existen cuando se dispone de un profundo conociento del API que se está usando, lo cual implica cierta experiencia con Java. Esto es bueno porque:

  • Uno de los pilares de Java es la reutilización de código.
  • Tu código es más universal al usar lenguaje Java puro.

Otra recomendación que no suele llevarse a cabo nunca o casi nunca es la de lanzar excepciónes acordes al nivel de abstracción en el que nos encontramos. Imaginemos una seríe de clases que actuan como capas, una encima de otra (cuanto más arriba más abstracta, cuanto más abajo más concreta). Cuando se produce un error en las capas más bajas y éste se propaga hacia arriba, llega un momento en que dicho error representando una condición excepcional muy concreta se encuentra en un contexto muy abstracto. Esto tiene básicamente tres problemas:

  1. Estamos contaminando el API de las capas superiores con suciedad de las inferiores.
  2. Estamos desvelando detalles de nuestra implementación muchos niveles por encima de lo deseable.
  3. Este puede ser crítico, es que si en el futuro deseamos intercambiar una de las capas más concretas y ésta ha cambiado su implementación, todas las capas por encima se romperán.

Por último debes documentar adecuadamente las excepciones que lanza tu código. Para ello, detalla en tus Javadoc todas las excepciones que lanzan tus métodos, informando que condiciones van a provocar el lanzamiento de cada una de ellas.

 

Curso de Java - Tema 26.5: HashMap | Curso de Java - Tema 28: creación y uso de menús por línea de comandos
Curso de Java - Índice Ejercicios Nivel Medio

Escribir un comentario

Aunque los comentarios no expresan la opinión del administrador del sitio web, éste si que tiene una responsabilidad legal sobre lo que aparece. Por lo tanto, habrá una labor de moderación de los mensajes. No se permitirán mensajes ofensivos ni publicidad


Código de seguridad
Refescar

Solicitamos su permiso para obtener datos estadísticos de su navegación en esta web, en cumplimiento del Real Decreto-Ley 13/2012, de 30 de marzo. Si continúa navegando consideramos que acepta el uso de cookies. . Más información