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):
È 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:
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
outerVar
non 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?
Per ogni ciclo
A partire da Java 1.5, gli sviluppatori Java ci hanno fornito un progetto
for each loop
descritto sul sito Oracle nella Guida chiamata "
The For-Each Loop " o per la versione
1.5.0 . In generale, sarà simile a questo:
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" :
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(); i.hasNext(); ) {
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
ArrayList
di 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.this
un 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
forEachRemaining
che 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
ArrayList
e
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.asList
restituisce 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.
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();
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
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();
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
forEachRemaining
può 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
GO TO FULL VERSION