JavaRush /Java Blog /Random-IT /For e For-Each Loop: una storia di come ho ripetuto, sono...
Viacheslav
Livello 3

For e For-Each Loop: una storia di come ho ripetuto, sono stato ripetuto, ma non sono stato ripetuto

Pubblicato nel gruppo Random-IT

introduzione

I cicli sono una delle strutture di base dei linguaggi di programmazione. Ad esempio, sul sito web di Oracle c'è una sezione “ Lesson: Language Basics ”, in cui i loop hanno una lezione separata “ The for Statement ”. Rinfreschiamo le nozioni di base: il ciclo è composto da tre espressioni (istruzioni): inizializzazione (inizializzazione), condizione (terminazione) e incremento (incremento):
For e For-Each Loop: una storia su come ho ripetuto, ripetuto, ma non ho ripetuto - 1
È interessante notare che sono tutti facoltativi, nel senso che possiamo, se vogliamo, scrivere:
for (;;){
}
È vero, in questo caso otterremo un ciclo infinito, perché Non specifichiamo una condizione per uscire dal ciclo (terminazione). L'espressione di inizializzazione viene eseguita solo una volta, prima che venga eseguito l'intero ciclo. Vale sempre la pena ricordare che un ciclo ha una sua portata. Ciò significa che inizializzazione , terminazione , incremento e il corpo del ciclo vedono le stesse variabili. L'ambito è sempre facile da determinare utilizzando le parentesi graffe. Tutto ciò che è all'interno delle parentesi non è visibile all'esterno delle parentesi, ma tutto ciò che è all'esterno delle parentesi è visibile all'interno delle parentesi. L'inizializzazione è solo un'espressione. Ad esempio, invece di inizializzare una variabile, in genere è possibile chiamare un metodo che non restituirà nulla. Oppure semplicemente saltalo, lasciando uno spazio vuoto prima del primo punto e virgola. La seguente espressione specifica la condizione di terminazione . Finché è true , il ciclo viene eseguito. E se false , una nuova iterazione non verrà avviata. Se guardi l'immagine qui sotto, riceviamo un errore durante la compilazione e l'IDE si lamenterà: la nostra espressione nel loop non è raggiungibile. Poiché non avremo una singola iterazione nel ciclo, usciremo immediatamente, perché falso:
For e For-Each Loop: una storia di come ho ripetuto, ripetuto, ma non ho ripetuto - 2
Vale la pena tenere d'occhio l'espressione nell'istruzione di terminazione : determina direttamente se la tua applicazione avrà cicli infiniti. Incremento è l'espressione più semplice. Viene eseguito dopo ogni iterazione riuscita del ciclo. E questa espressione può anche essere saltata. Per esempio:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Come puoi vedere dall'esempio, a ogni iterazione del ciclo incrementeremo con incrementi di 2, ma solo finché il valore outerVarè inferiore a 10. Inoltre, poiché l'espressione nell'istruzione di incremento è in realtà solo un'espressione, può contenere qualsiasi cosa. Nessuno vieta quindi di usare un decremento invece di un incremento, cioè ridurre il valore. Dovresti sempre monitorare la scrittura dell'incremento. +=esegue prima un incremento e poi un'assegnazione, ma se nell'esempio sopra scriviamo il contrario, otterremo un loop infinito, perché la variabile outerVarnon riceverà mai il valore modificato: in questo caso verrà =+calcolata dopo l'assegnazione. A proposito, è lo stesso con gli incrementi di visualizzazione ++. Ad esempio, abbiamo avuto un ciclo:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
Il ciclo ha funzionato e non ci sono stati problemi. Ma poi è arrivato l'uomo del refactoring. Non ha capito l'incremento e ha semplicemente fatto questo:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Se davanti al valore appare il segno di incremento significa che esso prima aumenterà e poi ritornerà al punto in cui è indicato. In questo esempio inizieremo immediatamente ad estrarre l'elemento con indice 1 dall'array, saltando il primo. E poi all'indice 3 andremo in crash con l'errore " java.lang.ArrayIndexOutOfBoundsException ". Come avrai intuito, prima funzionava semplicemente perché l'incremento viene chiamato dopo il completamento dell'iterazione. Durante il trasferimento di questa espressione all'iterazione, tutto si è rotto. A quanto pare, anche in un ciclo semplice puoi fare un pasticcio) Se hai un array, forse c'è un modo più semplice per visualizzare tutti gli elementi?
For e For-Each Loop: una storia di come ho ripetuto, ripetuto, ma non ho ripetuto - 3

Per ogni ciclo

A partire da Java 1.5, gli sviluppatori Java ci hanno fornito un progetto for each loopdescritto sul sito Oracle nella Guida chiamata " The For-Each Loop " o per la versione 1.5.0 . In generale, sarà simile a questo:
For e For-Each Loop: una storia di come ho ripetuto, ripetuto, ma non ho ripetuto - 4
Puoi leggere la descrizione di questo costrutto nella Java Language Specifica (JLS) per assicurarti che non sia magico. Questa costruzione è descritta nel capitolo " 14.14.2. L'istruzione migliorata ". Come puoi vedere, il ciclo for each può essere utilizzato con gli array e quelli che implementano l' interfaccia java.lang.Iterable . Cioè, se vuoi davvero, puoi implementare l' interfaccia java.lang.Iterable e per ogni ciclo può essere utilizzata con la tua classe. Dirai immediatamente: "Okay, è un oggetto iterabile, ma un array non è un oggetto. In un certo senso." E ti sbaglierai, perché... In Java, gli array sono oggetti creati dinamicamente. Le specifiche del linguaggio ci dicono questo: “ Nel linguaggio di programmazione Java, gli array sono oggetti ”. In generale, gli array sono un po' di magia JVM, perché... il modo in cui l'array è strutturato internamente è sconosciuto e si trova da qualche parte all'interno della Java Virtual Machine. Chiunque sia interessato può leggere le risposte su StackOverflow: " Come funziona la classe array in Java? " Si scopre che se non stiamo utilizzando un array, allora dobbiamo usare qualcosa che implementi Iterable . Per esempio:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Qui puoi solo ricordare che se usiamo le collezioni ( java.util.Collection ), grazie a questo otteniamo esattamente Iterable . Se un oggetto ha una classe che implementa Iterable, è obbligato a fornire, quando viene chiamato il metodo iteratore, un Iterator che itererà sul contenuto di quell'oggetto. Il codice sopra, ad esempio, avrebbe un bytecode simile a questo (in IntelliJ Idea puoi fare "Visualizza" -> "Mostra bytecode" :
For e For-Each Loop: una storia su come ho ripetuto, ripetuto, ma non ho ripetuto - 5
Come puoi vedere, viene effettivamente utilizzato un iteratore. Se non fosse per for each loop , dovremmo scrivere qualcosa del tipo:
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);
}

Iteratore

Come abbiamo visto sopra, l' interfaccia Iterable dice che per le istanze di alcuni oggetti, puoi ottenere un iteratore con il quale puoi scorrere i contenuti. Ancora una volta, si può dire che questo sia il principio di responsabilità unica di SOLID . La struttura dei dati in sé non dovrebbe guidare l'attraversamento, ma può fornirne uno che dovrebbe farlo. L'implementazione di base di Iterator è che viene solitamente dichiarato come una classe interna che ha accesso al contenuto della classe esterna e fornisce l'elemento desiderato contenuto nella classe esterna. Ecco un esempio dalla classe ArrayListdi come un iteratore restituisce un elemento:
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];
}
Come possiamo vedere, con l'aiuto di ArrayList.thisun iteratore si accede alla classe esterna e alla sua variabile elementData, quindi da lì restituisce un elemento. Quindi, ottenere un iteratore è molto semplice:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Il suo lavoro si riduce al fatto che possiamo verificare se ci sono elementi successivi (il metodo hasNext ), ottenere l'elemento successivo (il metodo next ) e il metodo delete , che rimuove l'ultimo elemento ricevuto tramite next . Il metodo di rimozione è facoltativo e non è garantito che venga implementato. Infatti, man mano che Java si evolve, anche le interfacce si evolvono. Pertanto in Java 8 esisteva anche un metodo forEachRemainingche permetteva di eseguire alcune azioni sui restanti elementi non visitati dall'iteratore. Cosa c'è di interessante in un iteratore e nelle raccolte? Ad esempio, esiste una classe AbstractList. Questa è una classe astratta che è il genitore di ArrayListe LinkedList. Ed è interessante per noi a causa di un campo come modCount . Ad ogni modifica cambia il contenuto dell'elenco. Quindi cosa ci importa? E il fatto che l'iteratore si assicuri che durante il funzionamento la raccolta su cui viene iterato non cambi. Come hai capito, l'implementazione dell'iteratore per gli elenchi si trova nello stesso posto di modcount , cioè nella classe AbstractList. Diamo un'occhiata ad un semplice esempio:
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());
Ecco la prima cosa interessante, anche se non in tema. In realtà Arrays.asListrestituisce il suo speciale ArrayList( java.util.Arrays.ArrayList ). Non implementa metodi di aggiunta, quindi non è modificabile. È scritto nel JavaDoc: dimensione fissa . Ma in realtà è più che una dimensione fissa . È anche immutabile , cioè immutabile; rimuovere non funzionerà neanche su di esso. Riceveremo anche un errore, perché... Dopo aver creato l'iteratore, ci siamo ricordati di modcount al suo interno . Quindi abbiamo modificato lo stato della raccolta "esternamente" (cioè non tramite l'iteratore) ed eseguito il metodo iteratore. Pertanto, otteniamo l'errore: java.util.ConcurrentModificationException . Per evitare ciò, la modifica durante l'iterazione deve essere eseguita tramite l'iteratore stesso e non tramite l'accesso alla raccolta:
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());
Come hai capito, se iterator.remove()non lo fai prima iterator.next(), allora perché. l'iteratore non punta ad alcun elemento, quindi otterremo un errore. Nell'esempio, l'iteratore andrà all'elemento John , lo rimuoverà e quindi otterrà l' elemento Sara . E qui tutto andrebbe bene, ma sfortuna, ancora una volta ci sono "sfumature") java.util.ConcurrentModificationException si verificherà solo quando hasNext()restituisce true . Cioè, se elimini l'ultimo elemento attraverso la raccolta stessa, l'iteratore non cadrà. Per maggiori dettagli è meglio guardare il rapporto sui puzzle Java dalla “ #ITsubbotnik Sezione JAVA: Java puzzles ”. Abbiamo iniziato una conversazione così dettagliata per il semplice motivo che si applicano esattamente le stesse sfumature quando for each loop... Il nostro iteratore preferito viene utilizzato dietro le quinte. E tutte queste sfumature si applicano anche lì. L'unica cosa è che non avremo accesso all'iteratore e non saremo in grado di rimuovere l'elemento in modo sicuro. A proposito, come hai capito, lo stato viene ricordato nel momento in cui viene creato l'iteratore. E la cancellazione sicura funziona solo dove viene chiamata. Cioè, questa opzione non funzionerà:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Perché per iterator2 la cancellazione tramite iterator1 è stata “esterna”, cioè è stata eseguita da qualche parte all'esterno e lui non ne sa nulla. Sul tema degli iteratori, vorrei sottolineare anche questo. Uno speciale iteratore esteso è stato creato specificatamente per le implementazioni dell'interfaccia List. E lo hanno chiamato ListIterator. Ti permette di spostarti non solo avanti, ma anche indietro e ti permette anche di scoprire l'indice dell'elemento precedente e di quello successivo. Inoltre, consente di sostituire l'elemento corrente o inserirne uno nuovo in una posizione tra la posizione corrente dell'iteratore e quella successiva. Come hai intuito, ListIteratorè consentito farlo poiché Listè implementato l'accesso tramite indice.
For e For-Each Loop: una storia di come ho ripetuto, ripetuto, ma non ho ripetuto - 6

Java 8 e iterazione

Il rilascio di Java 8 ha reso la vita più facile a molti. Inoltre, non abbiamo ignorato l'iterazione sul contenuto degli oggetti. Per capire come funziona, è necessario dire alcune parole al riguardo. Java 8 ha introdotto la classe java.util.function.Consumer . Ecco un esempio:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Consumer è un'interfaccia funzionale, il che significa che all'interno dell'interfaccia è presente solo 1 metodo astratto non implementato che richiede un'implementazione obbligatoria in quelle classi che specificano gli implementamenti di questa interfaccia. Ciò ti consente di utilizzare una cosa magica come lambda. Questo articolo non riguarda questo, ma dobbiamo capire perché possiamo usarlo. Quindi, utilizzando lambda, il Consumer di cui sopra può essere riscritto in questo modo: Consumer consumer = (obj) -> System.out.println(obj); Ciò significa che Java vede che qualcosa chiamato obj verrà passato all'input, e quindi l'espressione after -> verrà eseguita per questo obj. Per quanto riguarda l'iterazione, ora possiamo fare questo:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Se vai al metodo forEach, vedrai che tutto è pazzesco. C'è il nostro preferito for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
È anche possibile rimuovere magnificamente un elemento utilizzando un iteratore, ad esempio:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
In questo caso, il metodo deleteIf prende come input non un Consumer , ma un Predicate . Restituisce un valore booleano . In questo caso, se il predicato dice " true ", l'elemento verrà rimosso. È interessante notare che anche qui non tutto è ovvio)) Bene, cosa vuoi? È necessario dare alle persone lo spazio per creare enigmi durante la conferenza. Ad esempio, prendiamo il seguente codice per eliminare tutto ciò che l'iteratore può raggiungere dopo qualche iterazione:
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);
Ok, qui funziona tutto. Ma dopotutto ricordiamo Java 8. Proviamo quindi a semplificare il codice:
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);
È davvero diventato più bello? Tuttavia, ci sarà una java.lang.IllegalStateException . E il motivo è... un bug in Java. Si scopre che è stato corretto, ma in JDK 9. Ecco un collegamento all'attività in OpenJDK: Iterator.forEachRemaining vs. Iterator.remove . Naturalmente, questo è già stato discusso: perché iterator.forEachRemaining non rimuove l'elemento nel lambda Consumer? Bene, un altro modo è direttamente tramite 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));

conclusioni

Come abbiamo visto da tutto il materiale sopra, un ciclo for-each loopè semplicemente “zucchero sintattico” sopra un iteratore. Tuttavia, ora è utilizzato in molti posti. Inoltre, qualsiasi prodotto deve essere utilizzato con cautela. Ad esempio, uno innocuo forEachRemainingpuò nascondere spiacevoli sorprese. E questo dimostra ancora una volta che sono necessari test unitari. Un buon test potrebbe identificare un caso d'uso di questo tipo nel tuo codice. Cosa puoi guardare/leggere sull'argomento: #Viacheslav
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION