lunes
sep262011
TEMA 5: CONTROL DE FLUJO
TEMA 5: CONTROL DE FLUJO
1. IF
La palabra reservada If en Java permite realizar una comprobación sobre una condición booleana (verdadero / falso). La palabra reservada else va en otra dirección, de tal manera que si la condición es falsa, se ejecutará este trozo de código. Un ejemplo sencillo es el siguiente:
Hemos visto el ejemplo anterior, donde podemos concatenar tantos if y else como queramos. Podemos comprobar que if va sobre una condición lógica. Esto nos permite hacer lo siguiente, en lugar de realizar una sentencia if con muchos else anidados, podemos realizar la comprobación sobre la sentencia switch. Ahora bien, tenemos que tener en cuenta que en este caso, no podemos hacer comprobaciones de mayor, menor, etcétera, sólo tenemos la posibilidad de realizar comprobaciones exactas. Veamos el siguiente ejemplo:
Los loops o bucles de Java tienen como finalidad realizar un bloque de sentencias mientras dure una condición. Por ejemplo, podemos querer recorrer un conjunto de datos array, para imprimir sus posiciones, de la siguiente forma:
Las excepciones en Java nos permite controlar salidas no esperadas de un programa. Básicamente, la estructura de control de excepciones es la siguiente:
5.1. Introducción
Una aserción es una condición lógica insertada en nuestro código fuente. La idea es expresar condiciones que asumimos que son ciertas. El sistema se encargará de comprobarlas y avisar mediante una excepción en caso de que no se cumplan. Por ejemplo, supongamos que hacemos una búsqueda en un vector, con garantía de éxito, de algún valor mayor que 1000:
Hay dos formas de escribir las aserciones: assert expresion; y assert expresion1 : expresion2; En el primer caso, el significado es el que ya se ha descrito: si el resultado de evaluar expresion es true la ejecución continúa normalmente y si es false, se lanza una excepción de tipo AssertionError. En el segundo caso, primero se evalúa expresion1. Si el resultado es true, la ejecución continúa. Si el resultado es false, entonces se evalúa expresion2 y el resultado se pasa como argumento al constructor del AssertionError lanzado. Así, el primer caso es un caso particular del segundo. En caso de que la expresión se evalúe a falso, se utiliza el constructor sin parámetros de AssertionError. 5.3. Usos de las aserciones
5.3.1. Precondiciones
Una precondición es una condición lógica que impone restricciones a la ejecución de un determinado fragmento de código; típicamente, un método. Por ejemplo, nos puede interesar diseñar un método para calcular raíces cuadradas de números estrictamente positivos. Formalmente, estableceríamos la siguiente condición: { x > 0 } siendo x el argumento del método. Podemos imponer esta restricción a los usuarios de ese método, de dos maneras complementarias: Por contrato: En la documentación del método, se establece un contrato entre el implementador y el usuario del método. En el ejemplo anterior, el resultado sólo está definido para números estrictamente positivos. El usuario que viole este contrato no deberá esperar el correcto funcionamiento del método. Sin embargo, a menudo es necesario blindar incluso los métodos que están protegidos por un contrato, para garantizar la solidez y robustez del método concreto y de la aplicación a nivel global. Esto se puede conseguir escribiendo las asserciones adecuadas, que validen los datos de entrada, de acuerdo a las restricciones del contrato. Por código: Lo primero que hay que hacer en el método es comprobar que el argumento es válido. En el ejemplo anterior, serviría algo como: if (x <= 0) throw new IllegalArgumentException(); Esta sentencia se puede sustituir por assert (x > 0); La diferencia entre una y otra y el porqué de usar una u otra se verá en un apartado posterior. 5.3.2. Postcondiciones
Una postcondición es una condición lógica que impone restricciones al estado alcanzado después de la ejecución de un fragmento de código (típicamente, un método). Por ejemplo, en un método para ordenar los elementos de un vector, al final del método se podría comprobar que todos los elementos están efectivamente ordenados:
Las aserciones permiten comprobar en tiempo de ejecución que el flujo del programa es correcto. Por ejemplo, dado el siguiente código:
Por defecto, las aserciones están deshabilitadas. Para habilitarlas, hay varias alternativas: Habilitar las aserciones en todas las clases de todos los paquetes: java -enableassertions Habilitar las aserciones en el paquete com.javahispano y sus subpaquetes: java -enableassertions:com.javahispano Habilitar las aserciones en todas las clases que no pertenecen a ningún paquete, que estén en el directorio actual: java -enableassertions Habilitar las aserciones en la clase com.javahispano.Util: java -enableassertions:com.javahispano.Util Además, existe un parámetro -disableassertions, para deshabilitar explicitamente las aserciones, que se utiliza de forma similar. Es posible combinar ambos parámetros. En este caso, el orden de los mismos es importante. Por ejemplo: java -disableassertions:com.javahispano... -enableassertions:com.javahispano.Util deshabilita las aserciones en todas las clases del paquete com.javahispano y de sus subpaquetes, excepto las de la clase com.javahispano.Util. (Existen dos parámetros más, -enablesystemassertions y -disablesystemassertions para controlar el uso de las aserciones en el código de las clases del sistema. Su uso se describe en la documentación oficial de la versión 1.4 del SDK). Por otra parte, es posible habilitar y deshabilitar por programa, el uso de las aserciones. Para ello, en la clasejava.lang.ClassLoader se definen varios métodos (setDefaultAssertionStatus,setPackageAssertionStatus, setClassAssertionStatus y clearAssertionStatus) cuyo uso se describe en la documentación oficial de la versión 1.4 del SDK. 5.3.5. Ventajas de las aserciones
Las aserciones son especialmente útiles en tiempo de desarrollo y depurado ya que ayudan a seguir la ejecución del código de forma sencilla y limpia. Una forma práctica de utilizar las aserciones consiste en utilizarlas para depurar el código y una vez que el código es correcto y estable, deshabilitarlas global o individualmente (por clases o paquetes) para agilizar la ejecución del código. Esta forma de utilizarlas pone de manifiesto la ventaja de las aserciones frente a las clásicas sentencias if comprobando expresiones lógicas: según el esquema clásico, la sentencia if siempre se evalúa, mientras que usando las aserciones (y habilitándolas y deshabilitándolas a conveniencia), éstas sólo se evalúan cuando es necesario. Por otra parte, existe una diferencia (subjetiva) entre el uso de aserciones y sentencias if. El uso de sentencias if parece más indicado en aquellas situaciones en las que la condición a evaluar puede ser tanto cierta como falsa de acuerdo con el funcionamiento normal del código. Sin embargo, las aserciones parecen más indicadas cuando se da por supuesto que una condición se cumple y se desea confimar que efectivamente es así. 5.3.6. Efectos laterales
La única restricción sintáctica que se impone a las condiciones utilizadas en las aserciones es que sean expresiones lógicas. Sin embargo, una expresión como (x = true) incluida en una aserción, es una expresión lógica pero también un efecto lateral. Nótese que la evaluación de x depende de que las aserciones estén o no activadas (si no están activadas, el assert no se ejecutará y la variable x no será modificada). Si el resto de código depende del valor de x, es posible que el código no se ejecute correctamente a menos que las aserciones estén activadas. Se desaconseja de forma rotunda el uso de las aserciones con efectos laterales, pues la ejecución correcta del código dependerá de ellos y del estado de las aserciones. Existe un caso concreto en el que se admite el uso de efectos laterales, que será explicado en un apartado posterior. 5.3.7. Compatibilidad de código
Las aserciones introducen una nueva palabra reservada (assert). ¿Qué pasa con el código fuente que contiene algún identificador llamado assert? Esta nueva característica del lenguaje hace que ese código sea incorrecto. Un compilador de la versión 1.4 o posterior dará un error de compilación si encuentra algún identificador llamado assert. (No hay problema con los ficheros .class ya compilados). Para solucionar esta incidencia, los compiladores de la versión 1.4 generan el código según la versión 1.3, es decir, no admiten los identificadores llamados assert pero tampoco activan las aserciones (ya que en la versión 1.3 no existían las aserciones). Para permitir el modo 1.4, y por tanto, el uso de aserciones, se usa el parámetro -source 1.4 en la orden de compilación: javac -source 1.4 *.java 5.3.8. Cómo saber si las aserciones están activas
Existe un patrón muy sencillo que permite saber en tiempo de ejecución si las aserciones están activadas o no: boolean asercionesActivadas = false; assert (asercionesActivadas = true); En el caso de que las aserciones estén activadas, el assert se ejecutará, produciendo intencionadamente un efecto lateral: poner a true el indicador. En caso de que no estén activadas, la linea assert no será ejecutada y el indicador tendrá el valorfalse con el que ha sido inicializado. A pesar de que la evaluación de la condición produce efectos laterales, en este caso, éstos están perfectamente delimitados. Es de esperar que en cada ocasión que se haga uso de la variable asercionesActivadas, se tenga en cuenta la influencia de su valor en el resto del código, según las aserciones estén o no activadas (precisamente ése es el objetivo de esta variable!!). 5.3.9. Conclusión
En la versión 1.4 del SDK se introducen las aserciones (a pesar de que estaban contempladas en el diseño original del lenguaje) como mecanismo para evaluar condiciones lógicas que ayuden a determinar la corrección de nuestro código fuente.
1. IF
La palabra reservada If en Java permite realizar una comprobación sobre una condición booleana (verdadero / falso). La palabra reservada else va en otra dirección, de tal manera que si la condición es falsa, se ejecutará este trozo de código. Un ejemplo sencillo es el siguiente:
package es.scjp.javahispano.tema5; public class P51If { public static void main(String args[]) { int cantidad = 200; final int CANTIDAD_MINIMA = 100; final int CANTIDAD_MAXIMA = 300; } }En este caso, definimos una variable cantidad, a la que asignamos un valor, y dos constantes con valores como máximo y mínimo. Inicialmente, se hace una comprobación de que, si la cantidad es menor a la cantidad mínima, se imprima un mensaje, por otro lado, se realiza la comprobación de que, si la cantidad es mayor a la cantidad máxima, imprimimos otro mensaje de ERROR. Por otra lado si no se han cumplido las condiciones anteriores imprimimos un mensaje diciendo que todo está OK. Aquí podemos ver que podemos agrupar las condiciones iniciales relativas a tamaño mínimo y máximo de la siguiente manera:
package es.scjp.javahispano.tema5; public class P51If { public static void main(String args[]) { int cantidad = 200; final int CANTIDAD_MINIMA = 100; final int CANTIDAD_MAXIMA = 300; if ((cantidad < CANTIDAD_MINIMA) || (cantidad > CANTIDAD_MAXIMA)) { System.out.println("ERROR: El tamaño no es aceptable"); } else { System.out.println("TODO OK"); } } }Como vemos, If espera realizar una comprobación lógica, donde espera un valor booleano true, o bien false. Si el valor es true, ejecutará la acción correspondiente, en caso de ser un valor false, se ejecutará la sentencia else, si está definida. Internamente, podemos tener un nº no limitado de operadores internos, quiero referirme a dentro de la condición booleana, por ejemplo:
if (((cantidad < CANTIDAD_MINIMA) || (cantidad > CANTIDAD_MAXIMA)) && (cantidad > 0) && (cantidad < 10000))En el primer ejemplo, hemos visto que podemos anidar tantos if como queramos. Esto es, podemos realizar una primera comprobación lógica sobre una condición, y si no se cumple, se ejecutará la sentencia else, que a su vez podrá llamar a una sentencia if, que a la par podrá concatenar otra sentencia else, con su if correspondiente... así hasta que decidamos poner fin. Aprovechando el ejemplo anterior, podemos ver el siguiente ejemplo:
int cantidad = 200; final int CANTIDAD_MINIMA = 100; final int CANTIDAD_MAXIMA = 300; if (cantidad < CANTIDAD_MINIMA) { System.out.println("ERROR: El tamaño no es aceptable"); } else if (cantidad > CANTIDAD_MAXIMA) { System.out.println("ERROR: El tamaño no es aceptable"); } else if (cantidad > 0) { System.out.println("ERROR: El tamaño no es aceptable"); } else if (cantidad < 10000) { System.out.println("ERROR: El tamaño no es aceptable"); } else { System.out.println("TODO OK"); }2. SWITCH
Hemos visto el ejemplo anterior, donde podemos concatenar tantos if y else como queramos. Podemos comprobar que if va sobre una condición lógica. Esto nos permite hacer lo siguiente, en lugar de realizar una sentencia if con muchos else anidados, podemos realizar la comprobación sobre la sentencia switch. Ahora bien, tenemos que tener en cuenta que en este caso, no podemos hacer comprobaciones de mayor, menor, etcétera, sólo tenemos la posibilidad de realizar comprobaciones exactas. Veamos el siguiente ejemplo:
int notaDeExamen = 2; switch (notaDeExamen) { case (0): System.out.println("Suspenso"); case (1): System.out.println("Aprobado"); case (2): System.out.println("Notable"); case (3): System.out.println("Sobresaliente"); default: System.out.println("NOTA ERRÓNEA"); }En este caso, imprimiría el siguiente resultado:
- Notable Sobresaliente NOTA ERRÓNEA
int notaDeExamen = 2; switch (notaDeExamen) { case (0): System.out.println("Suspenso"); break; case (1): System.out.println("Aprobado"); break; case (2): System.out.println("Notable"); break; case (3): System.out.println("Sobresaliente"); break; default: System.out.println("NOTA ERRÓNEA");En este caso el valor es NOTABLE, puesto que se ejecuta el caso correspondiente y se sale mediante el uso de la sentencia break. Un caso especial es default, este caso se ejecutará siempre que no se hayan cumplido las sentencias anteriores. Por otra parte, un detalle importante sobre este punto es el siguiente. En las conciones internas de la variables switch, debemos tener en cuenta que se deben especificar constantes, no pudiéndose especificar variables normales. El siguiente código compila:
int notaDeExamen = 2; final int SUSPENSO = 0; final int APROBADO = 1; final int NOTABLE = 2; final int SOBRESALIENTE = 3; switch (notaDeExamen) { case (SUSPENSO): System.out.println("Suspenso"); break; case (APROBADO): System.out.println("Aprobado"); break; case (NOTABLE): System.out.println("Notable"); break; case (SOBRESALIENTE): System.out.println("Sobresaliente"); break; default: System.out.println("NOTA ERRÓNEA"); }Mientras que el siguiente código no compila, al tratar de realizar las comprobaciones contra variables y no constantes:
int notaDeExamen = 2; int suspendo = 0; int aprobado = 1; int notable = 2; int sobresaliente = 3; switch (notaDeExamen) { case (suspenso): System.out.println("Suspenso"); break; case (aprobado): System.out.println("Aprobado"); break; case (notable): System.out.println("Notable"); break; case (sobresaliente): System.out.println("Sobresaliente"); break; default: System.out.println("NOTA ERRÓNEA"); }3. LOOPS
Los loops o bucles de Java tienen como finalidad realizar un bloque de sentencias mientras dure una condición. Por ejemplo, podemos querer recorrer un conjunto de datos array, para imprimir sus posiciones, de la siguiente forma:
/** * @param args */ public static void main(String[] args) { final int LONGITUD_MAXIMA = 5; int[] numeros = new int[LONGITUD_MAXIMA]; numeros[0] = 0; numeros[1] = 1; numeros[2] = 2; numeros[3] = 3; numeros[4] = 4; for (int i = 0; i < LONGITUD_MAXIMA; i++){ System.out.println(numeros[i]); } }En este caso, estamos utilizando el bucle for, que tiene las siguientes características: 1) Necesita de una variable en la que se apoyará para recorrerla (int i = 0); 2) Necesita de una condición de finalización (i < LONGITUD_MAXIMA). 3) Necesita de la ejecución de una sentencia al finalizar el bucle de la misma (i++). En este caso concreto, la ejecución de este bucle realiza las siguientes ejecuciones correspondientes: i=0 / System.out.println(numeros[0]); Es i = 0 < LONGITUD MAXIMA = 5 -> No, entonces ejecutamos i++ y repetimos. i=1 / System.out.println(numeros[1]); Es i = 1 < LONGITUD MAXIMA = 5 -> No, entonces ejecutamos i++ y repetimos. ….......................................................... i=4 / System.out.println(numeros[4]); Siendo la salida: 0 1 2 3 4 En el anterior bucle for, hemos definido la variable i a nivel interno (es decir, dentro del bucle). Podemos, así mismo, coger una variable que sea externa al bucle, veamos el siguiente ejemplo:
int i = 0; for (i=0; i < LONGITUD_MAXIMA; i++){ System.out.println(numeros[i]); }En este caso, la variable i queda definida fuera del bucle, y por lo tanto podrá utilizarse en otra parte del código, lo que sí que veo es que el bucle for obliga a darle un valor inicial a la variable i. Existen otras opciones de bucle, por ejemplo, while... do y do … while. Realmente, yo lo veo como una variación del bucle for, cambiando el orden de comprobación de la condición, la ejecución del bloque de sentencias, y no permitiendo en este caso la inicialización de la variables auxiliar. Los siguientes ejemplos generan el mismo resultado:
// VARIABLE I DEFINIDA FUERA DEL BUCLE int i = 0; // BUCLE FOR for (i = 0; i < LONGITUD_MAXIMA; i++) { System.out.println(i); } // BUCLE WHILE while (i < LONGITUD_MAXIMA) { System.out.println(numeros[i]); i++; } // BUCLE DO WHILE do { System.out.println(numeros[i]); i++; } while (i < LONGITUD_MAXIMA);4. EXCEPTIONS
Las excepciones en Java nos permite controlar salidas no esperadas de un programa. Básicamente, la estructura de control de excepciones es la siguiente:
try{ //EJECUCIÓN DE CÓDIGO QUE SE INTENTA EJECUTAR, POR EJEMPLO INTENTO DE CONECTAR A UNA BASE DE DATOS } catch(Exception e){ //EXCEPCIÓN QUE PUEDE PRODUCIRSE Y QUEREMOS GESTIONAR, POR EJEMPLO, PROBLEMAS DE CONECTIVIDAD, CONTRASEÑA ERRÓNEA, ETC. } finally( //EJECUCIÓN DE CÓDIGO EN CUALQUIER CASO, HAYA EXCEPCIÓN O NO, POR EJEMPLO LIBERACIÓN DE RECURSOS CORRESPONDIENTES);Veamos el siguiente ejemplo y profundicemos en ello:
public static void main(String[] args) { Connection con = null; try { // Registramos el driver y intentamos acceder. Class.forName("com.mysql.jdbc.Driver"); con = DriverManager.getConnection("jdbc:mysql://localhost/prueba", "root", "la_clave"); } catch (Exception e) { // Si hay excepción, la imprimimos por pantalla e.printStackTrace(); } finally { // Finalmente, liberamos recursos en todos los casos try { con.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }En este caso queremos acceder a una base de datos, para la cual debemos primeramente registrar el driver y después intentar realizar la conexión, si tenemos algún problema lo que buscamos es imprimir la información correspondiente al error, y finalmente procuramos liberar los recursos utilizados. Es bueno darnos cuenta de que en este caso finally no es apropiado para realizar la gestión, por diferentes motivos: 1) Al intentar realizar el cierre de conexión, necesitamos gestionar una nueva excepción. 2) En este caso, si hay una excepción antes de abrir la conexión (registrando el driver) o bien en el propio proceso, no tendremos la conexión abierta como tal, y este trozo de código es redundante. No vamos a entrar más en detalle sobre este punto, podemos ver un ejemplo más sencillo en el siguiente bloque de código, para poder tener un esquema mental. Veamos el siguiente ejemplo:
try { // CODIGO QUE SE EJECUTA Y PUEDE TENER EXCEPCIONES } catch (Exception e) { // CODIGO QUE CAPTURA LA EXCEPCION Y LA TRATA } finally { //CODIGO QUE SE EJECUTA EN TODOS LOS CASOS: HAYA EXCEPCIÓN O NO LA HAYA }Podemos definir una excepción como un error no esperado en un programa Java. Las excepciones lógicamente tienen su jerarquía de clases, siendo la clase primaria Throwable, subclases padres Exception y Error de las que cuelgan las demás, heredando el resto de las demás. Veamos la siguiente tabla: Esto nos puede llevar a, dentro de un código de Java relacionado con excepciones, saber que pueden producirse diferentes excepciones dentro del mismo, y por lo tanto, podemos establecer mecanismos de tratado diferentes en función de la excepción que sea. Veamos el siguiente ejemplo, donde queremos tratar un fichero para leerlo y después intentar realizar operaciones dentro de una base de datos abierta.
public static void main(String[] args) { Connection con = null; try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { con = DriverManager.getConnection("jdbc:mysql://localhost/agenda", "root", "LA_PASSWORD"); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } File archivo = new File("C:\\archivo.txt"); FileReader fr = null; try { fr = new FileReader(archivo); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } BufferedReader br = new BufferedReader(fr); String linea = null; try { linea = br.readLine(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } while (linea != null) { PreparedStatement pstmt = null; try { pstmt = con.prepareStatement("insert into lineas ('?')"); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { pstmt.setString(0, linea); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { pstmt.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }Aquí podemos ver que tratamos cada excepción relativizandola a su propio trozo de código, sin embargo, veamos lo siguiente: Aquí tenemos un código más limpio y organizado:
/** * @param args */ public static void main(String[] args) { try { Connection con = null; Class.forName("com.mysql.jdbc.Driver"); con = DriverManager.getConnection("jdbc:mysql://localhost/agenda", "root", "LA_PASSWORD"); File archivo = new File("C:\\archivo.txt"); FileReader fr = null; fr = new FileReader(archivo); BufferedReader br = new BufferedReader(fr); String linea = null; linea = br.readLine(); while (linea != null) { PreparedStatement pstmt = null; pstmt = con.prepareStatement("insert into lineas ('?')"); pstmt.setString(0, linea); pstmt.executeUpdate(); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }donde dejamos la gestión de excepciones para el final. Perfecto, ahora vamos a ver un buen ejemplo donde ver cómo podemos aplicar el polimorfirmo en este caso:
/** * @param args */ public static void main(String[] args) { try { Connection con = null; Class.forName("com.mysql.jdbc.Driver"); con = DriverManager.getConnection("jdbc:mysql://localhost/agenda", "root", "LA_PASSWORD"); File archivo = new File("C:\\archivo.txt"); FileReader fr = null; fr = new FileReader(archivo); BufferedReader br = new BufferedReader(fr); String linea = null; linea = br.readLine(); while (linea != null) { PreparedStatement pstmt = null; pstmt = con.prepareStatement("insert into lineas ('?')"); pstmt.setString(0, linea); pstmt.executeUpdate(); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }En este caso, sabemos que todas las excepciones que capturamos son una excepción, con lo cual, nosotros podemos decir que, sea cual sea la excepción, nuestro cometido es gestionarla y como tal realizar una acción, pero vemos que en este caso, el control se hace de modo genérico, y no instanciando y diferenciando la misma. También podemos ver en los ejemplos anteriores que no es necesario definir el bloque finally. Volviendo a la jerarquía de clases, vemos que tenemos dos clases padre principales: Error: indica un problema serio no esperado por el programa, y como tal, no gestionable, que generar la finalización de un programa. Un ejemplo es un error interno no esperado, un error de memoria... etc. Exception: define excepciones que un programa espera y puede gestionar. Dentro de Exception, tenemos las siguientes subdivisiones: RunTimeException (error en tiempo de ejecución, por ejemplo un Null Pointer). IOException (error de entrada salida, por ejemplo un fichero no encontrado). 5. ASSERT
5.1. Introducción
Una aserción es una condición lógica insertada en nuestro código fuente. La idea es expresar condiciones que asumimos que son ciertas. El sistema se encargará de comprobarlas y avisar mediante una excepción en caso de que no se cumplan. Por ejemplo, supongamos que hacemos una búsqueda en un vector, con garantía de éxito, de algún valor mayor que 1000:
for (int i = 0; i < 10; i++) { if (v[i] > 1000) return i; }Si, de alguna manera, sabemos que en el vector hay algún valor mayor que 1000, sabemos que el return se va a ejecutar para algún i, y que por tanto, no se va a llegar más allá del bucle. Para garantizar esto, después del bucle podríamos incluir la siguiente sentencia: assert false; En el caso de que el bucle termine sin haber ejecutado el return, se evaluará la expresión del assert. En este caso, la expresión se evalúa directamente a false por lo que el sistema lanza una excepción (de tipoAssertionError), informando de esta situación. La expresión a evaluar puede ser todo lo complicada que queramos. El único requisito es que, sintácticamente, sea de tipo lógico. En el siguiente ejemplo, usamos una clase que nos devuelve un valor, en teoría siempre positivo.
try { resultado = Clase.operacionComplicada(); } catch (Throwable t) { t.printStackTrace(); } assert resultado > 0;El assert nos ayuda a garantizar que el resultado obtenido cumple la restricción establecida. 5.2. Sintaxis y semántica de las aserciones
Hay dos formas de escribir las aserciones: assert expresion; y assert expresion1 : expresion2; En el primer caso, el significado es el que ya se ha descrito: si el resultado de evaluar expresion es true la ejecución continúa normalmente y si es false, se lanza una excepción de tipo AssertionError. En el segundo caso, primero se evalúa expresion1. Si el resultado es true, la ejecución continúa. Si el resultado es false, entonces se evalúa expresion2 y el resultado se pasa como argumento al constructor del AssertionError lanzado. Así, el primer caso es un caso particular del segundo. En caso de que la expresión se evalúe a falso, se utiliza el constructor sin parámetros de AssertionError. 5.3. Usos de las aserciones
5.3.1. Precondiciones
Una precondición es una condición lógica que impone restricciones a la ejecución de un determinado fragmento de código; típicamente, un método. Por ejemplo, nos puede interesar diseñar un método para calcular raíces cuadradas de números estrictamente positivos. Formalmente, estableceríamos la siguiente condición: { x > 0 } siendo x el argumento del método. Podemos imponer esta restricción a los usuarios de ese método, de dos maneras complementarias: Por contrato: En la documentación del método, se establece un contrato entre el implementador y el usuario del método. En el ejemplo anterior, el resultado sólo está definido para números estrictamente positivos. El usuario que viole este contrato no deberá esperar el correcto funcionamiento del método. Sin embargo, a menudo es necesario blindar incluso los métodos que están protegidos por un contrato, para garantizar la solidez y robustez del método concreto y de la aplicación a nivel global. Esto se puede conseguir escribiendo las asserciones adecuadas, que validen los datos de entrada, de acuerdo a las restricciones del contrato. Por código: Lo primero que hay que hacer en el método es comprobar que el argumento es válido. En el ejemplo anterior, serviría algo como: if (x <= 0) throw new IllegalArgumentException(); Esta sentencia se puede sustituir por assert (x > 0); La diferencia entre una y otra y el porqué de usar una u otra se verá en un apartado posterior. 5.3.2. Postcondiciones
Una postcondición es una condición lógica que impone restricciones al estado alcanzado después de la ejecución de un fragmento de código (típicamente, un método). Por ejemplo, en un método para ordenar los elementos de un vector, al final del método se podría comprobar que todos los elementos están efectivamente ordenados:
public int [] ordenar (int [] v) { int [] v2; v2 = ... assert (v2.estaOrdenado()); } private boolean estaOrdenado (int [] v) { boolean ordenado; // Comprobar si v está ordenado ordenado = ...; return ordenado; }5.3.3. Control del flujo
Las aserciones permiten comprobar en tiempo de ejecución que el flujo del programa es correcto. Por ejemplo, dado el siguiente código:
if (a > 0) tratarNumeroPositivo(a); else if (a == 0) descartarNulo(); else tratarNumeroNegativo(a);podríamos sustituir la última parte por:
else { assert (a < 0); tratarNumeroNegativo(a); }También es útil para clasificar un valor entre un conjunto determinado de valores y excluir excepciones:
switch (op) { case AND: sumar(); break; case OR: restar(); break; case NOT: negar(); break; default: assert false; }... El assert utilizado es útil cuando sabemos que op va a tener uno de los tres valores contemplados y queremos asegurarnos de ello. Otro ejemplo, con un bucle while:
while (saldo > 0) { apostar(100); } assert (saldo <= 0);5.3.4. Habilitando y deshabilitando las aserciones
Por defecto, las aserciones están deshabilitadas. Para habilitarlas, hay varias alternativas: Habilitar las aserciones en todas las clases de todos los paquetes: java -enableassertions Habilitar las aserciones en el paquete com.javahispano y sus subpaquetes: java -enableassertions:com.javahispano Habilitar las aserciones en todas las clases que no pertenecen a ningún paquete, que estén en el directorio actual: java -enableassertions Habilitar las aserciones en la clase com.javahispano.Util: java -enableassertions:com.javahispano.Util Además, existe un parámetro -disableassertions, para deshabilitar explicitamente las aserciones, que se utiliza de forma similar. Es posible combinar ambos parámetros. En este caso, el orden de los mismos es importante. Por ejemplo: java -disableassertions:com.javahispano... -enableassertions:com.javahispano.Util deshabilita las aserciones en todas las clases del paquete com.javahispano y de sus subpaquetes, excepto las de la clase com.javahispano.Util. (Existen dos parámetros más, -enablesystemassertions y -disablesystemassertions para controlar el uso de las aserciones en el código de las clases del sistema. Su uso se describe en la documentación oficial de la versión 1.4 del SDK). Por otra parte, es posible habilitar y deshabilitar por programa, el uso de las aserciones. Para ello, en la clasejava.lang.ClassLoader se definen varios métodos (setDefaultAssertionStatus,setPackageAssertionStatus, setClassAssertionStatus y clearAssertionStatus) cuyo uso se describe en la documentación oficial de la versión 1.4 del SDK. 5.3.5. Ventajas de las aserciones
Las aserciones son especialmente útiles en tiempo de desarrollo y depurado ya que ayudan a seguir la ejecución del código de forma sencilla y limpia. Una forma práctica de utilizar las aserciones consiste en utilizarlas para depurar el código y una vez que el código es correcto y estable, deshabilitarlas global o individualmente (por clases o paquetes) para agilizar la ejecución del código. Esta forma de utilizarlas pone de manifiesto la ventaja de las aserciones frente a las clásicas sentencias if comprobando expresiones lógicas: según el esquema clásico, la sentencia if siempre se evalúa, mientras que usando las aserciones (y habilitándolas y deshabilitándolas a conveniencia), éstas sólo se evalúan cuando es necesario. Por otra parte, existe una diferencia (subjetiva) entre el uso de aserciones y sentencias if. El uso de sentencias if parece más indicado en aquellas situaciones en las que la condición a evaluar puede ser tanto cierta como falsa de acuerdo con el funcionamiento normal del código. Sin embargo, las aserciones parecen más indicadas cuando se da por supuesto que una condición se cumple y se desea confimar que efectivamente es así. 5.3.6. Efectos laterales
La única restricción sintáctica que se impone a las condiciones utilizadas en las aserciones es que sean expresiones lógicas. Sin embargo, una expresión como (x = true) incluida en una aserción, es una expresión lógica pero también un efecto lateral. Nótese que la evaluación de x depende de que las aserciones estén o no activadas (si no están activadas, el assert no se ejecutará y la variable x no será modificada). Si el resto de código depende del valor de x, es posible que el código no se ejecute correctamente a menos que las aserciones estén activadas. Se desaconseja de forma rotunda el uso de las aserciones con efectos laterales, pues la ejecución correcta del código dependerá de ellos y del estado de las aserciones. Existe un caso concreto en el que se admite el uso de efectos laterales, que será explicado en un apartado posterior. 5.3.7. Compatibilidad de código
Las aserciones introducen una nueva palabra reservada (assert). ¿Qué pasa con el código fuente que contiene algún identificador llamado assert? Esta nueva característica del lenguaje hace que ese código sea incorrecto. Un compilador de la versión 1.4 o posterior dará un error de compilación si encuentra algún identificador llamado assert. (No hay problema con los ficheros .class ya compilados). Para solucionar esta incidencia, los compiladores de la versión 1.4 generan el código según la versión 1.3, es decir, no admiten los identificadores llamados assert pero tampoco activan las aserciones (ya que en la versión 1.3 no existían las aserciones). Para permitir el modo 1.4, y por tanto, el uso de aserciones, se usa el parámetro -source 1.4 en la orden de compilación: javac -source 1.4 *.java 5.3.8. Cómo saber si las aserciones están activas
Existe un patrón muy sencillo que permite saber en tiempo de ejecución si las aserciones están activadas o no: boolean asercionesActivadas = false; assert (asercionesActivadas = true); En el caso de que las aserciones estén activadas, el assert se ejecutará, produciendo intencionadamente un efecto lateral: poner a true el indicador. En caso de que no estén activadas, la linea assert no será ejecutada y el indicador tendrá el valorfalse con el que ha sido inicializado. A pesar de que la evaluación de la condición produce efectos laterales, en este caso, éstos están perfectamente delimitados. Es de esperar que en cada ocasión que se haga uso de la variable asercionesActivadas, se tenga en cuenta la influencia de su valor en el resto del código, según las aserciones estén o no activadas (precisamente ése es el objetivo de esta variable!!). 5.3.9. Conclusión
En la versión 1.4 del SDK se introducen las aserciones (a pesar de que estaban contempladas en el diseño original del lenguaje) como mecanismo para evaluar condiciones lógicas que ayuden a determinar la corrección de nuestro código fuente.
Reader Comments (1)
Muy buen artículo, sobretodo el tema de las aserciones.
Gracias