JavaRush /Blog Java /Random-ES /For y For-Each Loop: una historia de cómo iteré, fui iter...
Viacheslav
Nivel 3

For y For-Each Loop: una historia de cómo iteré, fui iterado, pero no fui iterado

Publicado en el grupo Random-ES

Introducción

Los bucles son una de las estructuras básicas de los lenguajes de programación. Por ejemplo, en el sitio web de Oracle hay una sección " Lección: Conceptos básicos del lenguaje ", en la que los bucles tienen una lección separada " La declaración for ". Actualicemos lo básico: el bucle consta de tres expresiones (declaraciones): inicialización (inicialización), condición (terminación) e incremento (incremento):
For y For-Each Loop: una historia sobre cómo iteré, iteré, pero no iteré - 1
Curiosamente, todos son opcionales, lo que significa que podemos, si queremos, escribir:
for (;;){
}
Es cierto que en este caso obtendremos un bucle sin fin, porque No especificamos una condición para salir del bucle (terminación). La expresión de inicialización se ejecuta solo una vez, antes de que se ejecute todo el ciclo. Siempre vale la pena recordar que un ciclo tiene su propio alcance. Esto significa que la inicialización , la terminación , el incremento y el cuerpo del bucle ven las mismas variables. El alcance siempre es fácil de determinar usando llaves. Todo lo que está dentro de los corchetes no es visible fuera de los corchetes, pero todo lo que está fuera de los corchetes es visible dentro de los corchetes. La inicialización es solo una expresión. Por ejemplo, en lugar de inicializar una variable, generalmente puedes llamar a un método que no devolverá nada. O simplemente omítelo y deja un espacio en blanco antes del primer punto y coma. La siguiente expresión especifica la condición de terminación . Mientras sea verdadero , se ejecuta el bucle. Y si es false , no se iniciará una nueva iteración. Si observa la imagen a continuación, obtenemos un error durante la compilación y el IDE se quejará: nuestra expresión en el bucle es inalcanzable. Como no tendremos una sola iteración en el ciclo, saldremos inmediatamente, porque FALSO:
For y For-Each Loop: una historia de cómo iteré, iteré, pero no iteré - 2
Vale la pena estar atento a la expresión en la declaración de terminación : determina directamente si su aplicación tendrá bucles interminables. El incremento es la expresión más simple. Se ejecuta después de cada iteración exitosa del ciclo. Y esta expresión también se puede omitir. Por ejemplo:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Como puede ver en el ejemplo, en cada iteración del ciclo incrementaremos en incrementos de 2, pero solo siempre que el valor outerVarsea menor que 10. Además, dado que la expresión en la declaración de incremento es en realidad solo una expresión, puede contener cualquier cosa. Por tanto, nadie prohíbe utilizar una disminución en lugar de un incremento, es decir, reducir el valor. Siempre debes controlar la escritura del incremento. +=realiza primero un aumento y luego una asignación, pero si en el ejemplo anterior escribimos lo contrario, obtendremos un bucle infinito, porque la variable outerVarnunca recibirá el valor cambiado: en este caso se =+calculará después de la asignación. Por cierto, ocurre lo mismo con los incrementos de vista ++. Por ejemplo, teníamos un bucle:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
El ciclo funcionó y no hubo problemas. Pero entonces llegó el hombre de la refactorización. No entendió el incremento y simplemente hizo esto:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Si el signo de incremento aparece delante del valor, esto significa que primero aumentará y luego volverá al lugar donde se indica. En este ejemplo, comenzaremos inmediatamente a extraer el elemento en el índice 1 de la matriz, omitiendo el primero. Y luego, en el índice 3, fallaremos con el error " java.lang.ArrayIndexOutOfBoundsException ". Como habrás adivinado, esto funcionó antes simplemente porque el incremento se llama después de que se ha completado la iteración. Al trasladar esta expresión a la iteración, todo se rompió. Resulta que incluso en un bucle simple puedes hacer un desastre). Si tienes una matriz, ¿tal vez haya alguna manera más fácil de mostrar todos los elementos?
For y For-Each Loop: una historia de cómo iteré, iteré, pero no iteré - 3

Para cada bucle

A partir de Java 1.5, los desarrolladores de Java nos dieron un diseño for each loopdescrito en el sitio de Oracle en la Guía llamado " The For-Each Loop " o para la versión 1.5.0 . En general, se verá así:
For y For-Each Loop: una historia de cómo iteré, iteré, pero no iteré - 4
Puede leer la descripción de esta construcción en la Especificación del lenguaje Java (JLS) para asegurarse de que no sea mágica. Esta construcción se describe en el capítulo " 14.14.2. La declaración for mejorada ". Como puedes ver, el bucle for each se puede utilizar con arrays y aquellos que implementan la interfaz java.lang.Iterable . Es decir, si realmente lo desea, puede implementar la interfaz java.lang.Iterable y cada bucle puede usarse con su clase. Inmediatamente dirás: "Está bien, es un objeto iterable, pero una matriz no es un objeto. Más o menos". Y te equivocarás, porque... En Java, las matrices son objetos creados dinámicamente. La especificación del lenguaje nos dice esto: " En el lenguaje de programación Java, las matrices son objetos ". En general, las matrices son un poco mágicas de JVM, porque... Se desconoce cómo se estructura internamente la matriz y se encuentra en algún lugar dentro de la máquina virtual Java. Cualquiera interesado puede leer las respuestas en stackoverflow: "¿ Cómo funciona la clase de matriz en Java? " Resulta que si no estamos usando una matriz, entonces debemos usar algo que implemente Iterable . Por ejemplo:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Aquí puedes recordar que si usamos colecciones ( java.util.Collection ), gracias a esto obtenemos exactamente Iterable . Si un objeto tiene una clase que implementa Iterable, está obligado a proporcionar, cuando se llama al método iterador, un Iterador que iterará sobre el contenido de ese objeto. El código anterior, por ejemplo, tendría un código de bytes similar a este (en IntelliJ Idea puedes hacer "Ver" -> "Mostrar código de bytes":
For y For-Each Loop: una historia sobre cómo iteré, iteré, pero no iteré - 5
Como puede ver, en realidad se utiliza un iterador. Si no fuera por cada bucle , tendríamos que escribir algo como:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (Iterator i = names.iterator(); /* continue if */ i.hasNext(); /* skip increment */) {
	String name = (String) i.next();
	System.out.println("Name = " + name);
}

Iterador

Como vimos anteriormente, la interfaz Iterable dice que para instancias de algún objeto, puede obtener un iterador con el que puede iterar sobre el contenido. Nuevamente, se puede decir que este es el Principio de Responsabilidad Única de SOLID . La estructura de datos en sí no debería impulsar el recorrido, pero puede proporcionar uno que debería hacerlo. La implementación básica de Iterator es que generalmente se declara como una clase interna que tiene acceso al contenido de la clase externa y proporciona el elemento deseado contenido en la clase externa. Aquí hay un ejemplo de la clase ArrayListde cómo un iterador devuelve un elemento:
public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
}
Como podemos ver, con la ayuda de ArrayList.thisun iterador accede a la clase externa y su variable elementData, y luego devuelve un elemento desde allí. Entonces, conseguir un iterador es muy sencillo:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Su trabajo se reduce al hecho de que podemos verificar si hay elementos más (el método hasNext ), obtener el siguiente elemento (el método next ) y el método remove , que elimina el último elemento recibido hasta next . El método de eliminación es opcional y no se garantiza su implementación. De hecho, a medida que Java evoluciona, las interfaces también evolucionan. Por lo tanto, en Java 8 también existía un método forEachRemainingque le permite realizar alguna acción sobre los elementos restantes no visitados por el iterador. ¿Qué tiene de interesante un iterador y una colección? Por ejemplo, hay una clase AbstractList. Esta es una clase abstracta que es padre de ArrayListy LinkedList. Y esto es interesante para nosotros debido a un campo como modCount . Cada cambio cambia el contenido de la lista. Entonces, ¿qué nos importa eso? Y el hecho de que el iterador se asegura de que durante la operación la colección sobre la que se itera no cambie. Como comprenderá, la implementación del iterador para listas se encuentra en el mismo lugar que modcount , es decir, en la clase AbstractList. Veamos un ejemplo sencillo:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
names.add("modcount++");
System.out.println(iterator.next());
Aquí está la primera cosa interesante, aunque no relacionada con el tema. En realidad Arrays.asList, devuelve su propio especial ArrayList( java.util.Arrays.ArrayList ). No implementa métodos de adición, por lo que no es modificable. Está escrito en el JavaDoc: tamaño fijo . Pero, de hecho, es más que un tamaño fijo . También es inmutable , es decir, inmutable; eliminar tampoco funcionará en él. También obtendremos un error, porque... Habiendo creado el iterador, recordamos modcount en él . Luego cambiamos el estado de la colección "externamente" (es decir, no a través del iterador) y ejecutamos el método del iterador. Por lo tanto, obtenemos el error: java.util.ConcurrentModificationException . Para evitar esto, el cambio durante la iteración debe realizarse a través del propio iterador y no mediante el acceso a la colección:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.remove();
System.out.println(iterator.next());
Como comprenderás, si iterator.remove()no lo haces antes iterator.next(), entonces porque. el iterador no apunta a ningún elemento, entonces obtendremos un error. En el ejemplo, el iterador irá al elemento John , lo eliminará y luego obtendrá el elemento Sara . Y aquí todo estaría bien, pero mala suerte, nuevamente hay "matices") java.util.ConcurrentModificationException solo ocurrirá cuando hasNext()devuelva verdadero . Es decir, si elimina el último elemento a través de la propia colección, el iterador no caerá. Para obtener más detalles, es mejor ver el informe sobre acertijos de Java de " #ITsubbotnik Sección JAVA: acertijos de Java ". Comenzamos una conversación tan detallada por la sencilla razón de que se aplican exactamente los mismos matices cuando for each loop... Nuestro iterador favorito se usa bajo el capó. Y todos estos matices también se aplican allí. Lo único es que no tendremos acceso al iterador y no podremos eliminar el elemento de forma segura. Por cierto, como comprenderá, el estado se recuerda en el momento en que se crea el iterador. Y la eliminación segura sólo funciona donde se solicita. Es decir, esta opción no funcionará:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Porque para iterator2 la eliminación a través de iterator1 fue “externa”, es decir, se realizó en algún lugar externo y él no sabe nada al respecto. En cuanto al tema de los iteradores, también me gustaría señalar esto. Se creó un iterador extendido especial específicamente para implementaciones de interfaz List. Y le pusieron nombre ListIterator. Le permite avanzar no solo hacia adelante, sino también hacia atrás, y también le permite conocer el índice del elemento anterior y del siguiente. Además, le permite reemplazar el elemento actual o insertar uno nuevo en una posición entre la posición actual del iterador y la siguiente. Como habrás adivinado, ListIteratorestá permitido hacer esto ya que Listestá implementado el acceso por índice.
For y For-Each Loop: una historia de cómo iteré, iteré, pero no iteré - 6

Java 8 e iteración

El lanzamiento de Java 8 ha hecho la vida más fácil para muchos. Tampoco ignoramos la iteración sobre el contenido de los objetos. Para entender cómo funciona esto, es necesario decir algunas palabras al respecto. Java 8 introdujo la clase java.util.function.Consumer . He aquí un ejemplo:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
El consumidor es una interfaz funcional, lo que significa que dentro de la interfaz solo hay 1 método abstracto no implementado que requiere implementación obligatoria en aquellas clases que especifican los implementos de esta interfaz. Esto te permite utilizar algo tan mágico como lambda. Este artículo no trata sobre eso, pero debemos entender por qué podemos usarlo. Entonces, usando lambdas, el Consumidor anterior se puede reescribir así: Consumer consumer = (obj) -> System.out.println(obj); Esto significa que Java ve que algo llamado obj se pasará a la entrada, y luego la expresión después de -> se ejecutará para este obj. En cuanto a la iteración, ahora podemos hacer esto:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Si vas al método forEach, verás que todo es muy sencillo. Ahí está nuestro favorito for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
También es posible eliminar bellamente un elemento usando un iterador, por ejemplo:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
En este caso, el método removeIf no toma como entrada un Consumidor , sino un Predicado . Devuelve booleano . En este caso, si el predicado dice " verdadero ", entonces el elemento será eliminado. Es interesante que aquí tampoco todo es obvio)) Bueno, ¿qué quieres? Es necesario que la gente tenga espacio para crear rompecabezas en la conferencia. Por ejemplo, tomemos el siguiente código para eliminar todo lo que el iterador puede alcanzar después de alguna iteración:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
while (iterator.hasNext()) {
    iterator.next(); // Следующий элемент
    iterator.remove(); // Удалo его
}
System.out.println(names);
Bien, todo funciona aquí. Pero recordamos que Java 8 después de todo. Por tanto, intentemos simplificar el código:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
iterator.forEachRemaining(obj -> iterator.remove());
System.out.println(names);
¿Realmente se ha vuelto más hermoso? Sin embargo, habrá una java.lang.IllegalStateException . Y la razón es... un error en Java. Resulta que está arreglado, pero en JDK 9. Aquí hay un enlace a la tarea en OpenJDK: Iterator.forEachRemaining vs. Iterador.eliminar . Naturalmente, esto ya se ha discutido: ¿ Por qué iterator.forEachRemaining no elimina el elemento en la lambda del consumidor? Bueno, otra forma es directamente a través de Stream API:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

conclusiones

Como vimos en todo el material anterior, un bucle for-each loopes simplemente "azúcar sintáctico" encima de un iterador. Sin embargo, ahora se utiliza en muchos lugares. Además, cualquier producto debe utilizarse con precaución. Por ejemplo, uno inofensivo forEachRemainingpuede esconder sorpresas desagradables. Y esto demuestra una vez más que se necesitan pruebas unitarias. Una buena prueba podría identificar dicho caso de uso en su código. Lo que puedes ver/leer sobre el tema: #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION