Come accedere a un contatore di iterazioni in un ciclo For Each

1. Panoramica

Durante l'iterazione sui dati in Java, potremmo voler accedere sia all'elemento corrente che alla sua posizione nell'origine dati.

Questo è molto facile da ottenere in un ciclo for classico , dove la posizione è solitamente il fulcro dei calcoli del ciclo, ma richiede un po 'più di lavoro quando usiamo costrutti come per ogni ciclo o flusso.

In questo breve tutorial, vedremo alcuni modi in cui per ogni operazione può includere un contatore.

2. Implementazione di un contatore

Cominciamo con un semplice esempio. Prenderemo un elenco ordinato di film e li pubblicheremo con la loro classifica.

List IMDB_TOP_MOVIES = Arrays.asList("The Shawshank Redemption", "The Godfather", "The Godfather II", "The Dark Knight");

2.1. per Loop

Un ciclo for utilizza un contatore per fare riferimento all'elemento corrente, quindi è un modo semplice per operare sia sui dati che sul relativo indice nell'elenco:

List rankings = new ArrayList(); for (int i = 0; i < movies.size(); i++) { String ranking = (i + 1) + ": " + movies.get(i); rankings.add(ranking); }

Poiché questo elenco è probabilmente un ArrayList , l' operazione get è efficiente e il codice sopra è una semplice soluzione al nostro problema.

assertThat(getRankingsWithForLoop(IMDB_TOP_MOVIES)) .containsExactly("1: The Shawshank Redemption", "2: The Godfather", "3: The Godfather II", "4: The Dark Knight");

Tuttavia, non tutte le origini dati in Java possono essere iterate in questo modo. A volte get è un'operazione che richiede molto tempo oppure possiamo elaborare solo l'elemento successivo di un'origine dati utilizzando Stream o Iterable.

2.2. per ogni ciclo

Continueremo a utilizzare il nostro elenco di film, ma fingiamo di poter solo iterare su di esso utilizzando Java per ogni costrutto:

for (String movie : IMDB_TOP_MOVIES) { // use movie value }

Qui abbiamo bisogno di utilizzare una variabile separata per tenere traccia dell'indice corrente. Possiamo costruirlo al di fuori del ciclo e incrementarlo all'interno:

int i = 0; for (String movie : movies) { String ranking = (i + 1) + ": " + movie; rankings.add(ranking); i++; }

Dobbiamo notare che dobbiamo incrementare il contatore dopo che è stato utilizzato all'interno del ciclo.

3. Un funzionale per ciascuno

Scrivere l'estensione del contatore ogni volta che ne abbiamo bisogno potrebbe comportare la duplicazione del codice e il rischio di bug accidentali riguardanti quando aggiornare la variabile del contatore. Possiamo, quindi, generalizzare quanto sopra utilizzando le interfacce funzionali di Java.

Innanzitutto, dovremmo pensare al comportamento all'interno del ciclo come consumatore sia dell'elemento nella raccolta che dell'indice. Questo può essere modellato utilizzando BiConsumer , che definisce una funzione di accettazione che accetta due parametri

@FunctionalInterface public interface BiConsumer { void accept(T t, U u); }

Poiché l'interno del nostro ciclo è qualcosa che utilizza due valori, potremmo scrivere un'operazione di ciclo generale. Potrebbe richiedere l' Iterable dei dati di origine, su cui verrà eseguito il ciclo for each , e il BiConsumer per l'operazione da eseguire su ogni elemento e il relativo indice. Possiamo renderlo generico con il parametro di tipo T :

static  void forEachWithCounter(Iterable source, BiConsumer consumer) { int i = 0; for (T item : source) { consumer.accept(i, item); i++; } }

Possiamo usarlo con il nostro esempio di classifica dei film fornendo l'implementazione per il BiConsumer come lambda:

List rankings = new ArrayList(); forEachWithCounter(movies, (i, movie) -> { String ranking = (i + 1) + ": " + movies.get(i); rankings.add(ranking); });

4. Aggiunta di un contatore a forEach with Stream

L' API Java Stream ci consente di esprimere come i nostri dati passano attraverso filtri e trasformazioni. Fornisce inoltre una funzione forEach . Proviamo a convertirlo in un'operazione che includa il contatore.

La funzione Stream forEach richiede a un consumatore di elaborare l'elemento successivo. Potremmo, tuttavia, creare quel consumatore per tenere traccia del contatore e passare l'articolo a un BiConsumer :

public static  Consumer withCounter(BiConsumer consumer) { AtomicInteger counter = new AtomicInteger(0); return item -> consumer.accept(counter.getAndIncrement(), item); }

Questa funzione restituisce un nuovo lambda. Quel lambda usa l' oggetto AtomicInteger per tenere traccia del contatore durante l'iterazione. La funzione getAndIncrement viene chiamata ogni volta che c'è un nuovo elemento.

Il lambda creato da questa funzione delega al BiConsumer passato in modo che l'algoritmo possa elaborare sia l'elemento che il relativo indice.

Vediamo questo in uso dal nostro esempio di classificazione dei film contro uno Stream chiamato film :

List rankings = new ArrayList(); movies.forEach(withCounter((i, movie) -> { String ranking = (i + 1) + ": " + movie; rankings.add(ranking); }));

All'interno di forEach c'è una chiamata alla funzione withCounter per creare un oggetto che tiene traccia del conteggio e funge sia da Consumer che anche l' operazione forEach trasmette i suoi valori.

5. conclusione

In questo breve articolo, abbiamo esaminato tre modi per allegare un contatore a Java per ciascuna operazione.

Abbiamo visto come tenere traccia dell'indice dell'elemento corrente su ciascuna implementazione di essi per un ciclo. Abbiamo quindi esaminato come generalizzare questo modello e come aggiungerlo alle operazioni di streaming.

Come sempre il codice di esempio per questo articolo è disponibile su GitHub.