If y Switch en Java

Buenas tardes, en esta entrada vamos a estudiar un poco mas en profundidad las sentencias if y switch. Son comúnmente referidas como sentencias de decisión. Cuando usamos sentencias de decisión en nuestro programa, estamos preguntando al programa que evalúe unas expresiones dadas para determinar el curso de acciones que tomará.


  • Derivación mediante if-else

    El formato básico de una declaración de if es la siguiente:

    		if (booleanExpression){
    			System.out.println("Dentro de una sentencia if");
    		}
    		

    La expresión en parentesis debe evaluar a un boolean, es decir, true o false. Típicamente estaremos probando si algo es true, y entonces ejecutar el bloque de código que se encuentra en el interior si esto es true, y, opcionalmente, otro bloque de código if si no es así. El siguiente ejemplo de código demuestra un uso legal de una sentencia if-else:

    		if (x > 3) {
    			System.out.println("x es mayor que 3");
    		} else {
    			System.out.println("x no es mayor que 3");
    		}
    		

    El bloque else es opcional, por lo que podemos tambien usarlo así:

    		if (x > 3) {
    			y = 2;
    		}
    		z += 8;
    		a = y + x;
    		

    El ejemplo anterior asignará 2 a y si el test se cumple (es decir, que x realmente es mayor que 3), pero las otras 2 líneas se ejecutarán sin tener en cuenta lo anterior. Incluso las llaves son opcionales si tenemos solo una sentencia a ejecutar dentro del cuerpo de un bloque condicional. El siguiente código de ejemplo es legal ( aunque no recomendado porque es menos legible):

    		if (x > 3)	// Mala práctica, pero legal
    			y = 2;
    		z += 8;
    		a = y + x;
    		

    Se considera buena práctica cerrar siempre los bloques con llaves, incluso si hay una sola sentencia en el bloque. Tengamos cuidado con el código como el de arriba, porque podemos pensar que podría leerse así:

    “”Si x es mayor que 3, entonces establecemos y a 2, z a z + 8 y a a la suma de y + x”

    Pero las últimas 2 lineas se ejecutarán sin importar nada de lo que pase en el if. No son parte del flujo del condicional.

    Puede que necesitemos enlazar sentencias if-else (aunque de nuevo, no es recomendable porque no es legible). Podemos pone un if-else para comprobar múltiples condiciones. El siguiente ejemplo usa 2 condiciones con lo que si el primer test falla, queremos realizar un segundo test antes de decidir que hacer:

    		if (price < 300) {
    			buyProduct();
    		} else {
    			if (price < 400) {
    				getApproval();
    			}
    			else {
    				dontBuyProduct();
    			}
    		}
    		

    Esto nos lleva a otra construcción de if-else, el if, else if, else. El código anterior podría ser recreado de la siguiente manera:

    		if (price < 300) {
    			buyProduct();
    		} else if (price < 400) {
    			getApproval();
    		} else {
    			dontBuyProduct();
    		}
    		

    Hay un par de reflas para el uso de else y else if:

    • Podemos tener uno o ningún else para un if dado, y debe estar después de cualquier else if.
    • Podemos tener cero o múltiples else if para un if dado que deben venir antes del else (opcional).
    • Una vez que el else if tiene éxito, ninguno de los else if o elses serán comprobados.

    El siguiente ejemplo muestra código que está horriblemente formateado para el mundo real. Como podremos suponer probablemente, es bastante probable que encontremos este tipo de formato en muchos lugares. En cualquier caso, el código demuestra el uso de múltiples else if:

    		int x = 1;
    		if ( x == 3 ){ }
    		else if (x < 4) { System.out.println("<4"); }
    		else if (x < 2) { System.out.println("<2"); }
    		else { System.out.println("else"); }
    		

    Esto producirá la salida: <4

    A veces podemos tener problemas a la hora de figurar como emparejar nuestros if y else, como en el siguiente ejemplo:

    		if (exam.done())
    		if (exam.getScore() &lt; 0.61)
    		System.out.println(&quot;Try again&quot;);
    		// ¿ A que if corresponde este else?
    		else System.out.println(&quot;Java Master!&quot;);
    		

    Intencionadamente hemos dejado la sangría en este pedazo de código por lo que no obtenemos ninguna pista sobre a cual sentencia if le pertenece el else. La ley de Java dice que una clausula else pertenece al if mas cercano (en otras palabras, al if mas cercano que le precede). En el caso del ejemplo anterior, el else pertenece a la segunda sentencia if en el código. Con una sangría propia, se podría ver de la siguiente forma:

    		if (exam.done())
    			if (exam.getScore() &lt; 0.61)
    				System.out.println(&quot;Try again&quot;);
    			else
    				System.out.println(&quot;Java master!&quot;);
    		

    Siguiente nuestras convenciones de código usando llaves, sería mas facil leerlo de la siguiente manera:

    		if (exam.done()) {
    			if (exam.getScore &lt; 0.61) {
    				System.out.println(&quot;Try again&quot;);
    			} else {
    				System.out.println(&quot;Java master!&quot;);
    			}
    		}
    		

    Ahora pongamos atención a lo siguiente:

    		if (exam.done())
    			if (exam.getScore() &lt; 0.61)
    				System.out.println(&quot;Try again&quot;);
    		else
    			System.out.println(&quot;Java Master!&quot;); // A cual pertenecería?
    		

    Por supuesto, el código anterior es exactamente igual que los anteriores ejemplos, excepto por la manera en la que se vé.

    • Expresiones legales para las sentencias if

      La expresión en una sentencia if debe ser una expresión booleana. cualquier expresión que se resuelva en un boolean está bien, y algunas expresiones pueden ser complejas. Asumimos que doStuff() retorna true:

      				int y = 5;
      				int x = 2;
      				if (((x &gt; 3) &amp;&amp; (y &lt; 2)) | doStuff()) {
      					System.out.println(&quot;true&quot;);
      				}
      				

      Esto imprimirá por pantalla: true

      Podemos leer lo siguiente como, “Si las expresiones (x > 3) y (y 3) y (y < 2) tendrán que ser true para poder imprimir true. El código anterior es incluso mas complejo si dejamos fuera un set de parentesis como lo siguiente:

      				int y = 5;
      				int x = 2;
      				if ((x &gt; 3) &amp;&amp; (y &lt; 2) | doStuff()) {
      					System.out.println(&quot;true&quot;);
      				}
      				

      Esto imprimirá….NADA!. El anterior ejemplo (con unos parentesis de menos) evalúa la expresión de la siguiente manera: “Si (x > 3) es true, y cualquiera de los 2, (y 3) no es true, no se vuelev a ver el resto de la expresión. Ya que el && es un operador corto-circuito, la expresión es evaluada como si no hubiera parentesis alrededor de (y < 2) | doStuff(). En otras palabras, es evaluada como una expresión singular antes de que && y la exprexión singular despues del &&.

      Recordemos que solo son legales expresiones en un test if si es boolean. En algunos lenguajes, 0 == false, y 1 == true. Pero esto no es así en Java. El siguiente código muestra una sentencia if que puede ser tentadora, pero son ilegales, seguidas de unas sustituciones legales:

      				int trueInt = 1;
      				int falseInt = = 0;
      				if (trueInt)			// ilegal
      				if (trueInt == true)	// ilegal
      				if (1)					// ilegal
      				if (falseInt == false)	// ilegal
      				if (trueInt == 1)		// legal
      				if (falseInt == 0)		// legal
      				
  • Sentencia switch

    Una manera de simular el uso de multiples sentencias if es con la sentencia switch. Vamos a ver el siguiente código de if-else, y vamos a ver como de confuso puede ser tener if enlazados, incluso aunque no sea muy profundo:

    		int x = 3;
    		if (x == 1) {
    			System.out.println(&quot;x es igual que 1&quot;);
    		}
    		else if (x == 2) {
    			System.out.println(&quot;x es igual que 2&quot;);
    			}
    			else if (x == 3) {
    				System.out.println(&quot;x es igual que 3&quot;);
    			}
    			else {
    				System.out.println(&quot;Ni idea de lo que es x&quot;);
    			}
    		

    Ahora vamos a ver la misma funcionalidad representada en una construcción switch:

    		int x = 3;
    		switch (x){
    			case 1:
    				System.out.println(&quot;x es igual que 1&quot;);
    				break;
    			case 2:
    				System.out.println(&quot;x es igual que 2&quot;);
    				break;
    			case 3:
    				System.out.println(&quot;x es igual que 3&quot;);
    				break;
    			default:
    				System.out.println(&quot;Ni idea de lo que es x&quot;);
    		}
    		

    Nota: La razón por la que el switch emula if enlazados vista anteriormente es porque las sentencias break dentro del switch. En general, las sentencias break son opcionales, y como veremos ahora, su inclusión o exclusión cambia de una manera muy grande como la sentencia switch se ejecutará.

    • Expresiones legales para switch y case

      La forma general de una sentencia switch es la siguiente:

      				switch (expression) {
      					case constant1: code block
      					case constant2: code block
      					default: code block
      				}
      				

      Una expresión switch debe evaluar un char, byte, short, int, o a partir de Java 6, una enumeración. Esto significa que si no estamos usando una enumeracion, solo variables y valores que pueden ser automáticamente promovidos (o implícitamente casteados) a un int son aceptables. No se nos permitirá si estamos usando cualquier otra closa, incluso tipos de dato como long, float o double.

      Una constante case debe evaluar al mismo tipo que la expresión switch está usando, con una restricción adicional, y grande: la constante del case debe ser constante en tiempo de compilación. Ya que el argumento del case tiene que ser resuelto en tiempo de compilación, esto quiere decir que podemos usar solo una constante o una variable final que ha sido asignada a un valor literal. No es suficiente que sea final, debe ser una constante en tiempo de compilación. Por ejemplo:

      				final int a = 1;
      				final int b;
      				b = 2;
      				int x = 0;
      				switch(x){
      					case a:		// ok
      					case b:		// error de compilación
      				}
      				

      Tambien, el switch solo puede comprobar igualdad. Esto significa que si otro operador relacional como un mayor que no pueden ser usados en un case. Lo siguiente es un ejemplo de una expresión válida usando una invocación a un método en una sentencia switch. Vemos que para que este código sea legal, el método que está siendo invocado en la referenia al objeto debe retornar un valor compatible con un int.

      				String s = &quot;xyz&quot;;
      				switch (s.length()){
      					case 1:
      						System.out.println(&quot;Longitud es 1&quot;);
      						break;
      					case 2:
      						System.out.println(&quot;Longitud es 2&quot;);
      						break;
      					case 3:
      						System.out.println(&quot;Longitud es 3&quot;);
      						break;
      					default:
      						System.out.println(&quot;no hay coincidencia&quot;);
      				}
      				

      Otra regla en la que nos podemos ver envueltos es la siguiente. “¿Qué pasa si una expresión en un switch es una variable mas pequeña que un int?” Vamos a ver el siguiente switch:

      				byte g = 2;
      				switch(g){
      					case 23:
      					case 128:
      				}
      				

      Este código anterior no compilará. aunque el argumento del switch sea legal, un byte es implícitamente casteado a un int, pero el segundo argumento del case (128) es demasiado largo para un byte y el compilador lo sabe. Intentando compilar el anterior código obtendremos un error.

      Es tambien ilegal tener mas de un case usando el mismo valor. Por ejemplo el siguiente bloque de código no compilará porque usa 2 cases con el mismo valor de 80:

      				int temp = 90;
      				switch(temp){
      					case 80: System.out.println(&quot;80&quot;);
      					case 80: System.out.println(&quot;80&quot;);	// no compilará
      					case 90: System.out.println(&quot;90&quot;);
      					default: System.out.println(&quot;default&quot;);
      				}
      				

      Es legal usar el poder del boxing en una expresión switch. Por ejemplo, lo siguiente es legal:

      				switch (new Integer(4)){
      					case 4: System.out.println(&quot;boxing is OK&quot;);
      				}
      				
    • Break y Fall-Through en Bloques switch

      Ahora estamos finalmente preparados para discutir las sentencias break, y mas detalles sobre el flujo de control dentro de una sentencia switch. La cosa mas importante que tenemos que recordar sobre el flujo de ejecución en un switch es lo siguiente:

      Las constantes de los case son evaluadas desde arriba hacia abajo, y la primera constante case que coincida con la expresión del switch es el punto de entrada de la ejecución.

      En otras palabras, una vez que se coincide con la constante de n case, la JVM ejecutará el bloque de código asociado, y TODAS los bloques de códigos posteriores (excepto los que tengan una una sentencia break) tambien. El siguiente ejemplo usa una enumeracion en una sentencia case.

      				enum Color {red, gree, blue}
      				class SwitchEnum {
      					public static void main(String[] args){
      						Color c = Color.green;
      						switch(c){
      							case red: System.out.println(&quot;red &quot;);
      							case green: System.out.println(&quot;green &quot;);
      							case blue: System.out.println(&quot;done&quot;);
      						}
      					}
      				}
      				

      En el ejemplo, el case green: coincide, por lo que la JVM ejecuta ese bloque de código y todos los bloques de código posteriores para producir la salida:

      				green blue done
      				

      De nuevo, cuando el programa encuentra la palabra clave break durante la ejecución de una sentencia switch, la ejecución se moverá inmediatamente fuera del bloque del switch la siguiente sentencia despues del switch. Si se omite el break, el programa continuará ejecutando los bloques de los case que quedan hasta que se encuentre un break o hasta que se termine la sentencia switch. Examinemos el siguiente código:

      				int x = 1;
      				switch(x) {
      					case 1: System.out.println(&quot;x es uno&quot;);
      					case 2: System.out.println(&quot;x es dos&quot;);
      					case 3: System.out.println(&quot;x es tres&quot;);
      				}
      				

      Este código imprimirá lo siguiente:

      				x es uno
      				x es dos
      				x es tres
      				

      Esta combinación ocurre porque el código no se encuentra con ningúna sentencia break en la ejecución, y esta sigue bajando entrando en cada case hasta el final. Esta caida es llamada actualmente como “fall-through”, ya que la manera en la que actúa la ejecución cae desde un case hacia el siguiente, hasta que termina. Recordemos, que el case que coincida, será el punto de entrada al bloque switch. En otras palabras, no tenemos que pensar así: “Encontramos el case que coincida, ejecutamos ese código, y nos vamos”. Así no es como funciona. Si queremos que sea así, que solo se ejecute el código que coincide, entonces tendremos que insertar un break para cada case como el siguiente ejemplo:

      				int x = 1;
      				switch(x){
      					case 1: {
      						System.out.println(&quot;x es uno&quot;); break;
      					}
      					case 2: {
      						System.out.println(&quot;x es dos&quot;); break;
      					}
      					case 3: {
      						System.our.println(&quot;x es tres&quot;); break;
      					}
      				}
      				System.out.println(&quot;Fuera del switch&quot;);
      				

      Si ejecutamos este código, ahora que hemos añadido las sentencias break, obtendremos la siguiente salida por pantalla:

      				x es uno
      				Fuera del switch
      				

      Y así es. Hemos entrado al bloque switch en el case 1. Como ha coincidido con el argumento del siwtch(), entonces se ha imprimido la sentencia que deseabamos, y al encontrarse con el break, ha saltado al final del switch. Un interesante ejemplo del fall-through la podemos ver en el siguiente ejemplo:

      				int x = someNumberBetweenOneAndTen;
      				switch(x){
      					case 2:
      					case 4:
      					case 6:
      					case 8:
      					case 10: {
      						System.out.println(&quot;x es es numero par&quot;); break;
      					}
      				}	
      				

      La sentencia switch imprimirá “x es un numero par” o nada, dependiendo de que número sea la variable x. Por ejemplo, si x es 4, la ejecución empezará en el case 4 y caerá pasando por la 6, la 8 y la 10, donde imprime y entonces sale. El break en el case 10, de otro modo, no es necesario, porque ya estaríamos al final del switch de todos modos. Tomemos nota de algo, aunque el fall-through es menos que intuitivo, se recomiendo añadir un comentario para notificarlo.

    • Default Case

      Que pasaría, si en el código anterior, hubieramos querido imprimir “x es un número impar” si ninguno de los casos (de los numeros pares) hubiera coincidido. No podríamos ponerlo después de la sentencia switch o incluso al final del switch, porque en ambas situaciones siempre imprimiría x es un número impar. Para conseguir este comportamiento, podríamos usar la palabra clave default. El único cambio que deberíamos añadir para hacer esto es agregar el default al código anterior:

      				int x = someNumberBetweenOneAndTen;
      				
      				switch(x){
      					case 2:
      					case 4:
      					case 6:
      					case 8:
      					case 10: {
      						System.out.println(&quot;x es un numero par&quot;);
      						break;
      					}
      					default:
      						System.out.println(&quot;x es un numero impar&quot;);
      				}
      				


Y hasta aquí un poco mas visto en profundidad como funcionan las sentencias if y switch, como podemos usarlas, en que casos podemos usarlas, y cual es la manera mas correcta y legible de escribirlas, para evitar problemas de mantenimiento en el futuro.

Sin mas, cualquier corrección o aporte es bienvenido.

Saludos!!!