Formas de iterar sobre una lista en Java
Siendo algo nuevo en el lenguaje Java, estoy tratando de familiarizarme con todas las formas (o al menos las no patológicas) en las que uno puede recorrer una lista (o quizás otras colecciones) y las ventajas o desventajas de cada una.
Dado un List<E> list
objeto, conozco las siguientes formas de recorrer todos los elementos:
Bucle for básico (por supuesto, también hay bucles / equivalentes)while
do while
// Not recommended (see below)!
for (int i = 0; i < list.size(); i++) {
E element = list.get(i);
// 1 - can call methods of element
// 2 - can use 'i' to make index-based calls to methods of list
// ...
}
Nota: Como señaló @amarseillan, esta forma es una mala elección para iterar sobre List
s, porque la implementación real del get
método puede no ser tan eficiente como cuando se usa un Iterator
. Por ejemplo, LinkedList
las implementaciones deben atravesar todos los elementos que preceden a i para obtener el i-ésimo elemento.
En el ejemplo anterior, no hay forma de que la List
implementación "guarde su lugar" para hacer que las iteraciones futuras sean más eficientes. Para an ArrayList
realmente no importa, porque la complejidad/costo de get
es tiempo constante (O(1)) mientras que para a LinkedList
es proporcional al tamaño de la lista (O(n)).
Para obtener más información sobre la complejidad computacional de las implementaciones integradas Collections
, consulte esta pregunta .
Bucle for mejorado (muy bien explicado en esta pregunta )
for (E element : list) {
// 1 - can call methods of element
// ...
}
Iterador
for (Iterator<E> iter = list.iterator(); iter.hasNext(); ) {
E element = iter.next();
// 1 - can call methods of element
// 2 - can use iter.remove() to remove the current element from the list
// ...
}
ListaIterador
for (ListIterator<E> iter = list.listIterator(); iter.hasNext(); ) {
E element = iter.next();
// 1 - can call methods of element
// 2 - can use iter.remove() to remove the current element from the list
// 3 - can use iter.add(...) to insert a new element into the list
// between element and iter->next()
// 4 - can use iter.set(...) to replace the current element
// ...
}
Java funcional
list.stream().map(e -> e + 1); // Can apply a transformation function for e
Iterable.forEach , Stream.forEach , ...
(Un método de mapa de Stream API de Java 8 (consulte la respuesta de @i_am_zero).)
En Java 8, las clases de colección que implementan Iterable
(por ejemplo, todas List
las s) ahora tienen un forEach
método, que se puede utilizar en lugar de la instrucción de bucle for demostrada anteriormente. (Aquí hay otra pregunta que proporciona una buena comparación).
Arrays.asList(1,2,3,4).forEach(System.out::println);
// 1 - can call methods of an element
// 2 - would need reference to containing object to remove an item
// (TODO: someone please confirm / deny this)
// 3 - functionally separates iteration from the action
// being performed with each item.
Arrays.asList(1,2,3,4).stream().forEach(System.out::println);
// Same capabilities as above plus potentially greater
// utilization of parallelism
// (caution: consequently, order of execution is not guaranteed,
// see [Stream.forEachOrdered][stream-foreach-ordered] for more
// information about this).
¿Qué otras formas existen, si las hay?
(Por cierto, mi interés no surge en absoluto del deseo de optimizar el rendimiento ; solo quiero saber qué formularios están disponibles para mí como desarrollador).
Las tres formas de bucle son casi idénticas. El bucle mejorado for
:
for (E element : list) {
. . .
}
es, según la Especificación del lenguaje Java , idéntico en efecto al uso explícito de un iterador con un for
bucle tradicional. En el tercer caso, sólo puedes modificar el contenido de la lista eliminando el elemento actual y, entonces, sólo si lo haces a través del remove
método del propio iterador. Con la iteración basada en índices, usted es libre de modificar la lista de cualquier forma. Sin embargo, agregar o eliminar elementos que vienen antes del índice actual corre el riesgo de que el bucle omita elementos o procese el mismo elemento varias veces; debe ajustar el índice de bucle correctamente cuando realice dichos cambios.
En todos los casos, element
es una referencia al elemento de la lista real. Ninguno de los métodos de iteración hace una copia de nada en la lista. Los cambios en el estado interno de element
siempre se verán en el estado interno del elemento correspondiente en la lista.
Básicamente, sólo hay dos formas de iterar sobre una lista: utilizando un índice o utilizando un iterador. El bucle for mejorado es solo un atajo sintáctico introducido en Java 5 para evitar el tedio de definir explícitamente un iterador. Para ambos estilos, puedes crear variaciones esencialmente triviales usando for
, while
o do while
bloques, pero todos se reducen a lo mismo (o, mejor dicho, dos cosas).
EDITAR: Como @iX3 señala en un comentario, puede usar a ListIterator
para establecer el elemento actual de una lista mientras itera. Necesitaría usar List#listIterator()
en lugar de List#iterator()
para inicializar la variable de bucle (que, obviamente, tendría que declararse como ListIterator
en lugar de como Iterator
).
Ejemplo de cada tipo enumerado en la pregunta:
ListaIteraciónEjemplo.java
import java.util.*;
public class ListIterationExample {
public static void main(String []args){
List<Integer> numbers = new ArrayList<Integer>();
// populates list with initial values
for (Integer i : Arrays.asList(0,1,2,3,4,5,6,7))
numbers.add(i);
printList(numbers); // 0,1,2,3,4,5,6,7
// replaces each element with twice its value
for (int index=0; index < numbers.size(); index++) {
numbers.set(index, numbers.get(index)*2);
}
printList(numbers); // 0,2,4,6,8,10,12,14
// does nothing because list is not being changed
for (Integer number : numbers) {
number++; // number = new Integer(number+1);
}
printList(numbers); // 0,2,4,6,8,10,12,14
// same as above -- just different syntax
for (Iterator<Integer> iter = numbers.iterator(); iter.hasNext(); ) {
Integer number = iter.next();
number++;
}
printList(numbers); // 0,2,4,6,8,10,12,14
// ListIterator<?> provides an "add" method to insert elements
// between the current element and the cursor
for (ListIterator<Integer> iter = numbers.listIterator(); iter.hasNext(); ) {
Integer number = iter.next();
iter.add(number+1); // insert a number right before this
}
printList(numbers); // 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
// Iterator<?> provides a "remove" method to delete elements
// between the current element and the cursor
for (Iterator<Integer> iter = numbers.iterator(); iter.hasNext(); ) {
Integer number = iter.next();
if (number % 2 == 0) // if number is even
iter.remove(); // remove it from the collection
}
printList(numbers); // 1,3,5,7,9,11,13,15
// ListIterator<?> provides a "set" method to replace elements
for (ListIterator<Integer> iter = numbers.listIterator(); iter.hasNext(); ) {
Integer number = iter.next();
iter.set(number/2); // divide each element by 2
}
printList(numbers); // 0,1,2,3,4,5,6,7
}
public static void printList(List<Integer> numbers) {
StringBuilder sb = new StringBuilder();
for (Integer number : numbers) {
sb.append(number);
sb.append(",");
}
sb.deleteCharAt(sb.length()-1); // remove trailing comma
System.out.println(sb.toString());
}
}
No se recomienda el bucle básico ya que no se conoce la implementación de la lista.
Si se tratara de una LinkedList, cada llamada a
list.get(i)
estaría iterando sobre la lista, lo que resultaría en una complejidad de tiempo N^2.