Maneiras de iterar sobre uma lista em Java

576

Sendo um pouco novo para a linguagem Java, estou tentando me familiarizar com todas as maneiras (ou pelo menos as não-patológicas) em que se pode percorrer uma lista (ou talvez outras coleções) e as vantagens ou desvantagens de cada uma.

Dado um List<E> listobjeto, conheço as seguintes maneiras de percorrer todos os elementos:

Básico para loop (é claro, também existem equivalentes while/ do whileloops)

// 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 o @amarseillan apontou, este formulário é uma má escolha para iterar sobre Lists, porque a implementação real do getmétodo pode não ser tão eficiente quanto ao usar um Iterator. Por exemplo, as LinkedListimplementações devem atravessar todos os elementos anteriores a i para obter o i-ésimo elemento.

No exemplo acima, não há como a Listimplementação "salvar seu lugar" para tornar as iterações futuras mais eficientes. Para um ArrayList, isso realmente não importa, porque a complexidade / custo de geté tempo constante (O (1)), enquanto para a LinkedListé proporcional ao tamanho da lista (O (n)).

Para obter mais informações sobre a complexidade computacional das Collectionsimplementações internas, consulte esta pergunta .

Aprimorado para loop (explicado detalhadamente nesta pergunta )

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 , ...

(Um método de mapa da API Stream do Java 8 (consulte a resposta de @i_am_zero).)

No Java 8, as classes de coleção que implementam Iterable(por exemplo, todos os Lists) agora têm um forEachmétodo, que pode ser usado em vez da instrução de loop for demonstrada acima. (Aqui está outra pergunta que fornece uma boa comparação.)

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).

Que outras maneiras existem, se houver?

(BTW, meu interesse não resulta de um desejo de otimizar o desempenho ; eu só quero saber quais formulários estão disponíveis para mim como desenvolvedor.)

iX3
fonte
1
Esses são os não patológicos, embora você também possa usar qualquer uma das várias bibliotecas de estilo funcional para processar coleções também.
Dave Newton
A pergunta sobre a Listinterface é específica?
Sotirios Delimanolis
@SotiriosDelimanolis, para todos os efeitos, sim, é específico da <code> List </code>, mas se houver outras maneiras interessantes de trabalhar com, por exemplo, uma <code> Collection </code>, eu estaria interessado em conhecê-los.
iX3 23/08/13
@ DaveNewton, obrigado pela ideia. Eu nunca usei algo assim. Dê uma olhada na minha pergunta editada e deixe-me saber se eu entendi o que você quis dizer.
iX3 23/08/13
2
@sdasdadas, feito: stackoverflow.com/questions/18410035/…
iX3

Respostas:

265

As três formas de loop são quase idênticas. O forloop aprimorado :

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

é, de acordo com a Java Language Specification , idêntico ao uso explícito de um iterador com um forloop tradicional . No terceiro caso, você só pode modificar o conteúdo da lista removendo o elemento atual e, somente se o fizer através do removemétodo do iterador. Com a iteração baseada em índice, você pode modificar a lista de qualquer maneira. No entanto, adicionar ou remover elementos anteriores ao índice atual corre o risco de o loop ignorar elementos ou processar o mesmo elemento várias vezes; você precisa ajustar o índice do loop corretamente quando fizer essas alterações.

Em todos os casos, elementé uma referência ao elemento da lista real. Nenhum dos métodos de iteração faz uma cópia de qualquer coisa na lista. As alterações no estado interno de elementsempre serão vistas no estado interno do elemento correspondente na lista.

Essencialmente, existem apenas duas maneiras de iterar em uma lista: usando um índice ou um iterador. O loop for aprimorado é apenas um atalho sintático introduzido no Java 5 para evitar o tédio de definir explicitamente um iterador. Para ambos os estilos, você pode criar variações essencialmente triviais usando for, whileou do whileblocos, mas todas se resumem à mesma coisa (ou melhor, duas coisas).

EDIT: Como o @ iX3 aponta em um comentário, você pode usar a ListIteratorpara definir o elemento atual de uma lista enquanto estiver iterando. Você precisaria usar em List#listIterator()vez de List#iterator()inicializar a variável de loop (que, obviamente, teria que ser declarada um em ListIteratorvez de um Iterator).

Ted Hopp
fonte
OK, obrigado, mas se o iterador estiver realmente retornando uma referência ao elemento real (não uma cópia), como posso usá-lo para alterar o valor na lista? Entendo que, se eu usar um e = iterator.next(), e = somethingElseapenas altere a que objeto ese refere, em vez de alterar o armazenamento real a partir do qual iterator.next()o valor foi recuperado.
iX3 23/08/13
@ iX3 - Isso também seria válido para uma iteração baseada em índice; atribuir um novo objeto a enão altera o que está na lista; você tem que ligar list.set(index, thing). Você pode alterar o conteúdo de e(por exemplo, e.setSomething(newValue)), mas para alterar qual elemento é armazenado na lista enquanto você o itera, você precisa se ater a uma iteração baseada em índice.
Ted Hopp
Obrigado por essa explicação; Acho que entendo o que você está dizendo agora e atualizarei minha pergunta / comentários de acordo. Não há "cópia" em si, mas por causa do design da linguagem, para fazer alterações no conteúdo, eeu teria que chamar um dos emétodos porque a atribuição apenas altera o ponteiro (perdoe meu C).
iX3 23/08/13
Portanto, para listas de tipos imutáveis ​​como Integere Stringnão seria possível alterar o conteúdo usando for-eachou Iteratormétodos - teria que manipular o próprio objeto da lista para substituir os elementos. Isso está certo?
iX3 23/08
@ iX3 - Correto. Você pode remover elementos com o terceiro formulário (iterador explícito), mas só pode adicionar ou substituir elementos na lista se você usar uma iteração baseada em índice.
Ted Hopp
46

Exemplo de cada tipo listado na pergunta:

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
fonte
23

O loop básico não é recomendado, pois você não conhece a implementação da lista.

Se fosse um LinkedList, cada chamada para

list.get(i)

estaria iterando sobre a lista, resultando em complexidade de tempo N ^ 2.

amarseillan
fonte
1
Corrigir; esse é um bom ponto, e vou atualizar o exemplo na pergunta.
iX3 8/04/2015
de acordo com este post, sua declaração não está correta: qual é mais eficiente, um loop for-each ou um iterador?
Hibbem 17/10/16
5
O que eu li naquele post é exatamente o que eu disse ... Que parte você está lendo exatamente?
amarseillan
20

Uma iteração no 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
fonte
Obrigado, pretendo atualizar a lista no topo com as soluções Java 8 eventualmente.
IX3 7/04
1
@ eugene82 essa abordagem é muito mais eficiente?
Nazar_art
1
@nazar_art Você não verá muitas melhorias em relação a qualquer outra coisa, a menos que talvez tenha usado o paralelStream em uma coleção muito grande. É mais eficiente apenas no sentido de curar a verbosidade, na verdade.
Rogue
7

No Java 8 , temos várias maneiras de iterar sobre as classes de coleção.

Usando o Iterable forEach

As coleções que implementam Iterable(por exemplo, todas as listas) agora têm forEachmétodo. Podemos usar a referência de método introduzida no Java 8.

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

Usando Streams forEach e forEachOrdered

Também podemos iterar sobre uma lista usando o Stream como:

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

Deveríamos preferir o forEachOrderedcontrário, forEachporque o comportamento de forEaché explicitamente não determinístico, onde, como ele forEachOrderedexecuta uma ação para cada elemento desse fluxo, na ordem de encontro do fluxo, se o fluxo tiver uma ordem de encontro definida. Portanto, o forEach não garante que o pedido seja mantido.

A vantagem dos fluxos é que também podemos usar fluxos paralelos sempre que apropriado. Se o objetivo é apenas imprimir os itens, independentemente da ordem, podemos usar o fluxo paralelo como:

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

Não sei o que você considera patológico, mas deixe-me fornecer algumas alternativas que você nunca viu antes:

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

Ou sua versão recursiva:

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

Além disso, uma versão recursiva do clássico 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);
}

Eu os mencionei porque você é "um pouco novo em Java" e isso pode ser interessante.

Mario Rossi
fonte
1
Obrigado por mencionar. Eu nunca tinha olhado para o método subList. Sim, consideraria patológico, pois não conheço nenhuma circunstância em que seria vantajoso usá-lo além de talvez concursos de ofuscação.
iX3 23/08/13
3
ESTÁ BEM. Eu não gosto de ser chamado de "patológico", então aqui vai uma explicação: eu os usei ao rastrear ou seguir caminhos em árvores. Mais um? Enquanto traduzia alguns programas Lisp para Java, não queria que eles perdessem seu espírito Lisp, e fiz o mesmo. Voto positivo para o comentário se você acha que esses usos são válidos e não patológicos. Eu preciso de um abraço em grupo !!! :-)
Mario Rossi
Os caminhos nas árvores não são diferentes das listas? BTW, eu não quis chamar você ou qualquer pessoa de "patológica", apenas quero dizer que algumas respostas à minha pergunta serão compreensivelmente impraticáveis ​​ou muito improváveis ​​que tenham valor nas boas práticas de engenharia de software.
iX3 23/08
@ iX3 Não se preocupe; Eu só estava brincando. Os caminhos são listas: "comece pela raiz" (óbvio), "mude para o segundo filho", "mude para o primeiro filho", "mude para o quarto filho". "Pare". Ou [2,1,4] para abreviar. Esta é uma lista.
Mario Rossi
Eu preferiria criar uma cópia da lista e usá-la while(!copyList.isEmpty()){ E e = copyList.remove(0); ... }. Isso é mais eficaz que a primeira versão;).
AxelH
2

Você pode usar o forEach a partir do Java 8:

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

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

Para uma pesquisa reversa, você deve usar o seguinte:

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

Se você deseja conhecer uma posição, use iterator.previousIndex (). Também ajuda a escrever um loop interno que compara duas posições na lista (os iteradores não são iguais).

CoolMind
fonte
0

Certo, muitas alternativas estão listadas. O mais fácil e limpo seria usar apenas a fordeclaração aprimorada , conforme abaixo. O Expressioné de algum tipo que é iterável.

for ( FormalParameter : Expression ) Statement

Por exemplo, para iterar por meio de List <String> ids, podemos simplesmente fazer isso,

for (String str : ids) {
    // Do something
}
Yu Chen
fonte
3
Ele está perguntando se existem outras maneiras além das descritas na pergunta (que incluem essa que você mencionou)!
ericbn
0

Em java 8você pode usar o List.forEach()método com lambda expressionpara iterar sobre uma 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 da pesquisa Resultados da Web Pi
fonte
Legal. Você poderia explicar como isso é diferente da eugene82resposta e i_am_zeroda resposta ?
iX3 10/09/18
@ iX3 ahh. Não leu toda a lista de respostas. Estava tentando ser útil .. Deseja que eu remova a resposta?
Resultados da pesquisa Resultados da Web Pi
Não importa para mim, embora talvez você possa melhorá-lo comparando-o e contrastando-o com outras técnicas. Ou se você acha que ela não demonstra nenhuma técnica nova, mas ainda pode ser útil como referência para outras pessoas, talvez você possa adicionar uma nota explicando isso.
iX3 04/10/19
-2

Você sempre pode alternar o primeiro e o terceiro exemplos com um loop while e um pouco mais de código. Isso oferece a vantagem de poder usar o tempo de ação:

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

Obviamente, esse tipo de coisa pode causar uma NullPointerException se o list.size () retornar 0, pois sempre é executado pelo menos uma vez. Isso pode ser corrigido testando se o elemento é nulo antes de usar seus atributos / métodos. Ainda assim, é muito mais simples e fácil usar o loop for

shieldgenerator7
fonte
É verdade ... acho que isso é tecnicamente diferente, mas o comportamento só é diferente se a lista estiver vazia, certo?
iX3 23/08
@ ix3 sim, e nesse caso, o comportamento é pior. Eu não chamaria isso de uma boa alternativa.
CPerkins 23/08
Claro, mas com justiça, essa pergunta é menos sobre o que é "bom" e mais sobre o que é "possível", então ainda aprecio essa resposta.
iX3 23/08