Control de flujo de Java: bucles for y for-each

Las declaraciones condicionales y los bucles son una herramienta muy importante en la programación. No hay muchas cosas que podamos hacer con un código que solo puede ejecutarse línea por línea...

Introducción

Las declaraciones condicionales y los bucles son una herramienta muy importante en la programación. No hay muchas cosas que podamos hacer con un código que solo puede ejecutarse línea por línea.

Eso es lo que significa "control de flujo": guiar la ejecución de nuestro programa, en lugar de dejar que se ejecute línea por línea, independientemente de cualquier factor interno o externo. Cada lenguaje de programación admite alguna forma de control de flujo, si no explícitamente a través de ifs y fors o declaraciones similares, implícitamente nos brinda las herramientas para crear tales construcciones, es decir, los lenguajes de programación de bajo nivel generalmente logran ese efecto con una gran cantidad de comandos go-to.

Los bucles eran un concepto utilizado mucho antes de que existiera la programación informática, pero la primera persona en utilizar un bucle de software fue Ada Lovelace, comúnmente conocida por su apellido de soltera - Byron, mientras calculaba [Números de Bernoulli](https://en .wikipedia.org/wiki/Bernoulli_number), allá por el siglo XIX.

En Java, hay varias formas de controlar el flujo del código:

El bucle for

Los bucles for se usan típicamente cuando el número de iteraciones es "fijo" de alguna manera. O sabemos exactamente cuántas veces se ejecutará el bucle o tenemos una mejor idea que "hasta que n se convierta en m".

Los bucles for se usan a menudo con matrices:

1
2
3
for (initialization; terminationCondition; update) {
    // Code here...
}

Y una implementación simple:

1
2
3
4
5
int[] arr = {1,2,3,4,5,6,7,8,9};

for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}

Esta es una declaración for muy simple, y así es como se ejecuta:

  1. Establezca la variable local i en 0
  2. Compruebe si i es menor que arr.length, si es así, proceda dentro del bloque
    2.1. Print out arr[i]
    2.2. Increment i by 1, go to step 2.
  3. Si i no es menor que arr.length, proceda después del bloque

Tan pronto como el paso 2 encuentra que i es mayor o igual que arr.length, los bucles detienen su ejecución y Java continúa la ejecución desde la línea posterior al bloque de bucle.

Nota: Aquí, la ubicación del operador de incremento (++) no es importante. Si nuestro paso de actualización fuera ++i, nada cambiaría ya que el paso de actualización siempre se ejecuta después del código en el bloque de bucle for.

Este código, por supuesto, imprimiría:

1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

Hay tres bloques diferentes usados ​​en el bucle for que nos permiten hacer esto: el bloque de inicialización, la condición de finalización y el paso de actualización.

Bloque de inicialización {#bloque de inicialización}

Se utiliza un bloque de inicialización en el bucle for para inicializar una variable. En nuestro ejemplo, queríamos que la variable i comenzara en 0, ya que 0 es el primer índice de una matriz.

Este bloque se ejecuta solo una vez, al comienzo del bucle for. También podemos definir múltiples variables del mismo tipo aquí:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Single variable
for (int i = 0; i < 10; i++) {
    // Code
}

// Multiple variables
for (int i = 10, j = 25; i < arr.length; i++) {
    // Code
}

// Multiple variables
for (int i = 10, double j = 25.5; i < arr.length; i++) {
    // WON'T compile because we used two different types - int and double
}

Las variables inicializadas dentro de la declaración for solo se pueden usar dentro del bloque for. Acceder a ellos fuera de su alcance dará como resultado una excepción, como se esperaba:

1
2
3
4
for (int i = 0; i < 10; i++) {
    System.out.println(i);
}
System.out.println(i);

La variable i ha sido referenciada fuera del alcance:

1
2
3
4
5
6
MyClass.java:6: error: cannot find symbol
System.out.println(i);
                   ^
  symbol:   variable i
  location: class MyClass
1 error

Nota: El código en el bloque de inicialización es opcional y no es necesario incluirlo. Sin embargo, el bloque tiene que ser. Por lo tanto, podemos escribir algo como esto:

1
2
3
4
5
int i = 0;
for (; i < 10; i++) {
    System.out.println(i);
}
System.out.println("\ni variable is " + i);

Y daría como resultado el mismo resultado que si el bloque de inicialización estuviera allí, excepto que la variable i ya no está fuera del alcance después de ejecutar el bucle for:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
0
1
2
3
4
5
6
7
8
9

i variable is 10

El bloque de inicialización está técnicamente ahí, ya que incluimos el final ;, pero no hay código dentro.

Condición de rescisión {#condición de rescisión}

La condición de terminación le dice al bucle for que ejecute el código siempre que sea verdadero. Si la condición de terminación se evalúa como falsa, se saltan el paso de actualización y el resto del bucle for. Sólo puede haber una condición de terminación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
for (int i = 0; i < 10; i++) {
    System.out.println("i = " + i + " | i < 10 is true");
}

// Compiles as there's only one termination condition,
// although there's two operators - two relational and one logical
for (int i = 6; i < 10 & i > 5; i++) {
    System.out.println("i = " + i + " | i < 10 is true");
}

// WON'T compile, multiple separate termination conditions
for (int i = 0; i < 10, i > 5; i++) {
    System.out.println("i = " + i + " | i < 10 is true");
}

La salida del primer bucle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
i = 0 | i < 10 is true
i = 1 | i < 10 is true
i = 2 | i < 10 is true
i = 3 | i < 10 is true
i = 4 | i < 10 is true
i = 5 | i < 10 is true
i = 6 | i < 10 is true
i = 7 | i < 10 is true
i = 8 | i < 10 is true
i = 9 | i < 10 is true

Al llegar a ‘10’, la condición ‘i < 10’ ya no es ‘verdadera’ y el código deja de repetirse.

Y la salida del segundo bucle:

1
2
3
4
i = 6 | i < 10 is true
i = 7 | i < 10 is true
i = 8 | i < 10 is true
i = 9 | i < 10 is true

Nota: La condición de rescisión también es opcional. Es válido definir un bucle for sin una condición de terminación. Sin embargo, excluir la condición de terminación hará que el código se repita infinitamente, o hasta que ocurra un StackOverflowError.

1
2
3
for (int i = 0; ; i++) {
    System.out.println("Looping forever!");
}

Aunque no hay código en la condición de finalización, debe incluir un punto y coma para marcar que ha decidido dejarlo vacío; de lo contrario, el código no se compilará.

1
2
3
4
MyClass.java:3: error: ';' expected
    for (int i = 0; i++) {
                       ^
1 error

Actualizar paso

El paso de actualización generalmente disminuye/incrementa algún tipo de variable de control (en nuestro caso, i) después de cada iteración del ciclo. Esencialmente, se asegura de que nuestra condición de terminación se cumpla en algún momento; en nuestro caso, incrementa i hasta que llega a 10.

El paso de actualización solo se ejecutará si la condición de terminación se evalúa como “verdadera” y después de que se haya ejecutado el código en el bloque de bucle “for”. Puede tener varios pasos de actualización si lo desea, e incluso puede llamar a métodos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
for (int i = 0; i < 10; i++) {
    // Code
}

int j = 10;

for (int i = 0; i < 10; i++, j--) {
    System.out.println(i + " | " + j);
}

// Multiple variables
for (int i = 10; i < arr.length; i++, doSomething()) {
    System.out.println("Hello from the for loop");
}

public static void doSomething() {
    System.out.println("Hello from another method");
}

Y la salida de este bucle sería:

1
2
3
4
5
6
7
8
Hello from the for loop
Hello from another method!
Hello from the for loop
Hello from another method!
Hello from the for loop
Hello from another method!
Hello from the for loop
Hello from another method!

Esto también confirma que el paso de actualización se ejecuta después del código dentro del bloque.

Nota: El paso de actualización también es opcional, al igual que el bloque de inicialización y la condición de terminación. Puede ser reemplazado por código dentro del ciclo for, y también podríamos incrementar/decrementar la variable de control antes del código de esa manera.

¿Bucle for vacío?

Dado que los tres bloques del bucle for son técnicamente opcionales, podríamos, sin problema, escribir este bucle for:

1
2
3
4
5
6
7
8
int i = 0;

// This will compile, all blocks are "present" but no code is actually there
for (;;) {
    if (i < 10)
        System.out.println(i);
    i++;
}

If you take a closer look, this really looks similar to a mientras bucle:

1
2
3
4
5
6
int i = 0;

while (i < arr.length) {
    System.out.println(i);
    i++;
}

Todo lo que se puede hacer con un bucle ‘while’ se puede hacer con un bucle ‘for’ y viceversa, y la elección entre ellos se decide por la legibilidad y la conveniencia.

Los bucles for y las tres partes proporcionan una visión general clara de cuáles son las condiciones del bucle, dónde comienza, cómo cambia el estado y cuándo deja de iterar. Los hace muy concisos y fáciles de manipular.

Dado esto, se recomienda evitar cambiar la variable de control (i en nuestro caso) fuera de los paréntesis después de for, a menos que sea absolutamente necesario.

Bucles anidados for

Los bucles for anidados también son muy comunes, especialmente cuando se itera a través de matrices multidimensionales (matrices que tienen otras matrices como elementos):

1
2
3
4
5
6
7
8
int[][] multiArr = {{1,2,3},{4},{5,6}};

for (int i = 0; i < multiArr.length; i++) {
    for (int j = 0; j < multiArr[i].length; j++) {
        System.out.print(multiArr[i][j] + " ");
    }
    System.out.println();
}

Ejecutar este código produciría:

1
2
3
1 2 3
4
5 6

Bucle mejorado for

El for mejorado o for-each es un tipo especial de bucle for que se puede usar para cualquier objeto que implemente la interfaz Iterable y las matrices.

Está diseñado para recorrer los elementos en orden, uno tras otro, desde el principio hasta el final. Esto difiere del ciclo for tradicional en que podríamos haber dado el paso que quisiéramos; podríamos haber, por ejemplo, impreso todos los demás elementos:

1
2
3
4
5
int[] arr = {1,2,3,4,5,6,7,8,9};

for (int i = 0; i < arr.length; i+=2) {
    System.out.println(arr[i]);
}

Observe que incrementamos i en 2 cada vez. Esto habría impreso lo siguiente:

1
2
3
4
5
1
3
5
7
9

En cambio, con for-each iteramos a través de todos los elementos usando la siguiente sintaxis:

1
2
3
for (ElementType localVar : somethingIterable) {
    // Code
}

El bucle almacena un elemento tras otro en la variable localVar, hasta que no quedan más elementos. Fue creado para evitar hacer bucles for tradicionales que pasaban por arreglos/colecciones secuencialmente. Ya no necesitamos especificar un contador, establecer una posición inicial y una posición final, indexar manualmente la matriz y ya no tenemos que preocuparnos por los límites, todo lo cual puede ser muy tedioso de escribir.

Todo esto ha sido automatizado a través de for-each.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
List<String> list = new ArrayList<>(Arrays.asList("a","b","c"));

for (String s : list) {
    System.out.println(s);
}

// The traditional for syntax...
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}
// ...or...
for (Iterator<String> i = list.iterator(); i.hasNext();) {
    System.out.println(i.next());
}

El primer bucle es el más limpio y requiere menos mantenimiento por nuestra parte. Este fragmento de código podría leerse efectivamente como: "para cada String s en la lista de cadenas list, haga algo con s.

Bucles para-cada anidados

for-each también es compatible con matrices multidimensionales, uno de nuestros bucles for anteriores imprimió elementos de una matriz bidimensional; así es como se vería usando for-each:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int[][] multiArr = {{1,2,3},{4},{5,6}};

// Keep in mind that x is an array, since multiArr
// is an array of arrays
for (int[] x : multiArr) {
    for (int y : x) {
        System.out.print(y + " ");
    }
    System.out.println();
}

De manera similar, podemos usarlo para iterar colecciones anidadas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ArrayList<String> stronglyTyped = new ArrayList<>();
stronglyTyped.add("Java");
stronglyTyped.add("Go");
stronglyTyped.add("Harbour");
stronglyTyped.add("Haskell");

ArrayList<String> weaklyTyped = new ArrayList<>();
weaklyTyped.add("C++");
weaklyTyped.add("C");
weaklyTyped.add("JavaScript");

ArrayList<ArrayList<String>> programmingLanguages = new ArrayList<ArrayList<String>>();
programmingLanguages.add(stronglyTyped);
programmingLanguages.add(weaklyTyped);

La lista de matrices programmingLanguages contiene otras dos listas de matrices: stronglyTyped y weaklyTyped.

Para recorrerlos, simplemente escribiríamos:

1
2
3
4
5
for (ArrayList<String> languages : programmingLanguages) {
    for (String language : languages) {
        System.out.println(language);
    }
}

La salida:

1
2
3
4
5
6
7
Java
Go
Harbour
Haskell
C++
C
JavaScript

Modificando valores durante for-each

Es importante tener en cuenta que puedes cambiar los valores de los elementos que estás iterando. Por ejemplo, en el ejemplo anterior, si cambiamos System.out.println(language) con System.out.println(language.toUppercase()), nos recibiría con:

1
2
3
4
5
6
7
JAVA
GO
HARBOUR
HASKELL
C++
C
JAVASCRIPT

Esto se debe a que estamos tratando con objetos. Al iterar a través de ellos, estamos asignando sus variables de referencia a la cadena language. Llamar a cualquier cambio en la variable de referencia “idioma” también se reflejará en la original. En el caso de las cadenas, es posible que en realidad no afecte a los objetos debido al grupo de cadenas, pero entiende el punto.

Esto, sin embargo, no sucede con las variables primitivas, ya que con ellas, el valor simplemente se copia en la variable a la que estamos accediendo. Entonces, cambiarlo no afectará las variables originales.

Bono: forEach Método

Si bien no es el enfoque de este artículo, debemos mencionar que existe una nueva forma de recorrer las listas desde Java 8. El método .forEach() ahora está disponible, que se puede combinar con [expresiones lambda](/expresiones -lambda-en-java/) para bucles de una sola línea.

En algunos casos, esto se puede usar en lugar del bucle for-each, lo que simplifica aún más la iteración:

1
list.forEach(x -> System.out.println(x));

Conclusión

El control de flujo en el código es esencial en casi todas las aplicaciones. Las declaraciones que alteran el flujo del código son bloques de construcción fundamentales y cada aspirante a desarrollador debe tener el control completo y ser consciente de cómo funcionan.

Los bucles for y for-each son buenos para ejecutar un bloque de código un número conocido de veces, a menudo con una matriz u otro tipo de iterable. También son posibles bucles más complejos, como incrementar el índice en 2, o incrementando y verificando múltiples variables.

Licensed under CC BY-NC-SA 4.0