Ciclos

Código 1 - Ciclo while
int sumaDígitos(int n) {
  var suma = 0;
  while (n > 0) {
  var últimoDígito = n % 10;
      suma = suma + últimoDígito;
      n = n / 10;
  }
  return suma;
} 
Input
> sumaDígitos(123)
Output
6

¡El Código 1 muestra el uso de un ciclo! Antes de los ciclos, el flujo de ejecución de un método era: se ejecuta la primera instrucción, luego la segunda, luego la tercera, y así sucesivamente. Los ciclos cambian eso y te permiten repetir un conjunto de declaraciones un número controlado de veces. Hay varias formas de ciclos, sumaDígitos() usa un ciclo while. En un ciclo while, se especifica la condición que debe ser verdadera para que se ejecuten las declaraciones que estén contenidas en su bloque.

La línea 03 es el comienzo del ciclo, indicado por la palabra clave while y entre paréntesis la condición del ciclo: que el parámetro «n» debe ser mayor que 0. Las líneas 04 a 06 son las declaraciones que forman parte del ciclo, dado que están dentro del bloque del ciclo, y se ejecutarán hasta que la condición sea falsa. La parte más importante de ese ciclo es la línea 06 en la que hay un ajuste a la variable utilizada en la condición, sin él el ciclo se ejecutará para siempre.

La línea 04 utiliza la operación «n % 10» como una forma de obtener el último dígito de un valor entero. La línea 06 utiliza la operación «n / 10» como una forma de obtener un número resultante de eliminar el último dígito de un número entero. En el pasado hicimos ejercicios para demostrar este comportamiento. El truco no funcionaría si «n» fuera un doble o si escribieras «n / 10.0»

Para entender lo que sucede en el código, tengamos una tabla con el valor durante cada iteración para sumaDígitos(123):

# Iteración n n % 10 n / 10 suma
0 123 n/a n/a 0
1 123 3 12 3
2 12 2 1 5
3 1 1 0 6
4 0 n/a n/a 6

Operadores compuestos

Código 2 - sumaDígitos() usando operadores compuestos
int sumaDigitos(int n) {
  var suma = 0;
  while (n > 0) {
  var últimoDígito = n % 10;
    suma += últimoDígito; // sum = sum + lastDigit
    n /= 10; // n = n / 10
  } 
  return suma;
} 

El código 2 utiliza operadores compuestos como atajo para asignaciones cuando se actualiza el valor de una variable. Este tipo de operación es muy común en los ciclos, por lo que tiene sentido presentarlas ahora. En ciclos los operadores compuestos más usados son los relacionados con las operaciones numéricas: +, -, /, * y %. También son útiles los operadores compuestos con operaciones lógicas !, && y ||. Estos son los operadores compuestas de uso más general.

Entre estos, hay dos que son muy comunes: ajuste sumando 1 o restando 1. Pero vienen en dos formas:

Operadores Compuestos Post Pre
n += 1 n++ ++n
n -= 1 n-- --n

La diferencia entre un post-incremento o una post-disminución y un pre-incremento o una pre-disminución solo es importante cuando los usas en la misma línea con otros operadores, incluidas las asignaciones. Este uso fue muy popular cuando la economía de espacio en la pantalla hizo atractivo meter tanto código en la menor cantidad de caracteres posible. Ahora que esto ya no es tan importante como antes, no tiene sentido hacer que el código sea más difícil de leer sólo para ahorrar algo de espacio en la pantalla.

Ciclos For

Código 3 - sumaRangoNúmeros() con ciclo for
int sumaRangoNumeros(int límite) {
  var suma = 0;
  for (var actual = 1; actual <= límite; ++actual) {
    suma += actual;
  } 
  return suma;
} 
Input
> sumaRangoNúmeros(5) // 1 + 2 + 3 + 4 + 5
> sumaRangoNúmeros(3)
Output
15
6         

El ciclo más usado es el ciclo for. En el ciclo for tienes las 3 partes de un ciclo: inicialización, condición y ajuste, en una misma línea de código. En el Código 03, la función sumaRangoNúmeros() suma todos los números enteros entre 1 y el límite dado. La línea 02 crea una variable «suma» para acumular la suma de los números. El ciclo for se encuentra entre las líneas 03 y 06, empieza con la palabra reservada for y entre paréntesis las 3 partes del ciclo: inicialización, condición y ajuste. Para controlar el ciclo inicializamos una variable «actual» en 1. Esta variable también nos sirve para ir llevando la suma. La condición es que «actual» sea menor o igual a «limite». El ajuste es incrementar en 1 la variable «actual». Por lo tanto el ciclo se va a realizar tantas veces como el valor de la variable «limite», si esa variables es 5 se realizará cinco veces..

No te dejes engañar por la inicialización/condición/ajuste que está en la misma línea: la inicialización sólo ocurre una vez antes de que se ejecute el bloque del ciclo. La condición se verifica una vez por iteración, antes de ejecutar el bloque del ciclo. El ajuste siempre al terminar una iteración, antes de verificar la condición.

Un ciclo for no tiene ninguna parte obligatoria, nada impide que escribamos sumaDígitos() así:

Código 4 - ciclos for sin inicialización
int sumOfDigits(int n) {
  var sum = 0;
  for (; n > 0; n /= 10) {
  var lastDigit = n % 10;
    sum += lastDigit;
  } 

  return sum;
} 

Pero, ¡seguramente es una mala idea! El Código 4 parece ser una degradación del Código 2 en términos de legibilidad.

Strings

Código 5 - los ciclos son los mejores amigos de los Strings.
String espaciar(String palabra) {
  var builder = new StringBuilder();
  for (var i = 0; i < palabra.length(); ++i) {
  var letra = palabra.charAt(i);
    builder.append(letra);
    builder.append(" ");
  }

  var espaciado = builder.toString();
  return espaciado;
} 
Input
> espaciar("Java")
> espaciar("Programming")
Output
"J a v a "
"P r o g r a m m i n g "

Recuerda que un String es una secuencia de caracteres. Cada carácter está asociado con un índice. Los ciclos for son una excelente manera de recorrer una cadena, carácter por carácter. En la línea 04 usamos el método charAt() para acceder al carácter actual que se está atravesando. Y después de agregar el caracter a un StringBuilder, agregamos un espacio para generar el espaciado

Ciclos For vs Ciclos While

¿Cuándo usar ciclos for y cuándo usar ciclos while? Lo primero que es importante decir es que no hay ninguna diferencia en funcionalidad. Todo código que utilice un ciclo for se puede reescribir como un código que use un ciclo while y viceversa, aunque puede requerir algunos cambios menores.

Veamos la función sumaRangoNúmeros reescrita con un ciclo while:

Código 6 - sumaRangoNúmeros con un ciclo while
int sumaRangoNúmeros(int límite) {
  var suma = 0;
  var actual = 0;
  while (actual <= límite) {
      suma += actual;
      ++actual;
  } 
  return suma;
} 

En este caso, la legibilidad se ve afectada negativamente porque las declaraciones que definen el ciclo están dispersas; el control del ciclo está en las líneas 03, 04 y 06 mientras que antes eran todos parte de la definición del ciclo for.

Código 7 - esCapicúa(int)
boolean esCapicua(int número) {
  var n = Integer.toString(número);
  var i = 0;
  var j = n.length() - 1;
  while (i < j && n.charAt(i) == n.charAt(j)) {
    ++i;
    –-j;
  } 
  return i >= j;
} 

Un número capicúa es un número que se lee igual de izquierda a derecha que de derecha a izquierda, es el equivalente a un palíndromo para las palabras salvo que es un problema más sencillo porque no tenemos que lidiar con caracteres diferentes de dígitos.

Código 07 tiene una implementación que verifica que un número sea capicúa. La idea general es crear un String con el número, ya que es más fácil de analizar el número como un String que como un int. Inicializamos un índice en el primer caracter y otro en el último caracter. Ahora hacemos un ciclo que se ejecute mientras los índices no se hayan cruzado y mientras que los caracteres sean los mismos. En caso de que sea cierto, ajustamos los índices, moviendo el índice de la izquierda hacia la derecha y el de la derecha hacia la izquierda.

Cuando el ciclo se termina podemos tener la garantía de que el ciclo terminó porque los índices se cruzaron, en cuyo caso todos los caracteres analizados fueron iguales y el número es capicúa, o terminó porque encontró 2 caracteres que debían ser iguales pero no lo son. Así que al final simplemente verificamos la razón por la cuál terminó: si los dos índices se cruzaron, es decir que el índice que empezaba en 0 termina siendo igual o mayor al otro índice, entonces todas las comparaciones con el operador == fueron exitosas. Si esa condición no es cierta, entonces alguna de esas comparaciones fallaron.

En este caso el ciclo while ofrece mucha mejor legibilidad dado que las variables «i» e «j» deben ser usadas por fuera del ciclo. Sin embargo se podría reimplementar con un ciclo for solo que necesitaría elementos adicionales que no hemos visto.

Ciclos Anidados

Código 8 - Un cuadrado con *
String cuadrado(int tamaño) {
   var builder = new StringBuilder();
   for (var hilera = 1; hilera <= tamaño; ++hilera) {
   for (var columna = 1; columna <= size; ++columna) {
    builder.append("*");
   } 
    builder.append(System.lineSeparator());
   } 
   return builder.toString();
} 
Input
> cuadrado(2)


> cuadrado(3)


Output
**
**

***                         
***
***

No hay restricciones sobre lo que puedes hacer dentro de un ciclo. También puedes usar un ciclo. Si lo piensas bien, un ciclo dentro de un ciclo es solo un ciclo interno que se repite varias veces y está controlado por un ciclo externo. El código 8 construye una figura cuadrada usando "*". El loop exterior controla el número de filas del cuadrado, mientras que el ciclo interior controla el aspecto de una hilera. Después de cada hilera insertamos un carácter de salto de línea, una de las maneras más sencillas de hacer este carácter es con System.lineSeparator().

Si bien anidar ciclos es válido, en la gran mayoría de casos es seguramente mejor hacer una función que encapsule el ciclo interno. Eso potencialmente mejora la legibilidad:

Código 9 - cuadrado() con 2 funciones
String cuadrado(int tamaño) {
  var builder = new StringBuilder();
  for (var hilera = 1; hilera <= size; ++hilera) {
      builder.append(hilera(tamaño));
      builder.append(System.lineSeparator());
  } 
  return builder.toString();
  } 
String hilera(int tamaño) {
  var rowBuilder = new StringBuilder();
  for (var columna = 1; columna <= tamaño; ++columna) {
    rowBuilder.append("*");
  } 
  return rowBuilder.toString();
} 

Si tienes ciclos anidado, considera mover parte del código interno a una función. Esto mejora la legibilidad de tu código y si la función no se comporta como esperas, puedes probar el ciclo interno por separado del ciclo externo y encontrar el problema más rápidamente. No todos los ciclos anidados pueden beneficiarse al extraer el ciclo interno de una función, ¡pero vale la pena intentarlo!

Strings de múltiples líneas

Un tema que no habíamos visto antes porque no teníamos un buen propósito eran los literales para Strings de múltiples líneas, llamados Text Blocks. Existe en Java una manera de escribir un String de múltiples líneas sin tener que hacer ninguna operación especial. Miremos un ejemplo:

Código 10
var cuadrado = """
               ***
               ***
               ***
               """;

La variable cuadrado es un String que tiene 3 saltos de línea pero pudimos representarlo simplemente con un literal. La manera sencilla de escribir Strings de múltiple líneas es:

  • Empieza con 3 comillas dobles en una sola línea
  • En las siguientes líneas escribe el contenido del String pero alineado verticalmente con el delimitador anterior.
  • Cierra con las mismas 3 comillas dobles, también alineadas.

¿Por qué es ahora importante? Si quieres hacer una prueba unitaria para la función cuadrado() que vimos anteriormente, la manera más sencilla es con un String de múltiples líneas:

La manera más sencilla es reemplazar el salto de línea de Windows al estándar:

Código 11
assertEquals("""
             ***
             ***
             ***
             """.replace("\r\n", "\n"), cuadrado(3));

Por motivos prácticos el material no incluye estos replace() pero debes tenerlos en cuenta en tus pruebas unitarias en tu ambiente local.

Ciclos (Accidentalmente) Infinitos

Código 12
int cuantasIteraciones() {
  var cuenta = 0;
  for (var n = 1; n != 100; n += 2) {
    ++cuenta;
  } 
  return cuenta;
} 

Dado este código, ¿qué valor crees que se devolvería? Bueno, ¡el hecho es que el código nunca se detendrá! ¿Puedes encontrar la razón?

¿Qué sucede si escribes un ciclo infinito? Si tienes suerte, habrá un error dentro del loop que terminará el programa. De lo contrario, tu programa simplemente ejecutará el código dentro del loop hasta que se detenga por la fuerza la ejecución de tu programa.

Vigila tus ciclos para evitar ciclos infinitos.

Logs

Código 13 - Logs con System.out
int sumaDígitos(int n) {
  var suma = 0;
  while (n > 0) {
  var últimoDígito = n % 10;
    suma += últimoDígito;
    System.out.println(suma);
    n = n / 10;
  } 
  return suma;
} 
Input
> sumaDígitos(123)



Output
3
5
6
==> 6

Cuando estás haciendo software, a veces es importante dejar un registro de lo que estás haciendo en tus funciones para que si luego hay un error ya tengas algunos datos recopilados sobre lo sucedido. Hay herramientas para hacer esto a nivel empresarial que están fuera del alcance de este tutorial, pero hay una herramienta que puede ayudarte a entender tus programas sin tener que usar un debugger: imprimir en la salida estándar de la computadora, dentro de tus funciones.

La línea 06 del Código 13 utiliza System.out.println() dentro de un ciclo para verificar cómo cambia la suma a través de las iteraciones. Si bien ese código es sencillo, y quizás no se beneficie mucho del log, en general imprimir a la pantalla dentro de un ciclo puede ayudar a entender lo que hace el código:

No hay límite en cuántas veces puedes imprimir a la salida estándar, ni dónde puedes invocar una función para hacerlo, así que úsala tanto como quieras. Sólo recuerda que existen mejores herramientas para hacer estos logs, pero para ejercicios simples, System.out está bien.

Ejercicios

E6.1

Escribe una función que reciba un String y lo reverse.

assertEquals("avaJ", reversar("Java"));
assertEquals("nuf si avaJ", reversar("Java is fun"));

E6.2

¿Recuerdas los métodos indexOf(char) e indexOf(char, int) de la clase String? Utilízalos para crear una función que cuente cuántas veces esta una subcadena en un String dado. Los caracteres de una subcadena no pueden ser usados en otra, mira el último ejemplo: la respuesta es 1 porque después de encontrar el primer "ss", el algoritmo debe buscar la siguiente subcadena después del final de esa subcadena.

assertEquals(2, conteo("abracadabra", "abr"));
assertEquals(4, conteo("110101110101100", "01"));
assertEquals(3, conteo("xoxox", "x"));
assertEquals(1, conteo("sssa", "ss"));

E6.3

Escribe una función que tome un String que represente una oración y cuente cuántas palabras contiene. Puedes asumir que la oración solo constará de caracteres alfanuméricos y 1 espacio separará las palabras que contiene.

assertEquals(1, cuentaPalabras("one"));
assertEquals(3, cuentaPalabras("one two three"));

E6.4

Escribe una versión sobrecargada de sumaRangoNúmeros que tome dos parámetros y retorne la sumatoria de los números entre ese rango incluyendo los extremos. Luego vuelve a implementar la función sumaRangoNúmeros(int) para usar esta nueva función.

assertEquals(75, sumaRangoNumeros(10, 15)); // 10 + 11 + 12 + 13 + 14 + 15
assertEquals(15, sumaRangoNumeros(1, 5))

E6.5

La "dígitos suma" de un número es el número resultante después de aplicar esta operación: suma los dígitos de un número. Si el resultado tiene más de un dígito, es mayor o igual a 10, repite la operación. Y este procedimiento se repite hasta tener un número de un solo dígito. Escribe una función para calcular la dígitos suma de un número entero. Recuerda re utilizar la función sumaDígitos() que vimos en este capítulo.

assertEquals(1, dígitosSuma(1234)); // 1 + 2 + 3 + 4 = 10 -> 1 + 0 = 1
assertEquals(6, dígitosSuma(555)); // 5 + 5 + 5 = 15 -> 1 + 5 = 6

E6.6

Escribe una función serieArmónica() que dado un número «n», calcule la suma de los términos de la serie.1 + ½ + ⅓ + … 1/n, por ejemplo si la función es llamada con «n» igual a 6, calcule la suma 1 + ½ + ⅓ + ¼ + ⅕ + ⅙.

assertEquals(1.0, serieArmonica(1));
assertEquals(1.5, serieArmonica(2)); // 1 + ½
assertEquals(2.0833, serieArmonica(4), 1e-4);

E6.7

Escribe una función que reciba un String que representa un Id alfanumérico y retorne el número de ceros con los que empieza el Id.

assertEquals(0, cerosIniciales("ABC"));
assertEquals(2, cerosIniciales("00ABC"));
assertEquals(1, cerosIniciales("0ABC00"));
assertEquals(5, cerosIniciales("00000"));

E6.8

En numerología el número de la vida se calcula como el número resultante de hallar la dígitos suma (mira el ejercicio E5.5) de tu fecha de nacimiento, interpretada sólo como números. En Java existe una clase para representar fechas de este modo: LocalDate. Escribe una función que reciba una fecha y retorne su número de vida.

assertEquals(5, numeroVida(LocalDate.of(2000, 1, 20)));
assertEquals(6, numeroVida(LocalDate.of(1945, 10, 31)));

E6.9

Escribe una función que cree un String cuadrado con un tamaño dado, utilizando los caracteres "*" y "." altercados. Las hileras impares empiezan con "*" y las pares con ".".

assertEquals("""
            *.*
            .*.
            *.*
            """, patronAjedrez(3));
assertEquals("""
            *.*.*.
            .*.*.*
            *.*.*.
            .*.*.*
            *.*.*.
            .*.*.*
            """, patronAjedrez(6));

E6.10

Implementa un método que dado un número entero como parámetro, retorne un String con todas las parejas posibles de números entre 1 y el número dado. El resultado debe seguir estas reglas:

  • Cada pareja se escribe así: (#, #)
  • Las parejas se listan fila por fila: primero todas las del 1, luego todas las del 2, y así sucesivamente
  • Entre cada pareja va un |, y el String también empieza y termina con |
  • assertEquals("|(1, 1)|(1, 2)|(2, 1)|(2, 2)|", coordenadas(2));
    assertEquals("|(1, 1)|(1, 2)|(1, 3)|(2, 1)|(2, 2)|(2, 3)|(3, 1)|(3, 2)|(3, 3)|", coordenadas(3));

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

    Scroll to Top