Formas de iterar sobre una lista en Java

576

Al ser algo nuevo en el lenguaje Java, estoy tratando de familiarizarme con todas las formas (o al menos las no patológicas) que uno podría recorrer en iteración a través de 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:

Básico para bucle (por supuesto, también hay bucles while/ equivalentes 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, este formulario es una mala elección para iterar sobre Lists, porque la implementación real del getmétodo puede no ser tan eficiente como cuando se usa un Iterator. Por ejemplo,LinkedList implementaciones deben atravesar todos los elementos que preceden a i para obtener el elemento i-ésimo.

En el ejemplo anterior, no hay forma de que la Listimplementación "guarde su lugar" para que las iteraciones futuras sean más eficientes. Para un ArrayListno importa realmente, porque la complejidad / costo de getes tiempo constante (O (1)) mientras que para a LinkedListes proporcional al tamaño de la lista (O (n)).

Para obtener más información sobre la complejidad computacional de las Collectionsimplementaciones integradas , consulte esta pregunta .

Mejorado para bucle (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

    // ...
}

ListIterator

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 la API Stream de Java 8 (consulte la respuesta de @ i_am_zero)).

En Java 8, las clases de colección que implementan Iterable(por ejemplo, todas las Lists) ahora tienen un forEachmétodo, que se puede utilizar en lugar de la instrucción for loop mostrada 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 hay, si hay alguna?

(Por cierto, mi interés no proviene en absoluto del deseo de optimizar el rendimiento ; solo quiero saber qué formularios están disponibles para mí como desarrollador).

iX3
fuente
1
Esos son los no patológicos, aunque también puede usar cualquiera de varias bibliotecas de estilo funcional para procesar colecciones también.
Dave Newton el
¿Es la pregunta sobre la Listinterfaz específicamente?
Sotirios Delimanolis
@SotiriosDelimanolis, para todos los efectos, sí, es específico de <code> List </code>, pero si hay otras formas interesantes de trabajar, digamos, una <code> Collection </code>, estaría interesado en conocerlos.
iX3
@DaveNewton, gracias por la idea. Nunca he usado algo así. Echa un vistazo a mi pregunta editada y avísame si entendí lo que querías decir.
iX3
2
@sdasdadas, hecho: stackoverflow.com/questions/18410035/…
iX3

Respuestas:

265

Las tres formas de bucle son casi idénticas. El forbucle mejorado :

for (E element : list) {
    . . .
}

es, de acuerdo con la Especificación del lenguaje Java , idéntico en efecto al uso explícito de un iterador con un forbucle tradicional . En el tercer caso, solo puede modificar el contenido de la lista eliminando el elemento actual y, luego, solo si lo hace a través deremove método del iterador. Con la iteración basada en índices, puede modificar la lista de cualquier forma. Sin embargo, agregar o eliminar elementos que vienen antes del índice actual corre el riesgo de que su ciclo omita elementos o procese el mismo elemento varias veces; debe ajustar el índice de bucle correctamente cuando realice dichos cambios.

En todos los casos, elementes una referencia al elemento de 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 elementsiempre se verán en el estado interno del elemento correspondiente en la lista.

Esencialmente, solo hay dos formas de iterar sobre una lista: usando un índice o usando 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, se puede llegar a las variaciones esencialmente triviales utilizando for, whileo do whilebloques, pero todas se reducen a lo mismo (o, más bien, dos cosas).

EDITAR: Como @ iX3 señala en un comentario, puede usar a ListIteratorpara establecer el elemento actual de una lista a medida que itera. Debería usar en List#listIterator()lugar de List#iterator()inicializar la variable de bucle (que, obviamente, debería declararse como un en ListIteratorlugar de un Iterator).

Ted Hopp
fuente
Bien, gracias, pero si el iterador en realidad devuelve una referencia al elemento real (no una copia), ¿cómo puedo usarlo para cambiar el valor en la lista? Supongo que si uso un e = iterator.next()entonces, e = somethingElsesimplemente cambia a qué objeto ese refiere en lugar de cambiar la tienda real de la que iterator.next()recuperó el valor.
iX3
@ iX3: eso también sería cierto para una iteración basada en índices; asignar un nuevo objeto a eno cambiará lo que está en la lista; usted tiene que llamar list.set(index, thing). Puede cambiar el contenido de e(por ejemplo, e.setSomething(newValue)), pero para cambiar qué elemento está almacenado en la lista a medida que lo itera, debe seguir con una iteración basada en índices.
Ted Hopp
Gracias por esa explicación; Creo que entiendo lo que estás diciendo ahora y actualizaré mi pregunta / comentarios en consecuencia. No hay "copia" per-se, pero debido al diseño del lenguaje para hacer cambios en el contenido de este e, tendría que llamar a uno de elos métodos porque la asignación solo cambia el puntero (perdón por mi C).
iX3
Entonces, para listas de tipos inmutables como Integery Stringno sería posible cambiar el contenido usando for-eacho Iteratormétodos, tendría que manipular el objeto de la lista para reemplazar a los elementos. ¿Está bien?
iX3
@ iX3: correcto. Puede eliminar elementos con la tercera forma (iterador explícito), pero solo puede agregar o reemplazar elementos en la lista si usa una iteración basada en índices.
Ted Hopp
46

Ejemplo de cada tipo enumerado en la pregunta:

ListIterationExample.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());
     }
}
iX3
fuente
23

No se recomienda el bucle básico, ya que no conoce la implementación de la lista.

Si esa era una LinkedList, cada llamada a

list.get(i)

estaría iterando sobre la lista, resultando en una complejidad de tiempo N ^ 2.

amarseillan
fuente
1
Correcto; ese es un buen punto, y actualizaré el ejemplo en la pregunta.
iX3
Según esta publicación, su afirmación no es correcta: ¿cuál es más eficiente, un ciclo para cada uno o un iterador?
Hibbem
55
Lo que leí en esa publicación es exactamente lo que dije ... ¿Qué parte estás leyendo exactamente?
amarseillan
20

Una iteración de estilo JDK8:

public class IterationDemo {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3);
        list.stream().forEach(elem -> System.out.println("element " + elem));
    }
}
eugene82
fuente
Gracias, tengo la intención de actualizar la lista en la parte superior con las soluciones Java 8 eventualmente.
iX3
1
@ eugene82 ¿este enfoque es mucho más eficiente?
nazar_art
1
@nazar_art No verá muchas mejoras con respecto a cualquier otra cosa, a menos que quizás haya usado parallelStream en una colección muy grande. Es más eficiente solo en el sentido de verbosidad curativa, en realidad.
Pícaro
7

En Java 8 tenemos múltiples formas de iterar sobre las clases de colección.

Usando Iterable para cada uno

Las colecciones que implementan Iterable(por ejemplo, todas las listas) ahora tienen forEachmétodo. Podemos usar el método de referencia introducido en Java 8.

Arrays.asList(1,2,3,4).forEach(System.out::println);

Uso de secuencias para cada uno y para cada pedido

También podemos iterar sobre una lista usando Stream como:

Arrays.asList(1,2,3,4).stream().forEach(System.out::println);
Arrays.asList(1,2,3,4).stream().forEachOrdered(System.out::println);

Deberíamos preferir forEachOrderedmás que forEachporque el comportamiento deforEach es explícitamente no determinista, ya que a medida que forEachOrderedrealiza una acción para cada elemento de esta secuencia, en el orden de encuentro de la secuencia si la secuencia tiene un orden de encuentro definido. Por lo tanto, forEach no garantiza que se mantenga el pedido.

La ventaja de las transmisiones es que también podemos hacer uso de transmisiones paralelas siempre que sea apropiado. Si el objetivo es solo imprimir los artículos independientemente del pedido, entonces podemos usar la transmisión paralela como:

Arrays.asList(1,2,3,4).parallelStream().forEach(System.out::println);
akhil_mittal
fuente
5

No sé qué considera patológico, pero permítame brindarle algunas alternativas que no podría haber visto antes:

List<E> sl= list ;
while( ! sl.empty() ) {
    E element= sl.get(0) ;
    .....
    sl= sl.subList(1,sl.size());
}

O su versión recursiva:

void visit(List<E> list) {
    if( list.isEmpty() ) return;
    E element= list.get(0) ;
    ....
    visit(list.subList(1,list.size()));
}

Además, una versión recursiva de lo clásico for(int i=0...:

void visit(List<E> list,int pos) {
    if( pos >= list.size() ) return;
    E element= list.get(pos) ;
    ....
    visit(list,pos+1);
}

Los menciono porque eres "algo nuevo en Java" y esto podría ser interesante.

Mario Rossi
fuente
1
Gracias por mencionarlo. Nunca había mirado el método subList. Sí, lo consideraría patológico ya que no conozco ninguna circunstancia en la que sea ventajoso usar esto aparte de quizás concursos de ofuscación.
iX3
3
OKAY. No me gusta que me llamen "patológico", así que aquí va una explicación: los he usado para rastrear o seguir caminos en árboles. ¿Uno mas? Mientras traducía algunos programas Lisp a Java, no quería que perdieran su espíritu Lisp, e hice lo mismo. Vota el comentario si crees que estos son usos válidos, no patológicos. Necesito un abrazo grupal !!! :-)
Mario Rossi
¿No son los caminos en los árboles diferentes a las listas? Por cierto, no quise llamar a usted ni a ninguna persona "patológica", solo quiero decir que algunas respuestas a mi pregunta serán comprensiblemente poco prácticas o muy improbables que tengan valor en una buena práctica de ingeniería de software.
iX3
@ iX3 No te preocupes; Sólo bromeaba. Las rutas son listas: "comenzar en la raíz" (obvio), "pasar al segundo niño", "pasar al primer niño", "pasar al cuarto niño". "Detener". O [2,1,4] para abreviar. Esta es una lista.
Mario Rossi el
Preferiría crear una copia de la lista y usarla while(!copyList.isEmpty()){ E e = copyList.remove(0); ... }. Eso es más efectivo que la primera versión;).
AxelH
2

Puede usar forEach a partir de Java 8:

 List<String> nameList   = new ArrayList<>(
            Arrays.asList("USA", "USSR", "UK"));

 nameList.forEach((v) -> System.out.println(v));
Sudip Bhandari
fuente
0

Para una búsqueda hacia atrás debe usar lo siguiente:

for (ListIterator<SomeClass> iterator = list.listIterator(list.size()); iterator.hasPrevious();) {
    SomeClass item = iterator.previous();
    ...
    item.remove(); // For instance.
}

Si desea conocer una posición, use iterator.previousIndex (). También ayuda a escribir un bucle interno que compara dos posiciones en la lista (los iteradores no son iguales).

CoolMind
fuente
0

Correcto, se enumeran muchas alternativas. Lo más fácil y más limpio sería simplemente usar la fordeclaración mejorada como se muestra a continuación. El Expressiones de algún tipo que es iterable.

for ( FormalParameter : Expression ) Statement

Por ejemplo, para recorrer en iteración los ID de lista <String>, podemos simplemente así,

for (String str : ids) {
    // Do something
}
Yu Chen
fuente
3
¡Está preguntando si hay otras formas además de las descritas en la pregunta (que incluyen esta que mencionaste)!
ericbn
0

En java 8puede usar el List.forEach()método con lambda expressionpara iterar sobre una lista.

import java.util.ArrayList;
import java.util.List;

public class TestA {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("Apple");
        list.add("Orange");
        list.add("Banana");
        list.forEach(
                (name) -> {
                    System.out.println(name);
                }
        );
    }
}
Resultados de la búsqueda Resultados web Pi
fuente
Frio. ¿Podría explicar cómo esto es diferente de eugene82la respuesta y i_am_zerola respuesta de ?
iX3
@ iX3 ahh. No leí toda la lista de respuestas. Intentaba ser útil. ¿Quieres que elimine la respuesta?
Resultados de la búsqueda Resultados web Pi
No me importa, aunque quizás podrías mejorarlo comparándolo y contrastando con otras técnicas. O si cree que no demuestra ninguna técnica nueva, pero que podría ser útil como referencia para otros, tal vez podría agregar una nota que lo explique.
iX3
-2

Siempre puede cambiar el primer y el tercer ejemplo con un ciclo while y un poco más de código. Esto le brinda la ventaja de poder usar do-while:

int i = 0;
do{
 E element = list.get(i);
 i++;
}
while (i < list.size());

Por supuesto, este tipo de cosas podría causar una NullPointerException si list.size () devuelve 0, porque siempre se ejecuta al menos una vez. Esto se puede solucionar probando si el elemento es nulo antes de usar sus atributos / métodos. Aún así, es mucho más simple y fácil usar el bucle for

shieldgenerator7
fuente
Es cierto ... Supongo que eso es técnicamente diferente, pero el comportamiento solo es diferente si la lista está vacía, ¿verdad?
iX3
@ ix3 sí, y en ese caso, el comportamiento es peor. No llamaría a esto una buena alternativa.
CPerkins
Claro, pero para ser justos, esta pregunta es menos sobre lo que es "bueno" y más sobre lo que es "posible", por lo que todavía aprecio esta respuesta.
iX3