Este documento explica el manejo de excepciones en Java. Detalla que una excepción ocurre cuando hay un error en la secuencia de instrucciones de un programa. Cubre cómo capturar excepciones usando bloques try-catch y la cláusula finally. También describe cómo lanzar y propagar excepciones personalizadas y el uso de aserciones para validar suposiciones en el código.
Objetivos Entender el uso de control de Excepciones en un programa. Conocer el mecanismo de captura y tratamiento de excepciones en Java. Conocer el mecanismo para la propagación de una excepción capturada o de una nueva excepción. Crear Excepciones personalizadas. Entender el uso de aserciones para la identificación de errores de lógica de una aplicación.
Excepciones Una Excepción es una condición anormal que surgen en una secuencia de código durante su ejecución. Una Excepción es un Objeto que describe una condición excepcional producida en un fragmento de código. Esta Excepción se detecta en tiempo de ejecución y no durante la compilación del código. En Java, cuando se dispara una excepción, ocurre lo siguiente: 1) se crea un objeto excepción en la heap, con el operador new, como cualquier objeto Java, 2) luego, se interrumpe la ejecución y el objeto excepción es expulsado del contexto actual. En este punto, comienza a funcionar el mecanismo de manejo de errores: buscar un lugar apropiado donde continuar la ejecución del programa; el lugar apropiado es el manejador de excepciones, cuya función es recuperar el problema. 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 a su vez debería capturar ( catch ) y resolver la situación de error, o poder ser tratada finalmente ( finally ) por un gestor por defecto u omisión. Utilizadas en forma adecuada, las excepciones aumentan en gran medida la robustez de las aplicaciones.
Un programa sin control de Excepciones El siguiente bloque intenta recuperar el elemento de índice 5 del array alumnos , al no existir dicha referencia se lanza la Excepción ArrayIndexOutOfBoundsException, al no contemplar ninguna acción para este caso el programa detiene su ejecución.
Un programa que controla Excepciones Este bloque de código también desea acceder al elemento de índice 5 que no tiene ninguna referencia, pero a diferencia del anterior, este suceso a sido previsto y se muestra un mensaje al usuario indicando que no hay elementos en el arreglo y la ejecución del programa termina de forma natural.
Excepciones. Jerarquía base Cuando se produce una condición excepcional en el transcurso de la ejecución de un programa, se debería generar, o lanzar, una excepción. Esta excepción es un objeto derivado directa, o indirectamente, de la clase Throwable . Tanto el intérprete Java como muchos métodos de las múltiples clases de Java pueden lanzar excepciones y errores. La clase Throwable tiene dos subclases: Error y Exception . Un Error indica que se ha producido un fallo no recuperable, del que no se puede recuperar la ejecución normal del programa, por lo tanto, en este caso no hay nada que hacer. Los errores, normalmente, hacen que el intérprete Java presente un mensaje en el dispositivo estándar de salida y concluya la ejecución del programa. El único caso en que esto no es así, es cuando se produce la muerte de un thread, en cuyo caso se genera el error ThreadDead , que lo que hace es concluir la ejecución de ese hilo, pero ni presenta mensajes en pantalla ni afecto a otros hilos que se estén ejecutando. Una Exception indicará una condición anormal que puede ser subsanada para evitar la terminación de la ejecución del programa. Hay nueve subclases de la clase Exception ya predefinidas, y cada una de ellas, a su vez, tiene numerosas subclases. Throwable Clase base que representa todo lo que se puede “lanzar” en Java. Contiene una instantánea del estado de la pila en el momento en el que se creó el objeto (“stack trace” o “call chain”). Almacena un mensaje (variable de instancia de tipo String) que podemos utilizar para detallar que error se produjo. Puede tener una causa, también de tipo Trowable , que permite representar el error que causó este error.
Clasificación de Excepciones En Java las excepciones se clasifican en: Verificadas en Compilación (checked exception): son errores que el compilador verifica y que pueden recuperarse. Java requiere que los métodos que disparan excepciones, las capturen y manejen el error o, especifiquen todas las excepciones checked que pueden producirse dentro de su alcance. Por ej.: al intentar abrir un archivo en el file system, podría dispararse una excepción, ya que el archivo puede no existir, en ese caso una solución posible es pedirle al usuario que ingrese un nuevo nombre; otro ej. es el envió de una sentencia sql errónea a la base de datos. No-Verificadas en Compilación (unchecked exception) : representan errores de programación difíciles de preveer. Son excepciones disparadas automáticamente por el sistema de ejecución de JAVA. Por ejemplo, las excepciones aritméticas (división por cero), excepciones de referencias nulas (acceso a un objeto mediante un puntero nulo), excepciones de indexación (acceso a un elemento de un arreglo con un índice muy chico ó demasiado grande) y error de casting. JAVA no obliga que éstas excepciones sean especificadas, ni capturadas para su manejo.
Excepciones no verificadas en compilación Toda Excepción que herede de RuntimeException será una excepción no verifica en compilación, ya que depende de la ejecución del programa para que pueda lanzarse. Una excepción no verificada en compilación no necesita estar dentro de un bloque marcado para su lanzamiento.
Excepciones verificadas en compilación Toda Excepción que sea subclase de Exception (excepto RuntimeException) es verificada en tiempo de compilación. Exception y sus subclase indican situaciones que una aplicación debería tratar de forma razonable.
Captura de excepciones: Bloque try…catch Se utilizan en Java para capturar las excepciones que se han podido producir en el bloque de código delimitado por try y catch . En cuanto se produce la excepción, la ejecución del bloque try, que la contiene, termina. La cláusula catch recibe como argumento un objeto Throwable. try Es el bloque de código donde se prevé que se genere una excepción. Es como si dijésemos "intenta estas sentencias y mira a ver si se produce una excepción". El bloque try tiene que ir seguido, al menos, por una cláusula catch o una cláusula finally. La sintaxis general del bloque try consiste en la palabra clave try y una o más sentencias entre llaves. catch Es el código que se ejecuta cuando se produce la excepción. Es como si dijésemos " controlo cualquier excepción que coincida con mi argumento ". No hay código alguno entre un bloque try y un bloque catch , ni entre bloques catch .
Captura selectiva de Excepciones Es posible manejar con la clase Exception todas las excepciones lanzadas desde cualquier punto, ya que todas las excepciones derivan de Exception, aunque el tratamiento sería igual para cada una de ellas, sin poder tener un tratamiento especial en cada caso. Por otro lado, uno puede capturar Excepciones específicas indicándolas en el bloque catch; además, pueden haber tantos bloques catch como errores predecibles dentro del bloque try. ¡Ojo! Las cláusulas check se comprueban en orden, esto quiere decir, si existe una excepción más que es superclase de otra declarada en la parte inferior, la segunda nunca será ejecutada. Se produce el error de compilación por que Exception es superclase de A rithmeticException, por tanto se considera como verificada.
La cláusula finally En ocasiones, nos interesa ejecutar un fragmento de código independiente de si se produce o no un excepción (por ejemplo, cerrar un fichero que estemos manipulando o una conexión a una fuente de datos). Si el cuerpo del bloque try llega a comenzar su ejecución, el bloque finally siempre se ejecutará… Detrás del bloque try si no se producen excepciones. Después de un bloque catch si éste captura la excepción. Justo después de que se produzca la excepción si ninguna cláusula catch captura la excepción y antes de que la excepción se propague hacia arriba.
Lanzamiento de Excepciones. La sentencia throw La sentencia throw se utiliza para lanzar explícitamente una excepción. En primer lugar se debe obtener un descriptor de un objeto Throwable , bien mediante un parámetro en una cláusula catch o, se puede crear utilizando el operador new. La forma general de la sentencia throw es: throw ObjetoThrowable; El flujo de la ejecución se detiene inmediatamente después de la sentencia throw, y nunca se llega a la sentencia siguiente. Se inspecciona el bloque try que la engloba más cercano, para ver si tiene la cláusula catch cuyo tipo coincide con el del objeto o instancia Thorwable . Si se encuentra, el control se transfiere a ese sentencia. Si no, se inspecciona el siguiente bloque try que la engloba, y así sucesivamente, hasta que el gestor de excepciones más externo detiene el programa y saca por pantalla el trazado de lo que hay en la pila hasta que se alcanzó la sentencia throw.
Propagación de Excepciones: La sentencia throws Si un método es capaz de provocar una excepción que no maneja él mismo, debería especificar este comportamiento, para que todos los métodos que lo llamen puedan colocar protecciones frente a esa excepción. La palabra clave throws se utiliza para identificar la lista posible de excepciones que un método puede lanzar. Para la mayoría de las subclase de la clase Exception , el compilador Java obliga a declarar qué tipos podrá lanzar un método. Si el tipo de excepción es Error o RuntimeException, o cualquiera de sus subclases, no se aplica esta regla, dado que no se espera que se produzcan como resultado del funcionamiento normal del programa. Si un método lanza explícitamente una instancia de Exception o de sus subclases, a excepción de la excepción de runtime , se debe declarar su tipo con la sentencia throws. La declaración del método sigue ahora la sintaxis siguiente: type NombreMetodo( argumentos ) throws excepcionX, excepcionY,… { }
Si en el cuerpo de un método se lanza una excepción de un tipo derivado de la clase Exception, en la cabecera del método hay que añadir una cláusula throws que incluye una lista de los tipos de excepciones que se pueden producir al invocar el método. A continuación se muestra el procesamiento de una excepción:
Excepciones Personalizadas Es una forma de representar las situaciones inesperadas que infringen alguna regla del negocio. Toda excepción personalizada que sea subclase de Exception, pero no de RuntimeException, debe ser declarada en la cabecera del método que pueda lanzarla. Al respecto de cuándo crear excepciones propias y no utilizar las múltiples que ya proporciona Java. Como guía, se pueden plantear las siguientes cuestiones, y si la respuesta es afirmativa, lo más adecuado será implementar una clase Exception nueva y, en caso contrario, utilizar una del sistema. ¿Se necesita un tipo de excepción no representado en las que proporciona el entorno de desarrollo Java? ¿Ayudaría a los usuarios si pudiesen diferenciar las excepciones propias de las que lanzan las clases de otros desarrolladores? ¿Si se lanzan las excepciones propias, los usuarios tendrán acceso a esas excepciones? ¿El package propio debe ser independiente y auto-contenido?
Aserciones Un programa se puede reducir conceptualmente a la evolución del valor de una serie de variables desde un estado inicial hasta un resultado final. A lo largo del proceso, las diferentes variables van tomando valores concretos que llevan al resultado deseado por el programador. Cuando todo funciona según lo esperado, poco más hay que hablar. Pero cuando una variable toma un valor inesperado (erróneo en definitiva) las consecuencias se pueden apreciar inmediatamente o puede que el programa arrastre el error durante un tiempo antes que se aprecie un fallo de ejecución o entregue un resultado final equivocado. Informalmente, podemos decir que cada vez que un programador mientras escribe código se siente tentado a dejar una traza impresa de los valores de las variables en cierto punto, ese programador lo que necesita es una aserción que, también informalmente, podríamos definir como una traza condicional. Esta descripción informal nos define el espíritu de una aserción: if (! (estamos_como_queremos) ) throw new Error("fallo en tal zona del programa"); que en Java se escribe de forma compacta: assert estamos_como_queremos;
Las aserciones suponen un trabajo extra para el programa, trabajo que se compensa durante el desarrollo y las pruebas; pero que parece redundante cuando al programa ha sido exhaustivamente probado y simplemente queremos que produzca resultados a la mayor velocidad posible. En otras palabras, queremos eliminar las aserciones cuando entremos en producción. No obstante, si teniendo un programa en producción se detecta una situación de fallo, conviene reactivar las aserciones para poder afinar rápidamente dónde está el origen de la avería. En resumen, que las aserciones deberían poder activarse y desactivarse con comodidad para afrontar con el mismo código las situaciones de depuración de errores y de producción.
Diseño por contrato Diseñar por contrato es una metodología de programacióm relativamente difundida y que muchos programadores usan incluso sin denominarle de ninguna forma especial. La idea es simple: definamos los términos de lo que vamos a recibir en la entrada y de lo que vamos a entregar como resultado. Esos términos constituyen el "contrato entre las partes" y a él nos atendremos en caso de errores para determinar responsabilidades. El fallo de una precondición quiere decir que el usuario de nuestro código no está pidiendo según lo previsto. Si, por ejemplo, desarrollamos un programa para calcular la raíz cuadrada de un número, exigiremos como precondición que el número sea positivo. El fallo de una postcondición quiere decir que no hemos cumplido con nuestro compromiso. Si, por ejemplo, desarrollamos un programa para calcular la raíz cuadrada de un número con una cierta precisión, se nos exigirá a la salida que el resultado multiplicado por sí mismo difiera de la entrada en menos que la precisión prometida. El fallo de un invariante de bucle significa que el algoritmo se ha perdido. Tomemos como ejemplo el caso de estar buscando una palabra en un diccionario por el método binario. El algoritmo consiste en abrir el diccionario por la mitad y determinar en qué mitad está la palabra, y así recurrentemente con la mitad de la mitad de la mitad, etc. En todo momento debe ser cierto que la palabra que buscamos esté en la fracción de diccionario que estamos buscando; es decir, que sea posterior a la primera palabra de la fracción, y anterior a la última.
Errores que pueden ocurrir y errores que no Un programa siempre debe enfrentarse a la posibilidad de que se le suministren datos erróneos de entrada, situación que debe chequear y a la que debe reaccionar rechazando los datos. El chequeo de datos de entrada no es una aserción ; escribir código para tratar la situación anómala es una obligación del programador. Las aserciones se colocan cuando al ni debe ni puede ocurrir (teóricamente, claro). Cuando la teoría dice que un programa es correcto; pero la práctica lo desmiente, es cuando las aserciones se disparan. Las aserciones chequean condiciones que no pueden darse ... teóricamente.
Aserciones en Java En su forma simple basta con indicar una sentencia assert con una condición, aunque se puede escribir un mensaje para describir el error. ¿Qué forma es mejor? Pues el programador deberá elegir. No se trata de sacar un mensaje de error bonito, pues no olvidemos que (teóricamente) esto nunca lo van a ver los usuarios. Para saber qué aserción falla en un programa con miles, Java nos dirá en qué línea está la aserción violada. Lo único que suele ser conveniente es usar el mensaje para indicar el valor de alguna variable que nos de pistas de qué puede haber ido mal. Hemos indicado anteriormente razones por las que deberíamos poder activar y desactivar aserciones para diferenciar depuración de errores frente a producción. Java lo que hace es no chequear las aserciones salvo que le indiquemos explícitamente que queremos que lo haga. java -ea miPrograma activa las aserciones de mi programa java -da miPrograma las desactiva java -ea:class miPrograma activa las aserciones de mi programa; pero sólo en la clase class java -da:class miPrograma desactiva las aserciones de la clase class java -ea:package... miPrograma activa las aserciones de mi programa; pero sólo en el paquete package java -da:package... miPrograma desactiva las aserciones del paquete package
Se pueden incluir varias activaciones y desactivaciones, una tras otra, que se ejecutan en el orden en que aparecen, permitiendo un control exacto de qué aserciones se activan y cuales no. No obstante, lo normal es activarlas todas en cuanto entramos a depurar errores. El argumento de una aserción puede ser una simple expresión booleana, o puede ser un método completo que ejecute un chequeo de datos. Si las aserciones están activadas, el método se ejecuta y se lanza una excepción caso de que devuelva FALSE. Si las aserciones no estuvieran activadas, el método no se ejecuta. Conviene no olvidar esta regla: El código que se escribe en una expresión assert NO debe ser necesario para la correcta ejecución del programa: NO debe modificar el valor de variable alguna.
Criterios Son las aserciones un arma poderosa para detectar errores antes de perder el control del programa; pero hay que usarla con prudencia y sabiduría.
Práctica 8: Crear Excepciones Personalizadas Objetivo Definir excepciones personalizadas que puedan ser propagadas y capturadas dentro de una aplicación. Ejercicios Crear una clase con dos métodos f( ) y g( ). En g( ), lanza una excepción de un nuevo tipo que tu definas MiException . En f( ), se llama a g( ), captura su excepción en la cláusula catch, lanza una excepción diferente ( de un segundo tipo que definas OtraException ). Testea el código con un main que invoque a f(). 2. Dada la abstración tanque de agua de la Ilustración, se pide: a) Determinar los invariantes que debe cumplir la clase. b) Determinar las precondiciones asociadas a las operaciones de añadirAgua y quitarAgua. c) Definir una clase Tanque en Java que represente esta abstracción cumpliendo las precondiciones e invariantes definidos. Hay que utilizar al menos tres nuevos tipos de excepciones. Considerar el caso de Gestión Académica del módulo 5, identificar e implementar los posibles errores que se podría cometer en la ejecución del programa y crear las excepciones que podrían controlar estas situaciones. Ej. DiaErroneoException, puede controlar el ingreso de un valor incorrecto en la asignación de horarios. Considerar el caso de la empresa XYZ, si es que se añadiera un atributo stockActual a la clase Producto, identifique e implemente la excepción que podría ocurrir al ingresar un nuevo Item a un Pedido.