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):
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:
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
outerVar
sea 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
outerVar
nunca 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?
Para cada bucle
A partir de Java 1.5, los desarrolladores de Java nos dieron un diseño
for each loop
descrito 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í:
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":
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(); i.hasNext(); ) {
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
ArrayList
de 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.this
un 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
forEachRemaining
que 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
ArrayList
y
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,
ListIterator
está permitido hacer esto ya que
List
está implementado el acceso por índice.
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();
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
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();
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 loop
es 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
forEachRemaining
puede 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
GO TO FULL VERSION