Condicionales

Código 1 - usando if y switch para condicionales
boolean esVocal(char c) {
  var mayúscula = Character.toUpperCase(c);
  return switch(mayúscula) {
    case 'A', 'E', 'I', 'O', 'U' -> true;
    default -> false;
  };
}

int contarVocales(String texto) {
  var vocales = 0;
  for (var i = 0; i < texto.length(); ++i) {
    var actual = texto.charAt(i);
    if (esVocal(actual)) {
      ++vocales;
    }
  } 
  return vocales;
}
Input
> esVocal('a')
> esVocal('A')
> esVocal('z')
> contarVocales("¡Hola Mundo!")
Output
true
true
false
4

El código 1 muestra las dos construcciones condicionales que tienen la mayoría de los lenguajes de programación: declaraciones switch y declaraciones if. Recuerda que antes hemos visto el operador ternario, que también es una forma de tener condicionales en tu programa.

Con un switch, podemos actuar según el valor de una variable de tipo primitivo o Strings. La sentencia switch provee una lista de valores y adicionalmente el valor que se debe retornar si la variable tiene ese valor. Para indicar un valor dentro de un switch se utiliza una sentencia case, como la de la línea 04. En este caso la sentencia case tiene una lista separada por comas de varios valores y luego una «flecha», escrita en dos caracteres, el carácter de guión corto y el mayor que para finalmente especificar el valor asociado. Ese valor puede ser cualquier expresión por ejemplo un literal, un llamado a una función, etc. La línea 04 indica que si la variable dentro del switch tiene el valor 'A', 'E', 'I', 'O', o 'U' entonces retornamos el valor true.

Un switch puede tener tantas sentencias case como sean necesarias, pero también tiene que proveer una sentencia default, como en la línea 05, que indique el valor que se debe retornar si ninguna sentencia case se cumple, esto es importante para que el switch sea "completo", es decir que para todo valor existe un valor para asociar. Para la sentencia default solo necesitamos agregar la flecha y el valor correspondiente.

La línea 03 comienza la declaración de switch con la palabra clave switch, la expresión que se va a usar va entre paréntesis. Como las funciones y los ciclos, los condicionales también especifican un bloque como puede verse en las líneas 03 y 06. Un switch establece una regla de transformación donde indicamos para unos valores posibles qué expresión le corresponde según las reglas de negocio. El resultado de una expresión switch es un valor y este puede ser atrapado en una variable o retornado. En la línea 03 vemos como inmediatamente retornamos el valor dado que la sentencia return está antes de la sentencia switch.

Esta es la manera recomendada de usar un switch, envolver un switch en su propia función. Esto cumple con la idea de que una función debe hacer una sola cosa, y le da un nombre al switch lo que mejora la legibilidad del código.

La línea 04 especifica los valores para el primer caso a considerar. Anteriormente pusimos el carácter en mayúsculas para simplificar el código, aunque hubiera sido igualmente válido tener código así:

boolean esVocal(char c) {
  return switch(mayúscula) {
    case 'A', 'a', 'E', 'e', 'I', 'i', 'O', 'o', 'U', 'u' -> true;
    default -> false;
  };
}

La línea 06 termina en punto y coma después del final del bloque del switch. Esta forma nueva de switch en que el valor es atrapado en una variable o usado como parte de una expresión return, necesita terminar en punto y coma como cualquier otra sentencia.

contarVocales() tiene una declaración if en la línea 13. Las declaraciones if toman cualquier predicado y cuando este es verdadero, ejecutan el código dentro de su bloque. Antes, teníamos un acumulador y un ciclo para recorrer los caracteres de la variable «texto» de tipo String. Recuerde que charAt() devuelve el caracter en un índice dado y el ciclo, como está escrito, recorre todos los índices válidos de «texto». Si el carácter actual es una vocal, y solo si eso es cierto, entonces incrementamos el valor de la variable acumuladora «texto». Si el predicado es falso, no se ejecuta nada.

Los condicionales son simples, pero hay algunas sutilezas que discutiremos a lo largo de este capítulo

Las muchas formas de switch

El switch más básico es este:

switch(expression) {
  case valor1 -> valor1;
  case valor2 -> valor2;
  default -> valorPorDefecto;
}

La variable puede ser cualquier primitiva, String o Enum (que veremos más adelante). El switch necesita un bloque para que quede claro qué código forma parte de él. En una cláusula case, se asocia un posible valor con otro valor. Usamos la palabra clave default para especificar el valor a usar en caso de que ninguna sentencia case sea cierta. Esta sentencia es necesaria y aunque no tiene que ser la última dentro del switch, esto mejora su legibilidad.

Se pueden agrupar valores en una sentencia case separándolos por comas:

switch(expression) {
  case valor1, valor2 -> valorComún;
  case valor3 -> valorPara3;
  default -> valorPorDefecto;
}

Las muchas formas de if

La forma más simple de una declaración if es:

if (predicado) {
  código;
}

Esto significa que «codigo»; solo se ejecutará si el predicado es verdadero.

Puedes agregar una declaración else como última parte del bloque if:

if (predicado) {
  código-en-verdadero;
} else {
  código-en-falso;
}

Ahora «código-en-verdadero» solo se ejecutará si el predicado es verdadero; de lo contrario, se ejecutará «código-en-falso». Las declaraciones «else» son siempre opcionales. Nota además que nunca se ejecutará «código-en-verdadero» y «código-en-falso» al tiempo. Se ejecuta uno u el otro.

También son opcionales varias declaraciones else-if que deben venir después del bloque if pero antes del bloque else si está presente. Estas condiciones else-if se probarán sólo si todas las condiciones anteriores son falsas:

if (predicado1) {
  condicion-1-code;
} else if (predicado2) {
  condicion-2-code;
}

En este caso, el código de «predicado1» se ejecutará cuando «predicado1» sea verdadero y el código de «predicado2» se ejecuta si «predicado1» es falso y «predicado2» es verdadero. No se ejecutará nada si la «predicado1» y «predicado2» son falsos.

Código 2 usando if/else
String darFormato(String teléfono, String formato) {
  var builder = new StringBuilder();
  var índiceTeléfono = 0;
  
  for (var i = 0; i < formato.length(); ++i) {
    var actual = format.charAt(i);
    if (actual != 'X') {
      builder.append(actual);
    } else {
      var dígitoTeléfono = teléfono.charAt(índiceTeléfono);
      builder.append(dígitoTeléfono);
      ++índiceTeléfono;
    }
  } 
  return builder.toString();
}

String teléfonoConFormato(String teléfono) {
  return switch(teléfono.length()) {
    case 10 -> darFormato(teléfono, "(XXX) XXX-XXXX");
    case 7 -> darFormato(teléfono, "XXX-XXXX");
    default -> teléfono;
  };
}
Input
> teléfonoConFormato("1234567890")
> teléfonoConFormato("9001000")
Output
(123) 456-7890
900-1000

El código 2 muestra un par de funciones para dar formato a números de teléfono. El corazón de esta funcionalidad es la función darFormato() que toma un String que representa un número de teléfono que contiene solo dígitos y un formato que tiene 'X' como marcador de posición para los dígitos. La idea es copiar cada carácter del formato a un StringBuilder hasta llegar a un marcador de posición, en cuyo caso agregamos el siguiente dígito disponible del teléfono.

Para mezclar los caracteres necesitamos 2 índices: uno para los caracteres del formato y otro para los del teléfono. El ciclo recorre los caracteres del formato. Otro índice, teléfonoÍndice, se crea fuera del ciclo y se actualiza solo cuando se agrega el dígito de un teléfono al String que se está creando.

La otra función elige el formato que se utilizará en darFormato() según la longitud del teléfono: hay 2 formatos previstos: uno para números de teléfono de 10 dígitos y otro para teléfonos de 7 dígitos. Si el teléfono no tiene ninguno de los dos, entonces el número de teléfono se retorna sin aplicar ningún formato.

Flujos de ejecución con condicionales: una advertencia

¿Cuál es el flujo de ejecución del siguiente fragmento de código?
// A
if (1) {
  // B
}
// C

Sólo hay dos:

Si 1 es verdadero: A-B-C

Si 1 es falso: A-C

¿Qué pasa con este?
// A
if (1) {
  // B
} else if (2) {
  // C
} else {
  // D
}
// E

Ahora quedan 3:

Si 1 es verdadero, independientemente de 2: A-B-E

Si 1 es falso y 2 es verdadero: A-C-E

Si 1 y 2 son falsos: A-D-E

¿Qué pasa con este?
// A
if (1) {
  // B
} else {
  // C
  if (2) {
    // D
  } else if (3) {
    // E
  }
}

if (4) {
  // F
}
// G

¡Es demasiado complicado! Realmente necesitas evitar códigos como el último. Si mantienes tus funciones pequeñas y granulares, es difícil terminar con un código mazacotudo, pero si alguna vez te encuentras con él, ¡detente! Respira hondo e intenta desenredar tu lógica en funciones y luego reescribe la función principal llamando esas funciones. Esto suele ser suficiente para terminar con un código más simple.

Orden de condiciones

Código 3 fizzBuzz, cuidado con el orden de las condiciones
String fizzbuzz(int n) {
  if (n % 3 == 0) {
    return "Fizz";
  } else if (n % 5 == 0) {
    return "Buzz";
  } else if (n % 3 == 0 && n % 5 == 0) { // n % 15 == 0
    return "FizzBuzz";
  } else {
    return Integer.toString(n);
  }
}

En fizzbuzz() tomas un int y devuelves un String basado en las siguientes reglas:

  • Si el número es múltiplo de 3, devuelve "Fizz"
  • Si el número es múltiplo de 5, devuelve “Buzz”
  • Si el número es múltiplo de 3 y 5 (que es lo mismo que decir que es múltiplo de 15), devuelve “FizzBuzz”
  • En todos los demás casos, simplemente devuelva el mismo número pero como un String.

Entonces el Código 3 parece correcto, ¿verdad?

Porque si esa condición es verdadera, significa que alguna de las condiciones anteriores, «n % 3 == 0» o «n % 5 == 0», es verdadera y por estar antes significa que las demás condiciones son ignoradas. Por tanto, el orden de las condiciones podría importar. El código correcto para fizzbuzz es:

Código 4 fizzBuzz, cuidado con el orden de las condiciones
String fizzbuzz(int n) {
  if (n % 3 == 0 && n % 5 == 0) { // n % 15 == 0
    return "FizzBuzz";
  } else if (n % 3 == 0) {
    return "Fizz";
  } else if (n % 5 == 0) {
    return "Buzz";
  } else {
    return Integer.toString(n);
  }
}

Switch vs If

Todo lo que se puede escribir con una declaración de cambio se puede escribir con un if, pero lo contrario no es cierto, ya que un switch solo puede verificar valores pero no otras expresiones booleanas, como el resultado del operador «<».

Código 5 - Versión original de teléfonoConFormato() usando Switch
String teléfonoConFormato(String teléfono) {
  return switch(teléfono.length()) {
    case 10 -> darFormato(teléfono, "(XXX) XXX-XXXX");
    case 7 -> darFormato(phoneNumber, "XXX-XXXX");
    default -> phoneNumber;
 };
}

Código 6 - Versión de teléfonoConFormato() usando if/else if/else
String teléfonoConFormato(String teléfono) {
  if (teléfono.length() == 10) {
    return darFormato(teléfono, "(XXX) XXX-XXXX");
  } else if (phoneNumber.length() == 7) {
    return darFormato(teléfono, "XXX-XXXX");
  } else {
  return teléfono;
  }
}

Veamos nuestra función darFormato() implementada en ambos estilos: Puede ser subjetivo, pero las declaraciones de switch parecen más claras en su intención. Si puedes escribir un condicional con un switch probablemente deberías hacerlo, pero recuerda que hay casos en que solo vamos a poder usar un if.

Regreso anticipado

Un gran uso de una declaración if es tener un retorno anticipado en una función. Anteriormente hiciste un ejercicio para implementar una función ellipsify() que agregaba puntos suspensivos a un String si el String tenía más caracteres que un límite determinado. Por ejemplo, si tenemos un título como "Programar con Java es divertido" pero nuestro diseño web admite menos caracteres, entonces transformaremos el título a "Programar con Java...". Por esa función, tomamos un String y una longitud máxima y devolvemos el String transformado. Sin embargo, hay un caso muy simple para esa función: si el String ya tiene menos caracteres que la longitud máxima, entonces podemos devolver el String sin modificarlo sin tener que ejecutar ninguna instrucción adicional de la función.

Código 7 - Función ellipsify() ahora con un early return
String ellipsify(String texto, case longitudMáxima) {
  if (texto.length() <= longitudMáxima) {
    return texto;
  }
  
  // código para manejar el caso más complejo
}

Al ser claro y frontal sobre cuál es el valor devuelto en una condición determinada, tu código es mucho más legible. Es posible que puedas tener varios valores anticipados.

Misión secundaria: múltiples retornos en una función

Existe una especie de debate religioso sobre si las funciones deberían tener retornos múltiples o no. Existe un punto de vista purista de que las funciones de programación deberían ser como funciones matemáticas y, por eso, solo deberían tener un retorno. Creo que una mejor respuesta sería: depende.

El argumento habitual es que tener un solo return hace que sea más fácil leer un código porque solo necesitas verificar un lugar para comprender lo que se devuelve. Pero a veces, para evitar devoluciones múltiples, el código necesita reasignar variables, lo que va en contra de solo hay una verificación para comprender lo que se devuelve, ya que necesitará verificar todos los lugares donde las variables tienen una asignación para comprender todos los valores posibles a ser retornados y en qué condiciones se retornan.

Reescribamos fizzbuzz() de una manera que evite múltiples devoluciones y veámoslo lado a lado con nuestro código original:

String fizzbuzz(int n) {
  var s = "";
  if (n % 3 == 0 && n % 5 == 0) {
    s = "FizzBuzz";
  }  else if (n % 3 == 0) {
    s = "Fizz";
  } else if (n % 5 == 0) {
    s = "Buzz";
  } else {
    s = Integer.toString(n);
  } 
  return s;
}
String fizzbuzz(int n) {
  if (n % 3 == 0 && n % 5 == 0) {
    return "FizzBuzz";
  } else if (n % 3 == 0) {
    return "Fizz";
  } else if (n % 5 == 0) {
    return "Buzz";
  } else {
    return Integer.toString(n);
  }
}

Incluso con múltiples devoluciones, el código de la derecha es mucho más legible que el de la izquierda. Si escriben su código de manera limpia, tener múltiples devoluciones no perjudica la legibilidad. No evites múltiples return para seguir las tendencias, comprueba qué es lo mejor para el código.

Otra versión que a veces se muestra como una opción para evitar múltiples return parece un retroceso aún mayopr en legibilidad.

String fizzbuzz(int n) {
  var s = "";
  if (n % 3 != 0 || n % 5 != 0) {
    s = Integer.toString(n);
  } else {
    if (n % 3 == 0) {
      s += "Fizz";
    } 
    if (n % 5 == 0) {
      s += "Buzz";
    }
}

Resumen

Para influir en el flujo de ejecución de una función tienes, fundamentalmente, dos construcciones:

  • Ciclos
  • Condicionales

Con los condicionales, puede escribir código que solo se ejecute bajo una determinada condición.

Las declaraciones de switch asignan un valor posible con algún código, pero solo se ejecutará el código de una cláusula, la cláusula con el valor de la variable que se utiliza o el código predeterminado si no coincide ninguna cláusula.

Si las declaraciones son más complejas, garantizan que el código de su bloque solo se ejecutará si su condición es verdadera, que puede ser cualquier expresión booleana. Se debe tener mucho cuidado para que el código dentro de una declaración if/else sea fácil de leer.

Las declaraciones If son excelentes para proporcionar retornos tempranos en funciones, lo que mejora la legibilidad y hace que sea muy sencillo comprender cómo se comporta su código en condiciones especiales.

Ejercicios

E7.1

En Matemáticas todo número entero tiene al menos 2 divisores: 1 y el propio número. Estos se llaman divisores obvios. Los números primos son números mayores que 1 que no tienen ningún otro divisor salvo los divisores obvios. Recuerda que si «a» es múltiplo de «b», entonces «b» es divisor de «a». Escribe una función esPrimo para comprobar si un número es primo.

assertFalse(esPrimo(1));
assertTrue(esPrimo(7));
assertFalse(esPrimo(100));

E7.2

Los dígitos de verificación son un mecanismo simple para detectar errores cuando se digita un número. Por ejemplo supongamos que generamos un identificador "123", luego aplicamos una fórmula cuyo resultado es un dígito adicional, supongamos "6", entonces el identificador completo sería "1236", a veces escrito como "123-6". Ahora la siguiente vez que digitemos ese identificador en un sistema, este podría validarlo, separando "123" de su dígito de verificación "6", calcular el número de verificación de "123" y revisar que coincide con el "6" obtenido. Esto significa que si una persona se equivoca al digitar el número, por ejemplo intercambiando 2 dígitos de lugar, el número de verificación no coincidirá y así se sabrá que hubo un error al digitarlo. Para los números UPC (Universal Product Code) el algoritmo para hallar el dígito de verificación es:

  • Separa el último dígito que es el número de verificación.
  • Suma los dígitos en posiciones impares (el primer dígito, el tercer dígito, etc) y el resultado multiplícalo por 3.
  • Suma los dígitos en posiciones pares (el segundo dígito, el cuarto dígito, etc)
  • Suma los resultados de los dos pasos anteriores
  • Encuentra el resultado de aplicar la operación % 10 a la suma. Llamémoslo M
  • Si M es 0, ese es el número de verificación. Si no es 0, entonces el dígito de verificación es 10 - M

Crea una función que reciba un número dado como una cadena y verifique si es un número UPC válido verificando el dígito de verificación como se explica en las reglas anteriores.

assertTrue(upcValido("116"));
assertFalse(upcValido("111"));
assertTrue(upcValido("019"));
assertTrue(upcValido("107"));
assertTrue(upcValido("240"));
assertTrue(upcValido("036000241457"));

E7.3

Escribe una función contraseñaFuerte que tome un String y devuelva verdadero solo si una contraseña cumple con los siguientes criterios:

  • Tener al menos una letra mayúscula
  • Tener al menos un número
  • Tener al menos un carácter no alfanumérico
  • Tener al menos 8 caracteres.
assertTrue(contraseñaFuerte("Abc123$_"));
assertFalse(contraseñaFuerte("Abc12345"));
assertFalse(contraseñaFuerte("abc123$_"));
assertFalse(contraseñaFuerte("Abcdef$_"));
assertTrue(contraseñaFuerte("ABC123$_"));

E7.4

CamelCase es una forma de escribir identificadores con más de una palabra separándolas usando letras mayúsculas. SnakeCase, por otro lado, usa únicamente letras minúsculas pero utiliza guiones bajos para separar las palabras. Escriba una función camelCaseToSnakeCase que tome una String que sea un identificador válido escrito en CamelCase y la devuelva en Snake Case. Además, escribe una función snakeCaseToCamelCase para la operación inversa

assertEquals("holaMundo", snakeCaseToCamelCase("hola_mundo"));
assertEquals("unoDosTres", snakeCaseToCamelCase("uno_dos_tres"));
assertEquals("identificador", snakeCaseToCamelCase("identificador"));
assertEquals("último_dígito", camelCaseToSnakeCase("últimoDígito"));
assertEquals("es_vocal", camelCaseToSnakeCase("esVocal"));
assertEquals("hola_mundo_cruel", camelCaseToSnakeCase("holaMundoCruel"));

© 2022 Carlos Obregón. This work is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0)

Scroll to Top