JavaRush /Blogue Java /Random-PT /For e For-Each Loop: uma história de como eu iterei, fui ...
Viacheslav
Nível 3

For e For-Each Loop: uma história de como eu iterei, fui iterado, mas não fui iterado

Publicado no grupo Random-PT

Introdução

Loops são uma das estruturas básicas das linguagens de programação. Por exemplo, no site da Oracle há uma seção “ Leson: Language Basics ”, na qual os loops possuem uma lição separada “ The for Statement ”. Vamos atualizar o básico: O loop consiste em três expressões (instruções): inicialização (inicialização), condição (término) e incremento (incremento):
For e For-Each Loop: uma história sobre como eu iterei, iterei, mas não iterei - 1
Curiosamente, são todos opcionais, o que significa que podemos, se quisermos, escrever:
for (;;){
}
É verdade que neste caso teremos um loop infinito, porque Não especificamos uma condição para sair do loop (término). A expressão de inicialização é executada apenas uma vez, antes de todo o loop ser executado. Vale sempre lembrar que um ciclo tem seu escopo. Isso significa que inicialização , terminação , incremento e o corpo do loop veem as mesmas variáveis. O escopo é sempre fácil de determinar usando chaves. Tudo dentro dos colchetes não é visível fora dos colchetes, mas tudo fora dos colchetes é visível dentro dos colchetes. A inicialização é apenas uma expressão. Por exemplo, em vez de inicializar uma variável, geralmente você pode chamar um método que não retornará nada. Ou simplesmente ignore, deixando um espaço em branco antes do primeiro ponto e vírgula. A expressão a seguir especifica a condição de encerramento . Contanto que seja true , o loop é executado. E se false , uma nova iteração não será iniciada. Se você olhar a imagem abaixo, teremos um erro durante a compilação e o IDE irá reclamar: nossa expressão no loop está inacessível. Como não teremos uma única iteração no loop, sairemos imediatamente, pois falso:
For e For-Each Loop: uma história de como eu iterei, iterei, mas não iterei - 2
Vale a pena ficar de olho na expressão da instrução de terminação : ela determina diretamente se sua aplicação terá loops infinitos. Incremento é a expressão mais simples. Ele é executado após cada iteração bem-sucedida do loop. E esta expressão também pode ser ignorada. Por exemplo:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Como você pode ver no exemplo, cada iteração do loop incrementaremos em incrementos de 2, mas apenas enquanto o valor outerVarfor menor que 10. Além disso, como a expressão na instrução de incremento é na verdade apenas uma expressão, ela pode conter qualquer coisa. Portanto, ninguém proíbe o uso de um decremento em vez de um incremento, ou seja, reduzir valor. Você deve sempre monitorar a escrita do incremento. +=realiza primeiro um aumento e depois uma atribuição, mas se no exemplo acima escrevermos o contrário, obteremos um loop infinito, pois a variável outerVarnunca receberá o valor alterado: neste caso será =+calculada após a atribuição. A propósito, o mesmo acontece com os incrementos de visualização ++. Por exemplo, tivemos um loop:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
O ciclo funcionou e não houve problemas. Mas então veio o homem da refatoração. Ele não entendeu o incremento e apenas fez isso:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Se o sinal de incremento aparecer na frente do valor, significa que primeiro ele aumentará e depois retornará ao local onde está indicado. Neste exemplo, começaremos imediatamente a extrair o elemento do índice 1 do array, pulando o primeiro. E então no índice 3 iremos travar com o erro " java.lang.ArrayIndexOutOfBoundsException ". Como você deve ter adivinhado, isso funcionou antes simplesmente porque o incremento é chamado após a conclusão da iteração. Ao transferir esta expressão para iteração, tudo quebrou. Acontece que mesmo em um loop simples você pode fazer uma bagunça. Se você tiver um array, talvez haja uma maneira mais simples de exibir todos os elementos?
For e For-Each Loop: uma história de como eu iterei, iterei, mas não iterei - 3

Para cada ciclo

A partir do Java 1.5, os desenvolvedores Java nos deram um design for each loopdescrito no site da Oracle no Guia chamado " The For-Each Loop " ou para a versão 1.5.0 . Em geral, ficará assim:
For e For-Each Loop: uma história de como eu iterei, iterei, mas não iterei - 4
Você pode ler a descrição dessa construção na Especificação da Linguagem Java (JLS) para ter certeza de que não é mágica. Esta construção é descrita no capítulo “ 14.14.2. A instrução for aprimorada ”. Como você pode ver, o loop for each pode ser usado com arrays e aqueles que implementam a interface java.lang.Iterable . Ou seja, se você realmente quiser, você pode implementar a interface java.lang.Iterable e para cada loop pode ser usado com sua classe. Você dirá imediatamente: “Ok, é um objeto iterável, mas um array não é um objeto. Mais ou menos”. E você estará errado, porque... Em Java, arrays são objetos criados dinamicamente. A especificação da linguagem nos diz o seguinte: “ Na linguagem de programação Java, arrays são objetos .” Em geral, arrays são um pouco mágicos da JVM, porque... como o array é estruturado internamente é desconhecido e está localizado em algum lugar dentro da Java Virtual Machine. Quem estiver interessado pode ler as respostas no stackoverflow: " Como funciona a classe array em Java? " Acontece que se não estivermos usando um array, então devemos usar algo que implemente Iterable . Por exemplo:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Aqui você pode apenas lembrar que se usarmos coleções ( java.util.Collection ), graças a isso obteremos exatamente Iterable . Se um objeto possui uma classe que implementa Iterable, ele é obrigado a fornecer, quando o método iterador for chamado, um Iterator que irá iterar sobre o conteúdo daquele objeto. O código acima, por exemplo, teria um bytecode mais ou menos assim (no IntelliJ Idea você pode fazer "View" -> "Show bytecode" :
For e For-Each Loop: uma história sobre como eu iterei, iterei, mas não iterei - 5
Como você pode ver, um iterador é realmente usado. Se não fosse pelo loop for each , teríamos que escrever algo como:
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);
}

Iterador

Como vimos acima, a interface Iterable diz que para instâncias de algum objeto, você pode obter um iterador com o qual pode iterar sobre o conteúdo. Novamente, pode-se dizer que este é o Princípio de Responsabilidade Única do SOLID . A estrutura de dados em si não deve conduzir a travessia, mas pode fornecer aquela que deveria. A implementação básica do Iterator é que ele geralmente é declarado como uma classe interna que tem acesso ao conteúdo da classe externa e fornece o elemento desejado contido na classe externa. Aqui está um exemplo da classe ArrayListde como um iterador retorna um 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];
}
Como podemos ver, com a ajuda de ArrayList.thisum iterador acessa a classe externa e sua variável elementData, e então retorna um elemento a partir daí. Então, obter um iterador é muito simples:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Seu trabalho se resume ao fato de podermos verificar se existem elementos adicionais (o método hasNext ), obter o próximo elemento (o método next ) e o método remove , que remove o último elemento recebido através do next . O método remove é opcional e não há garantia de implementação. Na verdade, à medida que o Java evolui, as interfaces também evoluem. Portanto, no Java 8 também existia um método forEachRemainingque permite realizar alguma ação nos demais elementos não visitados pelo iterador. O que há de interessante em um iterador e coleções? Por exemplo, existe uma classe AbstractList. Esta é uma classe abstrata que é pai de ArrayListand LinkedList. E isso é interessante para nós por causa de um campo como modCount . Cada alteração altera o conteúdo da lista. Então, o que isso importa para nós? E o fato de o iterador garantir que durante a operação a coleção sobre a qual ele é iterado não mude. Como você entende, a implementação do iterador para listas está localizada no mesmo local que modcount , ou seja, na classe AbstractList. Vejamos um exemplo simples:
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());
Aqui está a primeira coisa interessante, embora fora do assunto. Na verdade, Arrays.asListretorna seu próprio especial ArrayList( java.util.Arrays.ArrayList ). Ele não implementa métodos de adição, portanto não pode ser modificado. Está escrito no JavaDoc: tamanho fixo . Mas, na verdade, é mais do que tamanho fixo . Também é imutável , ou seja, imutável; remove também não funcionará. Também receberemos um erro, porque ... Depois de criar o iterador, lembramos do modcount nele . Em seguida, alteramos o estado da coleção “externamente” (ou seja, não por meio do iterador) e executamos o método iterador. Portanto, obtemos o erro: java.util.ConcurrentModificationException . Para evitar isso, a alteração durante a iteração deve ser realizada através do próprio iterador, e não através do acesso à coleção:
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());
Como você entende, se iterator.remove()você não fizer isso antes iterator.next(), então porque. o iterador não aponta para nenhum elemento, obteremos um erro. No exemplo, o iterador irá para o elemento John , removê-lo e, em seguida, obterá o elemento Sara . E aqui tudo ficaria bem, mas azar, novamente há “nuances”) java.util.ConcurrentModificationException só ocorrerá quando hasNext()retornar true . Ou seja, se você deletar o último elemento através da própria coleção, o iterador não cairá. Para obter mais detalhes, é melhor consultar o relatório sobre quebra-cabeças Java da “ #ITsubbotnik Seção JAVA: quebra-cabeças Java ”. Iniciamos uma conversa tão detalhada pela simples razão de que exatamente as mesmas nuances se aplicam quando for each loop... Nosso iterador favorito é usado nos bastidores. E todas essas nuances se aplicam aí também. A única coisa é que não teremos acesso ao iterador e não poderemos remover o elemento com segurança. A propósito, como você entende, o estado é lembrado no momento em que o iterador é criado. E a exclusão segura só funciona onde é chamada. Ou seja, esta opção não funcionará:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Porque para o iterador2 a exclusão através do iterador1 foi “externa”, ou seja, foi realizada em algum lugar externo e ele não sabe nada sobre isso. Sobre o tema dos iteradores, também gostaria de observar isso. Um iterador especial estendido foi criado especificamente para implementações de interface List. E eles o nomearam ListIterator. Permite avançar não só para a frente, mas também para trás, e também permite descobrir o índice do elemento anterior e do seguinte. Além disso, permite substituir o elemento atual ou inserir um novo em uma posição entre a posição atual do iterador e a próxima. Como você adivinhou, ListIteratorisso é permitido porque Listo acesso por índice está implementado.
For e For-Each Loop: uma história de como eu iterei, iterei, mas não iterei - 6

Java 8 e Iteração

O lançamento do Java 8 facilitou a vida de muitos. Também não ignoramos a iteração sobre o conteúdo dos objetos. Para entender como isso funciona, você precisa dizer algumas palavras sobre isso. Java 8 introduziu a classe java.util.function.Consumer . Aqui está um exemplo:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
O consumidor é uma interface funcional, o que significa que dentro da interface existe apenas 1 método abstrato não implementado que requer implementação obrigatória nas classes que especificam os implementos desta interface. Isso permite que você use algo mágico como lambda. Este artigo não é sobre isso, mas precisamos entender por que podemos usá-lo. Portanto, usando lambdas, o Consumidor acima pode ser reescrito assim: Consumer consumer = (obj) -> System.out.println(obj); Isso significa que Java vê que algo chamado obj será passado para a entrada e então a expressão após -> será executada para este obj. Quanto à iteração, agora podemos fazer isso:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Se você seguir o método forEach, verá que tudo é extremamente simples. Aí está o nosso favorito for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
Também é possível remover um elemento lindamente usando um iterador, por exemplo:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
Nesse caso, o método removeIf recebe como entrada não um Consumer , mas um Predicate . Ele retorna booleano . Neste caso, se o predicado disser “ true ”, então o elemento será removido. É interessante que nem tudo é óbvio aqui)) Bem, o que você quer? As pessoas precisam ter espaço para criar quebra-cabeças na conferência. Por exemplo, vamos usar o seguinte código para excluir tudo o que o iterador pode alcançar após alguma iteração:
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, tudo funciona aqui. Mas, afinal, lembramos daquele Java 8. Portanto, vamos tentar simplificar o código:
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);
Realmente ficou mais bonito? No entanto, haverá um java.lang.IllegalStateException . E a razão é... um bug em Java. Acontece que foi corrigido, mas no JDK 9. Aqui está um link para a tarefa no OpenJDK: Iterator.forEachRemaining vs. Iterador.remove . Naturalmente, isso já foi discutido: Por que iterator.forEachRemaining não remove o elemento no lambda do Consumidor? Bem, outra forma é diretamente através da API Stream:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

conclusões

Como vimos em todo o material acima, um loop for-each loopé apenas um “açúcar sintático” em cima de um iterador. No entanto, agora é usado em muitos lugares. Além disso, qualquer produto deve ser usado com cautela. Por exemplo, um inofensivo forEachRemainingpode esconder surpresas desagradáveis. E isso prova mais uma vez que os testes unitários são necessários. Um bom teste poderia identificar esse caso de uso em seu código. O que você pode assistir/ler sobre o tema: #Viacheslav
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION