Comment accéder à un compteur d'itérations dans une boucle For Each

1. Vue d'ensemble

Lors de l'itération sur des données en Java, nous pouvons souhaiter accéder à la fois à l'élément actuel et à sa position dans la source de données.

C'est très facile à réaliser dans une boucle for classique , où la position est généralement le centre des calculs de la boucle, mais cela nécessite un peu plus de travail lorsque nous utilisons des constructions comme pour chaque boucle ou flux.

Dans ce court tutoriel, nous allons voir quelques façons que pour chaque opération peut inclure un compteur.

2. Implémentation d'un compteur

Commençons par un exemple simple. Nous prendrons une liste ordonnée de films et les publierons avec leur classement.

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

2.1. pour Loop

Une boucle for utilise un compteur pour référencer l'élément actuel, c'est donc un moyen facile d'opérer à la fois sur les données et sur son index dans la liste:

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

Comme cette liste est probablement une ArrayList , l' opération get est efficace et le code ci-dessus est une solution simple à notre problème.

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

Cependant, toutes les sources de données en Java ne peuvent pas être itérées de cette manière. Parfois, get est une opération qui prend du temps, ou nous ne pouvons traiter que l'élément suivant d'une source de données à l'aide de Stream ou Iterable.

2.2. pour chaque boucle

Nous allons continuer à utiliser notre liste de films, mais supposons que nous ne pouvons l'itérer qu'en utilisant Java pour chaque construction:

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

Ici, nous devons utiliser une variable distincte pour suivre l'index actuel. Nous pouvons construire cela en dehors de la boucle et l'incrémenter à l'intérieur:

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

Il faut noter que nous devons incrémenter le compteur après son utilisation dans la boucle.

3. Un fonctionnel pour chacun

L'écriture de l'extension de compteur à chaque fois que nous en avons besoin peut entraîner une duplication de code et peut entraîner des bogues accidentels concernant le moment de mettre à jour la variable de compteur. Nous pouvons donc généraliser ce qui précède en utilisant les interfaces fonctionnelles de Java.

Tout d'abord, nous devons penser au comportement à l'intérieur de la boucle en tant que consommateur à la fois de l'élément de la collection et de l'index. Cela peut être modélisé à l'aide de BiConsumer , qui définit une fonction d' acceptation qui prend deux paramètres

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

Comme l'intérieur de notre boucle est quelque chose qui utilise deux valeurs, nous pourrions écrire une opération de boucle générale. Il peut prendre l' Iterable des données source, sur lesquelles s'exécutera le for each loop, et le BiConsumer pour que l'opération s'exécute sur chaque élément et son index. Nous pouvons rendre cela générique avec le paramètre de type T :

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

Nous pouvons l'utiliser avec notre exemple de classement de films en fournissant l'implémentation pour le BiConsumer en tant que lambda:

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

4. Ajout d'un compteur à forEach avec Stream

L' API Java Stream nous permet d'exprimer comment nos données passent à travers les filtres et les transformations. Il fournit également une fonction forEach . Essayons de convertir cela en une opération qui inclut le compteur.

La fonction Stream forEach demande à un consommateur de traiter l'élément suivant. Nous pourrions, cependant, créer ce consommateur pour garder une trace du compteur et passer l'article à un bi - consommateur :

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

Cette fonction renvoie un nouveau lambda. Ce lambda utilise l' objet AtomicInteger pour suivre le compteur pendant l'itération. La fonction getAndIncrement est appelée à chaque fois qu'il y a un nouvel élément.

Le lambda créé par cette fonction délègue au BiConsumer transmis afin que l'algorithme puisse traiter à la fois l'élément et son index.

Voyons cela utilisé par notre exemple de classement de film par rapport à un flux appelé films :

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

À l'intérieur de forEach se trouve un appel à la fonction withCounter pour créer un objet qui à la fois suit le décompte et agit en tant que consommateur que l' opération forEach transmet également ses valeurs.

5. Conclusion

Dans ce court article, nous avons examiné trois façons d'attacher un compteur à Java pour chaque opération.

Nous avons vu comment suivre l'index de l'élément courant sur chaque implémentation de ceux-ci pour une boucle. Nous avons ensuite examiné comment généraliser ce modèle et comment l'ajouter aux opérations de streaming.

Comme toujours, l'exemple de code de cet article est disponible à l'adresse over sur GitHub.