Iterações e operações com elementos através da Iterator Interface

Fala Galera :slight_smile:

Estou estudando algumas classes e interfaces do Java Collections Framework.

No código existe uma variável sum inicializada com 0 (zero) que vai receber a soma do seu valor atual com o valor do elemento da coleção.

Na primeira iteração através do método forEachRemaining() já encontro o primeiro problema:
a IDE apresenta um erro com as seguintes mensagens:

"Variable used in lamba expression should be finel or effectively final"

ou

java: local variables referenced from a lambda expression must be final or effectively final.

Não entendi. Eu esperava poder realizar essa operação.

Código:

//1
numbersIterator.forEachRemaining(number -> sum += number);

Já no segundo loop (loop //2) eu uso a estrutura while para percorrer e realizar a operação de soma. Tudo ocorre como o esperado, a variável sum agora tem valor 10.

Código:

        //2
        while (numbersIterator.hasNext()) {
            Integer nextNumber = numbersIterator.next();
            sum += nextNumber;
        }

       System.out.println(sum); // output 1

Criei um novo loop (loop //3) para realizar uma nova operação de soma com a variável sum e os elementos da coleção. A variavel sum tem valor 10.

       //3
        while (numbersIterator.hasNext()) {
            Integer nextNumber = numbersIterator.next();
            sum += nextNumber;

        }

        System.out.println(sum); // output 2

Eu esperava ver:

  • output 1 = 10
  • output 2 = 20

Mas a saída é 10 nos dois casos.

Gostaria de enteder mais sobre essas questões apresentadas.

Código completo:

public class TestIterators {
    public static void main(String[] args) {
        Set<Integer> numbers = new HashSet<>();
        numbers.add(0);
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);

        Iterator<Integer> numbersIterator = numbers.iterator();

        Integer sum = 0;

        //1
        /*numbersIterator.forEachRemaining(number -> sum += number);*/

        //2
        while (numbersIterator.hasNext()) {
            Integer nextNumber = numbersIterator.next();
            sum += nextNumber;
        }

        System.out.println(sum); // output 1

        //3
        while (numbersIterator.hasNext()) {
            Integer nextNumber = numbersIterator.next();
            sum += nextNumber;

        }

        System.out.println(sum); // output 2


    }
}

Como a variável sum é a mesma usada nos dois loops, o segundo loop irá incrementar a variável que já estará valendo 10, ou seja: 10 + 10 = 20. Se vc espera que sum seja 10 nos dois loops, vc deve zerar sum após cada loop.


Sobre esse erro:

Variable used in lamba expression should be finel or effectively final

Isso é normal, pq vc está querendo alterar um valor de uma variável externa dentro do lambda, o que não é uma boa prática, pq lambda preza pela imutabilidade, ou seja, vc não altera variáveis existentes, vc apenas faz transformações atribuindo em novas variáveis (isso é característica de programação funcional, e pode ser um pouco abstrato para entender num primeiro momento). Posta o código do lambda que vc criou pra gente ver.

Na verdade, @Lucas_Camara, o problema dele é o contrário. Ele queria que a saída fosse 10 e 20, mas está sendo 10 e 10.

Isto acontece, @xgeovanedamasceno, porque o segundo while simplesmente não está sendo executado. Faz o teste vc mesmo, coloca um print dentro de cada while para vc ver.

O motivo para o segundo while não funcionar é porque não há mais elementos no Iterator que vc criou no início do código. O primeiro while consome todo o Iterator que já vai estar “vazio” quando chega no segundo while.

Vc precisaria criar um novo Iterator para usar no segundo while.

Iterator<Integer> numbersIterator = numbers.iterator();
while (numbersIterator.hasNext()) {
  Integer nextNumber = numbersIterator.next();
  sum += nextNumber;
}

System.out.println(sum); // output 1

numbersIterator = numbers.iterator();
while (numbersIterator.hasNext()) {
  Integer nextNumber = numbersIterator.next();
  sum += nextNumber;
}

Sobre a mensagem de erro, embora não seja possível alterar o valor de uma variável, como o Lucas te explicou, é possível alterar o estado do objeto apontado por aquela variável. Olha só este exemplo usando AtomicInteger:

Set<Integer> numbers = new HashSet<>();

numbers.add(0);
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);

AtomicInteger atomicInteger = new AtomicInteger();

Iterator<Integer> numbersIterator = numbers.iterator();

numbersIterator.forEachRemaining(number -> atomicInteger.addAndGet(number));

System.out.println(atomicInteger.get());

Sobre este negócio de variável final ou “efetivamente final” é o seguinte, considere o código abaixo:

import java.util.stream.Stream;

public class Main {
  public static void main(String... args) {
    final int x = 10;

    int y = 10;

    int z = 10;
    z = 11;

    Stream.of(1).forEach(n -> System.out.println(n + x + y));
  }
}

A variável x é explicitamente final já que foi declarada com a palavra-chave final.

A variável y é considerada efetivamente final porque ela recebe um valor e este valor não muda mais ao longo do programa.

Já a variável z não é explícita e nem efetivamente final já que o seu valor era 10 e depois passou a ser 11. Mesmo que o valor tenha mudado antes de ela ser usada na lambda, o compilador entende que o valor mudou, logo, não final de forma alguma e não permite seu uso.

2 curtidas

@Lucas_Camara acho que lambda que criei de forma não intencional foi o que já apresentei no código da dúvida, mas estava comentado para execução (iteração //1).

public class TestIterators {
    public static void main(String[] args) {
        Set<Integer> numbers = new HashSet<>();
        numbers.add(0);
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);

        Iterator<Integer> numbersIterator = numbers.iterator();

        Integer sum = 0;

        //1
        numbersIterator.forEachRemaining(number -> sum += number);

       // código restante....

@wldomiciano de fato o segundo while não é executado. Como posso entender melhor essa interface Iterator? Qual é a finalidade ou a lógica desse comportamento?

sobre AtomicInteger muito interessante mesmo. Mas confesso que essa afirmação "embora não seja possível alterar o valor de uma variável" é nova para mim. Isso apenas nos casos de lambda, certo?

e sobre efetivamente vs explicitamente existe algo mais que devo pesquisar? Muito interessante como o compilador faz essa verificação e interrompe a compilação. O objetivo é evitar bugs em tempo de execução?

Com lambda, poderia ser assim:

Set<Integer> numbers = new HashSet<>();
numbers.add(0);
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);

Integer sum = 0;

sum = numbers.stream().reduce(0, (a, b) -> a + b);

System.out.println(sum);
2 curtidas

A ideia do Iterator é que ele só pode ser percorrido uma vez - basta ver na documentação que seus únicos métodos são hasNext (pra ver se ainda tem mais elementos), next (que retorna o próximo elemento), remove (que remove o elemento atual) e forEachRemaining (faz algo com os elementos que ainda falta percorrer).

Ele foi feito pra “andar pra frente”, e só. Uma vez que chega ao fim (ao último elemento), não tem como voltar, e se quiser fazer outro loop tem que criar outro Iterator.

É o que aconteceu no seu caso: o primeiro while percorreu todos os elementos, por isso não tinha mais nenhum pro segundo while percorrer.

3 curtidas

Complementando a resposta do Hugo:

Vc já entendeu que o método hasNext() nos diz se o iterator ainda possui elementos a serem iterados e que o método next() avança para o próximo elemento.

Foca na palavra que eu destaquei. O iterator apenas avança, nunca retrocede.

Tentar avançar para o próximo elemento depois que o iterator chegou ao fim, causa uma NoSuchElementException.

Experimente este exemplo:

import java.util.HashSet;
import java.util.Iterator;

public class Main {
  public static void main(String... args) {
    HashSet<Integer> set = new HashSet<>();
    set.add(123);
    Iterator<Integer> iterator = set.iterator();
    System.out.println(iterator.next()); // Ok: Retorna 123
    System.out.println(iterator.next()); // Erro: NoSuchElementException
  }
}

Sobre isto não tem muito o que se pensar, a interface Iterator foi projetada para isso, apenas avançar.

Cada vez que vc invoca o método iterator() uma nova instância de Iterator é retornada, dá para atestar isso assim:

import java.util.HashSet;
import java.util.Iterator;

public class Main {
  public static void main(String... args) {
    HashSet<Integer> set = new HashSet<>();
    Iterator<Integer> it1 = set.iterator();
    Iterator<Integer> it2 = set.iterator();
    System.out.println(it1.equals(it2)); // false
  }
}

Será que se a gente construir nossa própria implementação de Iterator vc pega melhor a ideia? Olha só:

import java.util.Iterator;
import java.util.NoSuchElementException;

class IntegerList implements Iterable<Integer> {
  private Integer[] elements;

  private int currentSize = 0;

  IntegerList(int size) {
    this.elements = new Integer[size];
  }

  void add(int element) {
    this.elements[this.currentSize++] = element;
  }

  @Override
  public Iterator<Integer> iterator() {
    return new IntegerListIterator();
  }

  private class IntegerListIterator implements Iterator<Integer> {
    private int currentIndex = 0;

    @Override
    public boolean hasNext() {
      return this.currentIndex < IntegerList.this.elements.length;
    }

    @Override
    public Integer next() {
      try {
        return IntegerList.this.elements[this.currentIndex++];
      } catch (IndexOutOfBoundsException e) {
        throw new NoSuchElementException();
      }
    }
  }
}

public class Main {
  public static void main(String... args) {
    IntegerList list = new IntegerList(2);

    list.add(123);
    list.add(456);

    Iterator<Integer> iterator = list.iterator();

    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }

    // Olha que da hora:
    // Classes que implementam Comparable podem ser usadas no Enhanced For!
    for (Integer number : list) {
      System.out.println(number);
    }
  }
}

Deu pra pegar a ideia? Óbvio que falta muita coisa, mas é só pra vc ter uma noção mesmo.

Se quiser um exemplo real, leia o código fonte de ArrayList do OpenJDK a partir da linha abaixo:

Agora, se vc quer um Iterator que vai e vem, vc precisa do ListIterator:

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ListIterator.html

Olha um exemplo doido:

import java.util.ArrayList;
import java.util.ListIterator;

public class Main {
  public static void main(String... args) {
    ArrayList<Integer> list = new ArrayList<>();

    list.add(1);
    list.add(2);
    list.add(3);

    ListIterator<Integer> iterator = list.listIterator();

    System.out.println("Pra frente:");

    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }

    System.out.println("Pra trás:");

    while (iterator.hasPrevious()) {
      System.out.println(iterator.previous());
    }

    System.out.println("Pra frente de novo:");

    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}

Não sei se entendi bem a pergunta, mas este negócio de não dar para alterar o valor de uma variável final, mas dar para mudar o estado interno dos objetos apontados por aquela variável vale para qualquer contexto.

Por exemplo, o código abaixo vai dar erro:

class Person {
  private String name;

  Person(String name) {
    this.name = name;
  }

  void changeName(String name) {
    this.name = name;
  }

  String getName() {
    return this.name;
  }
}

public class Main {
  public static void main(String... args) {
    final Person joao = new Person("João");
    joao = new Person("Marcos");
    System.out.println(joao.getName());
  }
}

Mas vc é livre para mudar o estado interno de Person através do método changeName().

final Person joao = new Person("João");
joao.changeName("Marcos");
System.out.println(joao.getName());

Por isso a palavra-chave final não garante imutabilidade de verdade. Vc teria que projetar sua classe para ser imutável como é a String e a BigDecimal, por exemplo.

É interessante mesmo, mas eu também não sei qual é o motivo disso também :pensive:.

Bom, fora o caso das lambdas, que eu sei até o momento, tem o caso das inner classes.

Vc também não pode usar variáveis que não sejam final ou efetivamente final dentro inner classes. Isto vai dar erro:

public class Main {
  public static void main(String... args) {
    int x = 123;

    x = 456;

    class MyClass {
      void doSomething() {
        System.out.println(x);
      }
    }

    new MyClass().doSomething();
  }
}
3 curtidas

Em inner class pode sim, não pode em classes anônimas e em classes locais, também chamadas de classes temporárias, pois elas são declaradas dentro do escopo de um método ou bloco de código.

No seu exemplo não é uma inner class, é uma classe local, só existe dentro do escopo de um método.

Inner class é quando está declarada dentro do corpo de uma classe.
No seu exemplo, se declarar MyClass fora do método main, então ela vira uma inner class.

1 curtida

Então, Staroski, sua afirmação não está correta. Uma inner class não é apenas quando declarada como membro da classe.

Veja a definição que a especificação nos dá:

https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.1.3

An inner class is a nested class that is not explicitly or implicitly static.

An inner class is one of the following:

  • a member class that is not explicitly or implicitly static (§8.5)

  • a local class that is not implicitly static (§14.3)

  • an anonymous class (§15.9.5)

Ou seja, classes anônimas e classes locais também são inner classes.

Esse termo que vc usou “classes temporárias” pode ser um termo informal, pois não aparece na especificação. Na verdade, classes anônimas, locais, membros, estáticas ou não estáticas são coletivamente chamadas de Nested Classes.

Além disso, o compilador também não me deixa mentir. Execute este meu código que vc citou e verá a seguinte mensagem:

Main.java:9: error: local variables referenced from an inner class must be final or effectively final
        System.out.println(x);
3 curtidas

Legal, pelo jeito a definição mudou.
Na época que fiz o curso da Sun, inner class era só as declaradas no corpo de outra classe.
Obrigado por me atualizar.
:smiley:

2 curtidas

Sobre a questão da variável não poder ser usada dentro do lambda, isso está descrito na especificação da linguagem:

Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression must either be final or effectively final (§4.12.4), as specified in §6.5.6.1.

The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems. Compared to the final restriction, it reduces the clerical burden on programmers.

Lá inclusive tem exemplos do que é válido e do que não é (e você pode seguir os links acima para as seções 4.12.4 e 6.5.6.1, para entender melhor cada conceito).

Mas de forma resumida, permitir o uso de variáveis que não são efetivamente final traria mais problemas do que não permitir, e esta foi a opção feita.

Tem mais detalhes aqui, incluindo este trecho:

It is our intent to prohibit capture of mutable local variables. The reason is that idioms like this:

int sum = 0;
list.forEach(e -> { sum += e.size(); });

are fundamentally serial; it is quite difficult to write lambda bodies like this that do not have race conditions. Unless we are willing to enforce—preferably at compile time—that such a function cannot escape its capturing thread, this feature may well cause more trouble than it solves.

E aqui tem mais:

You may be asking yourself why local variables have these restrictions. First, there’s a key difference in how instance and local variables are implemented behind the scenes. Instance variables are stored on the heap, whereas local variables live on the stack. If a lambda could access the local variable directly and the lambda were used in a thread, then the thread using the lambda could try to access the variable after the thread that allocated the variable had deallocated it. Hence, Java implements access to a free local variable as access to a copy of it rather than access to the original variable. This makes no difference if the local variable is assigned to only once—hence the restriction. Second, this restriction also discourages typical imperative programming patterns (which, as we explain in later chapters, prevent easy parallelization) that mutate an outer variable.

E também tem mais detalhes aqui.

Enfim, é por isso que não dá pra fazer numbersIterator.forEachRemaining(number -> sum += number);.

2 curtidas