Modi per scorrere su un elenco in Java


576

Essendo un po 'nuovo nel linguaggio Java, sto cercando di familiarizzare con tutti i modi (o almeno quelli non patologici) che uno potrebbe scorrere attraverso un elenco (o forse altre raccolte) e i vantaggi o gli svantaggi di ciascuno.

Dato un List<E> listoggetto, conosco i seguenti modi per scorrere tutti gli elementi:

Basic per loop (ovviamente, ci sono anche equivalenti while/ do whileloop)

// 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: Come sottolineato da @amarseillan, questo modulo è una scelta sbagliata per iterare su Lists, poiché l'implementazione effettiva del getmetodo potrebbe non essere efficiente come quando si usa un Iterator. Ad esempio, le LinkedListimplementazioni devono attraversare tutti gli elementi che precedono i per ottenere l'i-esimo elemento.

Nell'esempio sopra non è possibile che l' Listimplementazione "salvi il suo posto" per rendere le iterazioni future più efficienti. Per un ArrayListnon importa, perché la complessità / costo di getè tempo costante (O (1)) mentre per a LinkedListè proporzionale alla dimensione della lista (O (n)).

Per ulteriori informazioni sulla complessità computazionale delle Collectionsimplementazioni integrate , consulta questa domanda .

Migliorato per il ciclo (ben spiegato in questa domanda )

for (E element : list) {
    // 1 - can call methods of element

    // ...
}

Iterator

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 funzionale

list.stream().map(e -> e + 1); // Can apply a transformation function for e

Iterable.forEach , Stream.forEach , ...

(Un metodo cartografico dall'API Stream di Java 8 (vedi la risposta di @ i_am_zero).)

In Java 8 le classi di raccolta che implementano Iterable(per esempio, tutte le Lists) ora hanno un forEachmetodo, che può essere usato al posto dell'istruzione for loop mostrata sopra. (Ecco un'altra domanda che fornisce un buon confronto.)

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

Quali altri modi ci sono, se ce ne sono?

(A proposito, il mio interesse non deriva affatto dal desiderio di ottimizzare le prestazioni ; voglio solo sapere quali moduli sono disponibili per me come sviluppatore.)


1
Questi sono quelli non patologici, anche se è possibile utilizzare anche una delle diverse librerie di stile funzionale per elaborare anche le raccolte.
Dave Newton,

La domanda Listsull'interfaccia è specifica?
Sotirios Delimanolis,

@SotiriosDelimanolis, a tutti gli effetti, sì, è specifico per <code> List </code>, ma se ci sono altri modi interessanti di lavorare con, diciamo, una <code> Collection </code>, sarei interessato a conoscerli.
iX3,

@DaveNewton, grazie per l'idea. Non ho mai usato niente del genere. Dai un'occhiata alla mia domanda modificata e fammi sapere se ho capito cosa intendevi.
iX3,

Risposte:


265

Le tre forme di loop sono quasi identiche. Il forciclo avanzato :

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

è, secondo la specifica del linguaggio Java , identico in effetti all'uso esplicito di un iteratore con un forciclo tradizionale . Nel terzo caso, è possibile modificare il contenuto dell'elenco solo rimuovendo l'elemento corrente e, quindi, solo se lo si fa attraverso il removemetodo dell'iteratore stesso. Con l'iterazione basata su indice, sei libero di modificare l'elenco in qualsiasi modo. Tuttavia, l'aggiunta o la rimozione di elementi che precedono l'indice corrente rischia che il ciclo salti gli elementi o elabori lo stesso elemento più volte; è necessario regolare correttamente l'indice del loop quando si apportano tali modifiche.

In tutti i casi, elementè un riferimento all'elemento elenco effettivo. Nessuno dei metodi di iterazione crea una copia di qualsiasi cosa nell'elenco. Le modifiche allo stato interno di elementsaranno sempre visualizzate nello stato interno dell'elemento corrispondente nell'elenco.

In sostanza, ci sono solo due modi per scorrere su un elenco: usando un indice o usando un iteratore. Il loop for avanzato è solo un collegamento sintattico introdotto in Java 5 per evitare il tedio di definire esplicitamente un iteratore. Per entrambi gli stili, si può trovare con variazioni essenzialmente banali usando for, whileo do whileblocchi, ma tutte si riducono alla stessa cosa (o, meglio, due cose).

EDIT: Come sottolinea @ iX3 in un commento, puoi usare a ListIteratorper impostare l'elemento corrente di un elenco mentre stai ripetendo. Dovresti usare List#listIterator()invece di List#iterator()inizializzare la variabile loop (che, ovviamente, dovrebbe essere dichiarata ListIteratorpiuttosto che una Iterator).


OK, grazie, ma se l'iteratore sta effettivamente restituendo un riferimento all'elemento reale (non una copia), come posso usarlo per modificare il valore nell'elenco? Presumo che se uso un e = iterator.next()allora facendo e = somethingElsesemplicemente cambia l'oggetto a cui esi riferisce piuttosto che cambiare l'archivio effettivo da cui ha iterator.next()recuperato il valore.
iX3,

@ iX3 - Ciò sarebbe vero anche per un'iterazione basata su indice; assegnando un nuovo oggetto per enon cambiare ciò che è nella lista; devi chiamare list.set(index, thing). È possibile modificare il contenuto di e(ad esempio, e.setSomething(newValue)), ma per cambiare l'elemento memorizzato nell'elenco mentre lo si sta ripetendo, è necessario attenersi a un'iterazione basata su indice.
Ted Hopp,

Grazie per quella spiegazione; Penso di capire cosa stai dicendo ora e aggiornerò di conseguenza la mia domanda / i miei commenti. Non esiste una "copia" di per sé, ma a causa del design del linguaggio per apportare modifiche al contenuto di edovrei chiamare uno dei emetodi perché l'assegnazione cambia solo il puntatore (scusate la mia C).
iX3,

Quindi per elenchi di tipi immutabili come Integere Stringnon sarebbe possibile cambiare il contenuto usando for-eacho Iteratormetodi - dovrebbe manipolare l'oggetto elenco stesso per sostituirlo con elementi. È giusto?
iX3,

@ iX3 - Corretto. È possibile rimuovere elementi con il terzo modulo (iteratore esplicito), ma è possibile aggiungere o sostituire elementi nell'elenco solo se si utilizza un'iterazione basata su indice.
Ted Hopp,

46

Esempio di ogni tipo elencato nella domanda:

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());
     }
}

23

Il ciclo di base non è raccomandato in quanto non si conosce l'implementazione dell'elenco.

Se quella era una LinkedList, ogni chiamata a

list.get(i)

verrebbe ripetuto sull'elenco, con conseguente complessità temporale N ^ 2.


1
Corretta; questo è un buon punto e aggiornerò l'esempio nella domanda.
iX3,

secondo questo post, la tua affermazione non è corretta: qual è più efficiente, un ciclo for-each o un iteratore?
Hibbem,

5
Quello che ho letto in quel post è esattamente quello che ho detto ... Che parte stai leggendo esattamente?
amarseillan,

20

Un'iterazione in stile 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));
    }
}

Grazie, ho intenzione di aggiornare l'elenco in alto con le soluzioni Java 8 alla fine.
iX3,

1
@ Eugene82 questo approccio è molto più efficiente?
nazar_art,

1
@nazar_art Non vedrai molti miglioramenti rispetto a qualsiasi altra cosa a meno che tu non abbia usato parallelStream su una collezione molto grande. È più efficace nel senso di curare solo la verbosità, davvero.
Ladro

7

In Java 8 abbiamo diversi modi per scorrere le classi di raccolta.

Utilizzo di Iterable forOach

Le raccolte che implementano Iterable(ad esempio tutte le liste) ora hanno il forEachmetodo. Possiamo usare il metodo di riferimento introdotto in Java 8.

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

Utilizzo di stream per ogni e per ogni ordine

Possiamo anche scorrere su un elenco usando Stream come:

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

Dovremmo preferire forEachOrderedrispetto forEachperché il comportamento di forEachè esplicitamente non deterministico dove come forEachOrderedesegue un'azione per ogni elemento di questo flusso, nell'ordine di incontro del flusso se il flusso ha un ordine di incontro definito. Quindi, Ciascuno non garantisce che l'ordine verrà mantenuto.

Il vantaggio con i flussi è che possiamo anche usare i flussi paralleli dove appropriato. Se l'obiettivo è solo quello di stampare gli articoli indipendentemente dall'ordine, allora possiamo usare il flusso parallelo come:

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

5

Non so cosa consideri patologico, ma lasciami fornire alcune alternative che non avresti mai visto prima:

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

O la sua versione ricorsiva:

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

Inoltre, una versione ricorsiva del classico 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);
}

Le menziono perché sei "un po 'nuovo di Java" e questo potrebbe essere interessante.


1
Grazie per averlo menzionato. Non avevo mai esaminato il metodo della lista secondaria. Sì, lo considererei patologico in quanto non conosco alcuna circostanza in cui sarebbe mai vantaggioso utilizzarlo a parte i concorsi forse di offuscamento.
iX3,

3
OK. Non mi piace essere chiamato "patologico", quindi ecco una spiegazione: li ho usati mentre seguivo o seguivo i percorsi sugli alberi. Ancora uno? Durante la traduzione di alcuni programmi Lisp in Java non volevo che perdessero il loro spirito Lisp, e ho fatto lo stesso. Valuta il commento se ritieni che si tratti di usi validi e non patologici. Ho bisogno di un abbraccio di gruppo !!! :-)
Mario Rossi,

I percorsi sugli alberi non sono diversi dalle liste? A proposito, non intendevo chiamare te o qualsiasi persona "patologica", intendo solo che alcune risposte alla mia domanda saranno comprensibilmente poco pratiche o molto improbabili che abbiano valore in una buona pratica di ingegneria del software.
iX3,

@ iX3 Non preoccuparti; Stavo solo scherzando. I percorsi sono elenchi: "inizia dalla radice" (ovvio), "sposta al 2 ° figlio", "sposta al 1 ° figlio", "sposta al 4 ° figlio". "Fermare". O [2,1,4] in breve. Questa è una lista
Mario Rossi,

Preferirei creare una copia dell'elenco e utilizzarlo while(!copyList.isEmpty()){ E e = copyList.remove(0); ... }. È più efficace della prima versione;).
AxelH,

2

Puoi usare forOach a partire da Java 8:

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

 nameList.forEach((v) -> System.out.println(v));

0

Per una ricerca all'indietro è necessario utilizzare quanto segue:

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

Se vuoi conoscere una posizione, usa iterator.previousIndex (). Aiuta anche a scrivere un ciclo interno che confronta due posizioni nell'elenco (gli iteratori non sono uguali).


0

Bene, sono elencate molte alternative. Il più semplice e pulito sarebbe semplicemente usare l' foristruzione avanzata come di seguito. Il Expressionè di un certo tipo che è iterabile.

for ( FormalParameter : Expression ) Statement

Ad esempio, per scorrere tra gli ID elenco <String>, possiamo semplicemente

for (String str : ids) {
    // Do something
}

3
Si sta chiedendo se ci sono altri modi oltre a quelli descritti alla domanda (che include questo che hai menzionato)!
ericbn,

0

In java 8puoi usare il List.forEach()metodo con lambda expressionper scorrere su un elenco.

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);
                }
        );
    }
}

Freddo. Potresti spiegare in che modo questo è diverso dalla eugene82risposta e dalla i_am_zerorisposta ?
iX3,

@ iX3 ahh. Non ho letto l'intero elenco di risposte. Stavo cercando di essere utile .. Vuoi che rimuova la risposta?
Risultati ricerca Risultati web Pi

Non importa per me, anche se forse potresti migliorarlo confrontandolo e confrontandolo con altre tecniche. Oppure, se ritieni che non mostri alcuna nuova tecnica ma potrebbe comunque essere utile come riferimento per gli altri, forse potresti aggiungere una nota per spiegarlo.
iX3,

-2

Puoi sempre cambiare il primo e il terzo esempio con un ciclo while e un po 'più di codice. Questo ti dà il vantaggio di poter usare il do-while:

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

Naturalmente, questo genere di cose potrebbe causare una NullPointerException se list.size () restituisce 0, poiché viene sempre eseguito almeno una volta. Questo può essere risolto testando se l'elemento è nullo prima di usare i suoi attributi / metodi. Tuttavia, è molto più semplice e facile usare il ciclo for


Vero ... Immagino che sia tecnicamente diverso, ma il comportamento è diverso solo se l'elenco è vuoto, giusto?
iX3,

@ ix3 sì, e in quel caso il comportamento è peggiore. Non lo definirei una buona alternativa.
CPerkins,

Certo, ma in tutta onestà, questa domanda è meno su ciò che è "buono" e più su ciò che è "possibile", quindi apprezzo ancora questa risposta.
iX3,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.