JavaRush /Blog Java /Random-FR /Boucle For et For-Each : une histoire sur la façon dont j...
Viacheslav
Niveau 3

Boucle For et For-Each : une histoire sur la façon dont j'ai itéré, été itéré, mais n'a pas été itéré

Publié dans le groupe Random-FR

Introduction

Les boucles sont l'une des structures de base des langages de programmation. Par exemple, sur le site Web d'Oracle, il y a une section « Leçon : bases du langage », dans laquelle les boucles ont une leçon distincte « L'instruction for ». Rafraîchissons les bases : La boucle se compose de trois expressions (instructions) : initialisation (initialisation), condition (termination) et incrément (incrément) :
Boucle For et For-Each : une histoire sur la façon dont j'ai itéré, itéré, mais n'ai pas itéré - 1
Fait intéressant, ils sont tous facultatifs, ce qui signifie que nous pouvons, si nous le souhaitons, écrire :
for (;;){
}
Certes, dans ce cas, nous obtiendrons une boucle sans fin, car Nous ne précisons pas de condition de sortie de la boucle (terminaison). L'expression d'initialisation n'est exécutée qu'une seule fois, avant que la boucle entière ne soit exécutée. Il convient toujours de rappeler qu’un cycle a sa propre portée. Cela signifie que initialisation , terminaison , incrément et le corps de la boucle voient les mêmes variables. La portée est toujours facile à déterminer à l’aide d’accolades. Tout ce qui se trouve à l’intérieur des parenthèses n’est pas visible à l’extérieur des parenthèses, mais tout ce qui se trouve à l’extérieur des parenthèses est visible à l’intérieur des parenthèses. L'initialisation n'est qu'une expression. Par exemple, au lieu d’initialiser une variable, vous pouvez généralement appeler une méthode qui ne retournera rien. Ou ignorez-le simplement, en laissant un espace vide avant le premier point-virgule. L'expression suivante spécifie la condition de fin . Tant que c'est vrai , la boucle est exécutée. Et si false , une nouvelle itération ne démarrera pas. Si vous regardez l'image ci-dessous, nous obtenons une erreur lors de la compilation et l'IDE se plaindra : notre expression dans la boucle est inaccessible. Puisque nous n’aurons pas une seule itération dans la boucle, nous sortirons immédiatement, car FAUX:
Boucle For et For-Each : une histoire sur la façon dont j'ai itéré, itéré, mais n'ai pas itéré - 2
Il vaut la peine de garder un œil sur l’expression dans l’instruction de terminaison : elle détermine directement si votre application aura des boucles sans fin. L'incrément est l'expression la plus simple. Il est exécuté après chaque itération réussie de la boucle. Et cette expression peut également être ignorée. Par exemple:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Comme vous pouvez le voir dans l'exemple, à chaque itération de la boucle, nous incrémenterons par incréments de 2, mais seulement tant que la valeur outerVarest inférieure à 10. De plus, puisque l'expression dans l'instruction d'incrémentation n'est en réalité qu'une expression, elle peut contenir n'importe quoi. Par conséquent, personne n'interdit d'utiliser une décrémentation au lieu d'une incrémentation, c'est-à-dire réduire la valeur. Vous devez toujours surveiller l'écriture de l'incrément. +=effectue d'abord une augmentation puis une affectation, mais si dans l'exemple ci-dessus on écrit le contraire, nous obtiendrons une boucle infinie, car la variable outerVarne recevra jamais la valeur modifiée : dans ce cas elle =+sera calculée après l'affectation. Au fait, c'est la même chose avec les incréments de vue ++. Par exemple, nous avions une boucle :
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
Le cycle a fonctionné et il n'y a eu aucun problème. Mais ensuite l’homme du refactoring est arrivé. Il n'a pas compris l'incrément et a juste fait ceci :
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Si le signe d'incrément apparaît devant la valeur, cela signifie qu'elle va d'abord augmenter puis revenir à l'endroit où elle est indiquée. Dans cet exemple, nous commencerons immédiatement à extraire l’élément d’index 1 du tableau, en ignorant le premier. Et puis à l'index 3 nous planterons avec l'erreur " java.lang.ArrayIndexOutOfBoundsException ". Comme vous l'avez peut-être deviné, cela fonctionnait auparavant simplement parce que l'incrément est appelé une fois l'itération terminée. Lors du transfert de cette expression en itération, tout s'est cassé. Il s'avère que même dans une simple boucle, vous pouvez créer des dégâts.) Si vous avez un tableau, existe-t-il peut-être un moyen plus simple d'afficher tous les éléments ?
Boucle For et For-Each : une histoire sur la façon dont j'ai itéré, itéré, mais n'ai pas itéré - 3

Pour chaque boucle

A partir de Java 1.5, les développeurs Java nous ont fourni un design for each loopdécrit sur le site Oracle dans le Guide intitulé " The For-Each Loop " ou pour la version 1.5.0 . En général, cela ressemblera à ceci :
Boucle For et For-Each : une histoire sur la façon dont j'ai itéré, itéré, mais n'ai pas itéré - 4
Vous pouvez lire la description de cette construction dans la spécification du langage Java (JLS) pour vous assurer qu'il ne s'agit pas de magie. Cette construction est décrite dans le chapitre " 14.14.2. L'instruction for améliorée ". Comme vous pouvez le voir, la boucle for each peut être utilisée avec des tableaux et ceux qui implémentent l' interface java.lang.Iterable . Autrement dit, si vous le souhaitez vraiment, vous pouvez implémenter l' interface java.lang.Iterable et pour chaque boucle, vous pouvez l'utiliser avec votre classe. Vous direz immédiatement : "D'accord, c'est un objet itérable, mais un tableau n'est pas un objet. En quelque sorte." Et vous aurez tort, parce que... En Java, les tableaux sont des objets créés dynamiquement. La spécification du langage nous dit ceci : « Dans le langage de programmation Java, les tableaux sont des objets . » En général, les tableaux sont un peu de la magie JVM, parce que... la manière dont le tableau est structuré en interne est inconnue et se trouve quelque part à l'intérieur de la machine virtuelle Java. Toute personne intéressée peut lire les réponses sur stackoverflow : " Comment fonctionne la classe array en Java ? " Il s'avère que si nous n'utilisons pas de tableau, nous devons alors utiliser quelque chose qui implémente Iterable . Par exemple:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Ici, vous pouvez simplement vous rappeler que si nous utilisons des collections ( java.util.Collection ), grâce à cela, nous obtenons exactement Iterable . Si un objet a une classe qui implémente Iterable, il est obligé de fournir, lorsque la méthode iterator est appelée, un Iterator qui itérera sur le contenu de cet objet. Le code ci-dessus, par exemple, aurait un bytecode ressemblant à ceci (dans IntelliJ Idea, vous pouvez faire "View" -> "Show bytecode" :
Boucle For et For-Each : une histoire sur la façon dont j'ai itéré, itéré, mais n'ai pas itéré - 5
Comme vous pouvez le constater, un itérateur est réellement utilisé. S'il n'y avait pas la boucle for each , nous devrions écrire quelque chose comme :
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (Iterator i = names.iterator(); /* continue if */ i.hasNext(); /* skip increment */) {
	String name = (String) i.next();
	System.out.println("Name = " + name);
}

Itérateur

Comme nous l'avons vu ci-dessus, l' interface Iterable indique que pour les instances d'un objet, vous pouvez obtenir un itérateur avec lequel vous pouvez parcourir le contenu. Encore une fois, cela peut être considéré comme le principe de responsabilité unique de SOLID . La structure de données elle-même ne doit pas piloter le parcours, mais elle peut en fournir une qui le devrait. L'implémentation de base d'Iterator est qu'il est généralement déclaré comme une classe interne qui a accès au contenu de la classe externe et fournit l'élément souhaité contenu dans la classe externe. Voici un exemple tiré de la classe ArrayListmontrant comment un itérateur renvoie un élément :
public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
}
Comme nous pouvons le voir, à l'aide d' ArrayList.thisun itérateur, accède à la classe externe et à sa variable elementData, puis renvoie un élément à partir de là. Ainsi, obtenir un itérateur est très simple :
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Son travail se résume au fait que nous pouvons vérifier s'il y a des éléments plus loin (la méthode hasNext ), obtenir l'élément suivant (la méthode next ) et la méthode Remove , qui supprime le dernier élément reçu via next . La méthode Remove est facultative et sa mise en œuvre n’est pas garantie. En fait, à mesure que Java évolue, les interfaces évoluent également. Par conséquent, dans Java 8, il existait également une méthode forEachRemainingqui vous permet d'effectuer une action sur les éléments restants non visités par l'itérateur. Qu'y a-t-il d'intéressant dans un itérateur et des collections ? Par exemple, il existe une classe AbstractList. Il s'agit d'une classe abstraite qui est le parent de ArrayListet LinkedList. Et cela nous intéresse à cause d'un domaine tel que modCount . À chaque changement, le contenu de la liste change. Alors qu’est-ce que cela nous importe ? Et le fait que l'itérateur s'assure que pendant le fonctionnement, la collection sur laquelle il est itéré ne change pas. Comme vous le comprenez, l'implémentation de l'itérateur pour les listes se situe au même endroit que modcount , c'est-à-dire dans la classe AbstractList. Regardons un exemple simple :
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
names.add("modcount++");
System.out.println(iterator.next());
Voici la première chose intéressante, mais pas sur le sujet. Renvoie en fait Arrays.asListson propre spécial ArrayList( java.util.Arrays.ArrayList ). Il n’implémente pas d’ajout de méthodes, il n’est donc pas modifiable. Il est écrit dans le JavaDoc : taille fixe . Mais en fait, c'est plus qu'une taille fixe . Il est également immuable , c'est-à-dire immuable ; supprimer ne fonctionnera pas non plus. Nous obtiendrons également une erreur, car... Après avoir créé l'itérateur, nous nous sommes souvenus de modcount . Ensuite, nous avons modifié l'état de la collection « en externe » (c'est-à-dire pas via l'itérateur) et exécuté la méthode itérateur. Par conséquent, nous obtenons l’erreur : java.util.ConcurrentModificationException . Pour éviter cela, le changement lors de l'itération doit être effectué via l'itérateur lui-même, et non via l'accès à la collection :
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.remove();
System.out.println(iterator.next());
Comme vous le comprenez, si iterator.remove()vous ne le faites pas avant iterator.next(), alors parce que. l'itérateur ne pointe vers aucun élément, nous obtiendrons alors une erreur. Dans l'exemple, l'itérateur ira à l' élément John , le supprimera, puis obtiendra l' élément Sara . Et ici, tout irait bien, mais pas de chance, encore une fois il y a des "nuances") java.util.ConcurrentModificationException ne se produira que lorsqu'il hasNext()retournera true . Autrement dit, si vous supprimez le dernier élément via la collection elle-même, l'itérateur ne tombera pas. Pour plus de détails, il est préférable de regarder le rapport sur les puzzles Java de « #ITsubbotnik Section JAVA : Java puzzles ». Nous avons entamé une conversation si détaillée pour la simple raison que exactement les mêmes nuances s'appliquent lorsque for each loop... Notre itérateur préféré est utilisé sous le capot. Et toutes ces nuances s’appliquent là aussi. Le seul problème est que nous n’aurons pas accès à l’itérateur et que nous ne pourrons pas supprimer l’élément en toute sécurité. À propos, comme vous l'avez compris, l'état est mémorisé au moment de la création de l'itérateur. Et la suppression sécurisée ne fonctionne que là où elle est appelée. Autrement dit, cette option ne fonctionnera pas :
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Parce que pour iterator2, la suppression via iterator1 était « externe », c'est-à-dire qu'elle a été effectuée quelque part à l'extérieur et il n'en sait rien. Au sujet des itérateurs, je voudrais également souligner ceci. Un itérateur spécial et étendu a été créé spécifiquement pour les implémentations d'interface List. Et ils l'ont nommé ListIterator. Il permet d'avancer non seulement, mais aussi de reculer, et permet également de connaître l'index de l'élément précédent et du suivant. De plus, il vous permet de remplacer l'élément actuel ou d'en insérer un nouveau à une position comprise entre la position actuelle de l'itérateur et la suivante. Comme vous l'avez deviné, ListIteratorcela est autorisé puisque Listl'accès par index est implémenté.
Boucle For et For-Each : une histoire sur la façon dont j'ai itéré, itéré, mais n'ai pas itéré - 6

Java 8 et itération

La sortie de Java 8 a facilité la vie de beaucoup. Nous n'avons pas non plus ignoré les itérations sur le contenu des objets. Pour comprendre comment cela fonctionne, vous devez dire quelques mots à ce sujet. Java 8 a introduit la classe java.util.function.Consumer . Voici un exemple :
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Consumer est une interface fonctionnelle, ce qui signifie qu'à l'intérieur de l'interface, il n'y a qu'une seule méthode abstraite non implémentée qui nécessite une implémentation obligatoire dans les classes qui spécifient les implémentations de cette interface. Cela vous permet d'utiliser une chose aussi magique que lambda. Cet article ne traite pas de cela, mais nous devons comprendre pourquoi nous pouvons l'utiliser. Ainsi, en utilisant lambdas, le consommateur ci-dessus peut être réécrit comme ceci : Consumer consumer = (obj) -> System.out.println(obj); cela signifie que Java voit que quelque chose appelé obj sera transmis à l'entrée, puis l'expression après -> sera exécutée pour cet obj. Quant à l'itération, nous pouvons maintenant faire ceci :
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Si vous allez à la méthode forEach, vous verrez que tout est d’une simplicité folle. Il y a notre préféré for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
Il est également possible de supprimer joliment un élément à l'aide d'un itérateur, par exemple :
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
Dans ce cas, la méthode removeIf prend en entrée non pas un Consumer , mais un Predicate . Il renvoie un booléen . Dans ce cas, si le prédicat dit « true », alors l'élément sera supprimé. C'est intéressant que tout ne soit pas évident ici non plus)) Eh bien, qu'est-ce que tu veux ? Les gens doivent disposer d’un espace pour créer des énigmes lors de la conférence. Par exemple, prenons le code suivant pour supprimer tout ce que l'itérateur peut atteindre après une itération :
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
while (iterator.hasNext()) {
    iterator.next(); // Следующий элемент
    iterator.remove(); // Удалor его
}
System.out.println(names);
D'accord, tout fonctionne ici. Mais on se souvient de Java 8 après tout. Essayons donc de simplifier le code :
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
iterator.forEachRemaining(obj -> iterator.remove());
System.out.println(names);
Est-ce vraiment devenu plus beau ? Cependant, il y aura une java.lang.IllegalStateException . Et la raison est... un bug en Java. Il s'avère que cela est corrigé, mais dans JDK 9. Voici un lien vers la tâche dans OpenJDK : Iterator.forEachRemaining vs. Itérateur.remove . Naturellement, cela a déjà été discuté : pourquoi iterator.forEachRemaining ne supprime pas l'élément dans le lambda Consumer ? Eh bien, une autre méthode consiste à passer directement par l'API Stream :
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

conclusions

Comme nous l’avons vu à partir de tout le matériel ci-dessus, une boucle for-each loopn’est qu’un « sucre syntaxique » au-dessus d’un itérateur. Cependant, il est désormais utilisé dans de nombreux endroits. De plus, tout produit doit être utilisé avec prudence. Par exemple, un objet inoffensif forEachRemainingpeut cacher de mauvaises surprises. Et cela prouve une fois de plus que des tests unitaires sont nécessaires. Un bon test pourrait identifier un tel cas d'utilisation dans votre code. Ce que vous pouvez regarder/lire sur le sujet : #Viacheslav
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION